@llblab/pi-telegram 0.52.2 → 0.53.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +9 -7
- package/BACKLOG.md +1 -18
- package/CHANGELOG.md +11 -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 +61 -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 +249 -126
- 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 +125 -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 +511 -300
- 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-follower.ts
CHANGED
|
@@ -6,64 +6,102 @@
|
|
|
6
6
|
* It must not spawn Pi processes or create hidden Telegram-originated instances.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { createHash } from "node:crypto";
|
|
9
10
|
import { basename } from "node:path";
|
|
10
11
|
import { isDeepStrictEqual } from "node:util";
|
|
11
|
-
import type { TelegramWorkspaceRestoreExecutor, TelegramWorkspaceRestoreIntent } from "./threads.ts";
|
|
12
12
|
|
|
13
|
-
import
|
|
14
|
-
import * as WorkspaceIdentity from "./workspace-identity.ts";
|
|
15
|
-
import { isWireRecord as isRecord } from "./wire.ts";
|
|
16
|
-
import { parseTelegramUpdateJournalQueueOwner, type TelegramUpdateJournalStoreOptions } from "./journal.ts";
|
|
17
|
-
import {
|
|
18
|
-
TELEGRAM_BUS_LEADER_STALE_HEARTBEAT_MS,
|
|
19
|
-
type TelegramLockEntry,
|
|
20
|
-
type TelegramLockState,
|
|
21
|
-
} from "./locks.ts";
|
|
13
|
+
import type { TelegramPreparedHeldCommand } from "./commands.ts";
|
|
22
14
|
import type {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
} from "./
|
|
26
|
-
import type {
|
|
15
|
+
TelegramWorkspaceRestoreExecutor,
|
|
16
|
+
TelegramWorkspaceRestoreIntent,
|
|
17
|
+
} from "./threads.ts";
|
|
18
|
+
import type {
|
|
19
|
+
TelegramLiveInputPreparation,
|
|
20
|
+
TelegramLiveSourceCompletionReadiness,
|
|
21
|
+
TelegramUpdateAdmissionLifecycleRuntime,
|
|
22
|
+
} from "./updates.ts";
|
|
23
|
+
|
|
27
24
|
import {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
} from "./telegram-api.ts";
|
|
25
|
+
getTelegramBusTransportRetryPolicy,
|
|
26
|
+
TELEGRAM_BUS_REGISTRATION_RETRY,
|
|
27
|
+
} from "./bus-transport.ts";
|
|
32
28
|
import {
|
|
33
29
|
createTelegramBusFollowerDeliveryIdentity,
|
|
34
30
|
createTelegramBusForeignOwnedUpdateForwarder,
|
|
35
31
|
createTelegramBusLocalServer,
|
|
36
32
|
createTelegramBusRequestIdFactory,
|
|
37
33
|
createUnauthorizedBusAck,
|
|
34
|
+
rejectTelegramBusRequest,
|
|
38
35
|
getTelegramBusProtocolCompatibility,
|
|
39
36
|
getTelegramBusSocketPath,
|
|
40
37
|
hasTelegramBusCapability,
|
|
41
38
|
isTelegramBusEnvelopeAuthorized,
|
|
39
|
+
parseTelegramBusEnvelope,
|
|
42
40
|
resolveTelegramBusSocketPath,
|
|
43
41
|
sendTelegramBusLocalEnvelope,
|
|
42
|
+
TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT,
|
|
43
|
+
TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_APPLY,
|
|
44
|
+
TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_COMMAND_SET,
|
|
45
|
+
TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SAVE,
|
|
46
|
+
TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SETTLE,
|
|
47
|
+
TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY,
|
|
48
|
+
TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT,
|
|
49
|
+
TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE,
|
|
50
|
+
TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE,
|
|
51
|
+
TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME,
|
|
52
|
+
TELEGRAM_BUS_HELD_COMMAND_CAPABILITIES,
|
|
53
|
+
TELEGRAM_BUS_LIVE_REBIND_CAPABILITIES,
|
|
54
|
+
TelegramBusLocalAuthorityError,
|
|
44
55
|
type TelegramBusAgentMessage,
|
|
45
56
|
type TelegramBusAgentTargetSelector,
|
|
46
57
|
type TelegramBusEnvelope,
|
|
47
|
-
type
|
|
58
|
+
type TelegramBusFollowerQueueHandoffOffer,
|
|
48
59
|
type TelegramBusForwardOwnership,
|
|
60
|
+
type TelegramBusLiveRebindApplyObservation,
|
|
61
|
+
type TelegramBusLiveRebindCommandObservation,
|
|
62
|
+
type TelegramBusLiveRebindSaveObservation,
|
|
63
|
+
type TelegramBusLiveRebindSettlementObservation,
|
|
64
|
+
type TelegramBusLiveRebindWorkObservation,
|
|
65
|
+
type TelegramBusLiveRebindWorkState,
|
|
49
66
|
type TelegramBusProtocolIdentity,
|
|
67
|
+
type TelegramBusSelectedMenuDeliveryObservation,
|
|
68
|
+
type TelegramBusSelectedMenuTextEffect,
|
|
50
69
|
type TelegramBusSocketPathSource,
|
|
51
|
-
|
|
52
|
-
TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE,
|
|
53
|
-
TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT,
|
|
54
|
-
TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT,
|
|
55
|
-
TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE,
|
|
70
|
+
type TelegramBusWorkspaceRestoreObservation,
|
|
56
71
|
} from "./bus.ts";
|
|
57
|
-
import type {
|
|
72
|
+
import type {
|
|
73
|
+
TelegramConfigStore,
|
|
74
|
+
TelegramThreadDisplayMode,
|
|
75
|
+
} from "./config.ts";
|
|
58
76
|
import {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
} from "./
|
|
77
|
+
parseTelegramUpdateJournalQueueOwner,
|
|
78
|
+
type TelegramUpdateJournalStoreOptions,
|
|
79
|
+
} from "./journal.ts";
|
|
80
|
+
import {
|
|
81
|
+
TELEGRAM_BUS_LEADER_STALE_HEARTBEAT_MS,
|
|
82
|
+
type TelegramLockEntry,
|
|
83
|
+
type TelegramLockState,
|
|
84
|
+
} from "./locks.ts";
|
|
85
|
+
import { isPiStaleContextError } from "./pi.ts";
|
|
86
|
+
import type { TelegramQueueHandoffStageResult } from "./queue.ts";
|
|
87
|
+
import {
|
|
88
|
+
parseTelegramTarget as parseTarget,
|
|
89
|
+
type TelegramTarget,
|
|
90
|
+
} from "./target.ts";
|
|
91
|
+
import {
|
|
92
|
+
isTelegramApiMethodRetrySafe,
|
|
93
|
+
TelegramApiAuthorityError,
|
|
94
|
+
TelegramApiCommitUnknownError,
|
|
95
|
+
TelegramApiStaleTargetError,
|
|
96
|
+
} from "./telegram-api.ts";
|
|
97
|
+
import * as Threads from "./threads.ts";
|
|
98
|
+
import { isWireRecord as isRecord } from "./wire.ts";
|
|
62
99
|
import {
|
|
63
100
|
createTelegramWorkspaceAdmissionOperationId,
|
|
64
101
|
runWithTelegramWorkspaceAdmissionsAsync,
|
|
65
102
|
type TelegramWorkspaceAdmissionLedger,
|
|
66
103
|
} from "./workspace-admission.ts";
|
|
104
|
+
import * as WorkspaceIdentity from "./workspace-identity.ts";
|
|
67
105
|
|
|
68
106
|
const TELEGRAM_BUS_FOLLOWER_PROMOTION_GRACE_MS = 2_500;
|
|
69
107
|
const TELEGRAM_FOLLOWER_SESSION_HANDOFF_TTL_MS = 30_000;
|
|
@@ -271,7 +309,9 @@ export interface TelegramBusFollowerClientRuntimeDeps<TMessage = unknown> {
|
|
|
271
309
|
getForwardCommentBatchPosition?: (
|
|
272
310
|
message: TMessage,
|
|
273
311
|
) => "comment" | "forward" | undefined;
|
|
274
|
-
validateForwardOwnership?: (
|
|
312
|
+
validateForwardOwnership?: (
|
|
313
|
+
ownership: TelegramBusForwardOwnership,
|
|
314
|
+
) => boolean;
|
|
275
315
|
recordRuntimeEvent?: (
|
|
276
316
|
category: string,
|
|
277
317
|
error: unknown,
|
|
@@ -354,10 +394,12 @@ export type TelegramBusFollowerPromotionHandler<TContext> = (
|
|
|
354
394
|
) => Promise<boolean>;
|
|
355
395
|
|
|
356
396
|
type TelegramBusFollowerWorkspaceAdmissionDeps = {
|
|
357
|
-
getWorkspaceAdmission?: () =>
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
397
|
+
getWorkspaceAdmission?: () =>
|
|
398
|
+
| Pick<
|
|
399
|
+
TelegramWorkspaceAdmissionLedger,
|
|
400
|
+
"acquireAdmission" | "releaseAdmission"
|
|
401
|
+
>
|
|
402
|
+
| undefined;
|
|
361
403
|
recordRuntimeEvent?: (
|
|
362
404
|
category: string,
|
|
363
405
|
error: unknown,
|
|
@@ -409,83 +451,84 @@ export function createTelegramBusFollowerPromotionHandler<
|
|
|
409
451
|
) => void;
|
|
410
452
|
getNowMs?: () => number;
|
|
411
453
|
getPid?: () => number;
|
|
412
|
-
getWorkspaceAdmission?:
|
|
413
|
-
TelegramBusFollowerWorkspaceAdmissionDeps["getWorkspaceAdmission"];
|
|
454
|
+
getWorkspaceAdmission?: TelegramBusFollowerWorkspaceAdmissionDeps["getWorkspaceAdmission"];
|
|
414
455
|
}): TelegramBusFollowerPromotionHandler<TContext> {
|
|
415
|
-
return (ctx, binding, election) =>
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
await Threads.promoteTelegramFollowerBindingToLeader(
|
|
423
|
-
|
|
424
|
-
|
|
456
|
+
return (ctx, binding, election) =>
|
|
457
|
+
runTelegramBusFollowerWorkspaceMutation(
|
|
458
|
+
input,
|
|
459
|
+
"workspace.promote-follower",
|
|
460
|
+
async () => {
|
|
461
|
+
let promotedRecord: Threads.TelegramTopicTargetRecord | undefined;
|
|
462
|
+
const promoted = await input.startLeader(ctx, election, async () => {
|
|
463
|
+
promotedRecord = await Threads.promoteTelegramFollowerBindingToLeader(
|
|
464
|
+
{
|
|
465
|
+
store: input.topicTargetStore,
|
|
466
|
+
instanceId: input.instanceId,
|
|
467
|
+
cwd: ctx.cwd,
|
|
468
|
+
sessionId: input.getSessionId?.(ctx),
|
|
469
|
+
telegramProfile: input.getActiveProfileName(),
|
|
470
|
+
target: binding.target,
|
|
471
|
+
slot: binding.slot,
|
|
472
|
+
threadName: binding.threadName,
|
|
473
|
+
},
|
|
474
|
+
);
|
|
475
|
+
if (!promotedRecord && typeof binding.target?.threadId === "number") {
|
|
476
|
+
throw new Error(
|
|
477
|
+
"Telegram follower promotion slot authority is unavailable.",
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
if (promotedRecord) {
|
|
481
|
+
input.recordRuntimeEvent(
|
|
482
|
+
"bus",
|
|
483
|
+
"Follower thread binding promoted to leader",
|
|
484
|
+
{
|
|
485
|
+
phase: "follower-promoted-binding",
|
|
486
|
+
chatId: promotedRecord.target.chatId,
|
|
487
|
+
threadId: promotedRecord.target.threadId,
|
|
488
|
+
slot: promotedRecord.slot,
|
|
489
|
+
threadName: promotedRecord.threadName,
|
|
490
|
+
},
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
if (
|
|
495
|
+
promoted &&
|
|
496
|
+
promotedRecord &&
|
|
497
|
+
typeof binding.target?.threadId === "number"
|
|
498
|
+
) {
|
|
499
|
+
const profileKey = Threads.getTelegramThreadOwnerKey({
|
|
500
|
+
kind: "leader",
|
|
425
501
|
cwd: ctx.cwd,
|
|
426
|
-
|
|
502
|
+
instanceId: input.instanceId,
|
|
427
503
|
telegramProfile: input.getActiveProfileName(),
|
|
428
|
-
target: binding.target,
|
|
429
|
-
slot: binding.slot,
|
|
430
|
-
threadName: binding.threadName,
|
|
431
504
|
});
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
505
|
+
Threads.setTelegramLeaderSessionHandoff({
|
|
506
|
+
pid: input.getPid?.() ?? process.pid,
|
|
507
|
+
instanceId: input.instanceId,
|
|
508
|
+
createdAtMs: input.getNowMs?.() ?? Date.now(),
|
|
509
|
+
profileKey,
|
|
510
|
+
target: {
|
|
511
|
+
chatId: binding.target.chatId,
|
|
512
|
+
threadId: binding.target.threadId,
|
|
513
|
+
},
|
|
514
|
+
slot: promotedRecord.slot,
|
|
515
|
+
threadName: promotedRecord.threadName,
|
|
516
|
+
});
|
|
438
517
|
input.recordRuntimeEvent(
|
|
439
518
|
"bus",
|
|
440
|
-
"
|
|
519
|
+
"Promoted leader binding retained for session replacement",
|
|
441
520
|
{
|
|
442
|
-
phase: "follower-promoted-
|
|
443
|
-
chatId:
|
|
444
|
-
threadId:
|
|
521
|
+
phase: "follower-promoted-session-handoff",
|
|
522
|
+
chatId: binding.target.chatId,
|
|
523
|
+
threadId: binding.target.threadId,
|
|
445
524
|
slot: promotedRecord.slot,
|
|
446
525
|
threadName: promotedRecord.threadName,
|
|
447
526
|
},
|
|
448
527
|
);
|
|
449
528
|
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
promotedRecord &&
|
|
454
|
-
typeof binding.target?.threadId === "number"
|
|
455
|
-
) {
|
|
456
|
-
const profileKey = Threads.getTelegramThreadOwnerKey({
|
|
457
|
-
kind: "leader",
|
|
458
|
-
cwd: ctx.cwd,
|
|
459
|
-
instanceId: input.instanceId,
|
|
460
|
-
telegramProfile: input.getActiveProfileName(),
|
|
461
|
-
});
|
|
462
|
-
Threads.setTelegramLeaderSessionHandoff({
|
|
463
|
-
pid: input.getPid?.() ?? process.pid,
|
|
464
|
-
instanceId: input.instanceId,
|
|
465
|
-
createdAtMs: input.getNowMs?.() ?? Date.now(),
|
|
466
|
-
profileKey,
|
|
467
|
-
target: {
|
|
468
|
-
chatId: binding.target.chatId,
|
|
469
|
-
threadId: binding.target.threadId,
|
|
470
|
-
},
|
|
471
|
-
slot: promotedRecord.slot,
|
|
472
|
-
threadName: promotedRecord.threadName,
|
|
473
|
-
});
|
|
474
|
-
input.recordRuntimeEvent(
|
|
475
|
-
"bus",
|
|
476
|
-
"Promoted leader binding retained for session replacement",
|
|
477
|
-
{
|
|
478
|
-
phase: "follower-promoted-session-handoff",
|
|
479
|
-
chatId: binding.target.chatId,
|
|
480
|
-
threadId: binding.target.threadId,
|
|
481
|
-
slot: promotedRecord.slot,
|
|
482
|
-
threadName: promotedRecord.threadName,
|
|
483
|
-
},
|
|
484
|
-
);
|
|
485
|
-
}
|
|
486
|
-
return promoted;
|
|
487
|
-
},
|
|
488
|
-
);
|
|
529
|
+
return promoted;
|
|
530
|
+
},
|
|
531
|
+
);
|
|
489
532
|
}
|
|
490
533
|
|
|
491
534
|
export type TelegramBusFollowerLeaderState =
|
|
@@ -538,13 +581,18 @@ export interface TelegramBusForwardedUpdateReceiverRuntimeDeps<TContext> {
|
|
|
538
581
|
getAuthSecret?: () => string | undefined;
|
|
539
582
|
getRegistrationGeneration: () => string | undefined;
|
|
540
583
|
getRecipientBindingKey: () => string | undefined;
|
|
584
|
+
/** Live preparation names the current journal, not the stable forwarding/profile identity. */
|
|
585
|
+
getLiveRebindJournalBindingKey?: () => string | undefined;
|
|
541
586
|
durableAdmission: TelegramBusFollowerDurableAdmissionPort<TContext>;
|
|
542
587
|
sourceReferenceAdmission?: TelegramBusFollowerDurableAdmissionPort<TContext>;
|
|
543
588
|
isSourceReferenceAdmissionEnabled?: () => boolean;
|
|
544
589
|
hasAuthenticatedSourceReferenceTransport?: () => boolean;
|
|
545
590
|
getContext: () => TContext | undefined;
|
|
546
591
|
handleInputCustodyHandoff?: (
|
|
547
|
-
envelope: Extract<
|
|
592
|
+
envelope: Extract<
|
|
593
|
+
TelegramBusEnvelope,
|
|
594
|
+
{ kind: "leader.offerInputCustodyHandoff" }
|
|
595
|
+
>,
|
|
548
596
|
ctx: TContext,
|
|
549
597
|
) => Promise<unknown> | unknown;
|
|
550
598
|
handleQueueHandoff?: (
|
|
@@ -553,10 +601,49 @@ export interface TelegramBusForwardedUpdateReceiverRuntimeDeps<TContext> {
|
|
|
553
601
|
{ kind: "leader.offerQueueHandoff" }
|
|
554
602
|
>,
|
|
555
603
|
ctx: TContext,
|
|
556
|
-
) =>
|
|
604
|
+
) =>
|
|
605
|
+
Promise<TelegramQueueHandoffStageResult> | TelegramQueueHandoffStageResult;
|
|
606
|
+
getSessionId?: () => string | undefined;
|
|
607
|
+
getLeaderProtocol?: () => TelegramBusProtocolIdentity | undefined;
|
|
608
|
+
getLocalProtocol?: () => TelegramBusProtocolIdentity | undefined;
|
|
609
|
+
isLiveRebindSaveEnabled?: () => boolean;
|
|
610
|
+
isLiveRebindApplyEnabled?: () => boolean;
|
|
611
|
+
isLiveRebindSettleEnabled?: () => boolean;
|
|
612
|
+
handleLiveRebindSettle?: (
|
|
613
|
+
envelope: Extract<TelegramBusEnvelope, { kind: "leader.settleLiveRebind" }>,
|
|
614
|
+
ctx: TContext,
|
|
615
|
+
isCurrent: () => boolean,
|
|
616
|
+
) => Promise<
|
|
617
|
+
| TelegramBusLiveRebindSettlementObservation
|
|
618
|
+
| TelegramBusLiveRebindWorkObservation
|
|
619
|
+
| TelegramBusLiveRebindCommandObservation
|
|
620
|
+
>;
|
|
621
|
+
handleLiveRebindApply?: (
|
|
622
|
+
envelope: Extract<TelegramBusEnvelope, { kind: "leader.applyLiveRebind" }>,
|
|
623
|
+
ctx: TContext,
|
|
624
|
+
isCurrent: () => boolean,
|
|
625
|
+
) => Promise<TelegramBusLiveRebindApplyObservation>;
|
|
626
|
+
/** Availability only; never future recipient activation or a command execution grant. */
|
|
627
|
+
isLiveRebindCommandSetEnabled?: () => boolean;
|
|
628
|
+
handleLiveRebindSave?: (
|
|
629
|
+
envelope: Extract<
|
|
630
|
+
TelegramBusEnvelope,
|
|
631
|
+
{ kind: "leader.prepareLiveRebind" }
|
|
632
|
+
>,
|
|
633
|
+
ctx: TContext,
|
|
634
|
+
isCurrent: () => boolean,
|
|
635
|
+
) =>
|
|
636
|
+
| TelegramBusLiveRebindSaveObservation
|
|
637
|
+
| Promise<TelegramBusLiveRebindSaveObservation>;
|
|
557
638
|
isWorkspaceRestoreEnabled?: () => boolean;
|
|
558
|
-
handleWorkspaceRestore?: (
|
|
559
|
-
|
|
639
|
+
handleWorkspaceRestore?: (
|
|
640
|
+
input: {
|
|
641
|
+
operationId: string;
|
|
642
|
+
registrationGeneration: string;
|
|
643
|
+
mode: "apply" | "inspect";
|
|
644
|
+
},
|
|
645
|
+
ctx: TContext,
|
|
646
|
+
) => Promise<TelegramBusWorkspaceRestoreObservation>;
|
|
560
647
|
recordRuntimeEvent?: (
|
|
561
648
|
category: string,
|
|
562
649
|
error: unknown,
|
|
@@ -582,9 +669,7 @@ export interface TelegramBusFollowerRuntimeAssemblyPorts<
|
|
|
582
669
|
) => void;
|
|
583
670
|
receiver: Omit<
|
|
584
671
|
TelegramBusForwardedUpdateReceiverRuntimeDeps<TContext>,
|
|
585
|
-
| "
|
|
586
|
-
| "recordRuntimeEvent"
|
|
587
|
-
| "getRegistrationGeneration"
|
|
672
|
+
"instanceId" | "recordRuntimeEvent" | "getRegistrationGeneration"
|
|
588
673
|
>;
|
|
589
674
|
recovery: Omit<
|
|
590
675
|
TelegramBusFollowerHeartbeatRecoveryHandlerDeps<TContext>,
|
|
@@ -610,25 +695,47 @@ export function createTelegramBusFollowerRuntimeAssembly<
|
|
|
610
695
|
ports: TelegramBusFollowerRuntimeAssemblyPorts<TContext>,
|
|
611
696
|
): TelegramBusFollowerRuntimeAssembly<TContext> {
|
|
612
697
|
// Binding startup needs registration identity before it can grant inbound authority.
|
|
613
|
-
let readyContext:
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
698
|
+
let readyContext:
|
|
699
|
+
| {
|
|
700
|
+
generation: string;
|
|
701
|
+
ctx: TContext;
|
|
702
|
+
sessionGeneration: number | undefined;
|
|
703
|
+
sessionId: string | undefined;
|
|
704
|
+
}
|
|
705
|
+
| undefined;
|
|
706
|
+
const isReadyContext = (ctx?: TContext): boolean =>
|
|
707
|
+
Boolean(
|
|
708
|
+
readyContext &&
|
|
709
|
+
(ctx === undefined || readyContext.ctx === ctx) &&
|
|
710
|
+
ports.registrationState.getGeneration() === readyContext.generation &&
|
|
711
|
+
ports.registration.getSessionGeneration?.() ===
|
|
712
|
+
readyContext.sessionGeneration &&
|
|
713
|
+
ports.registration.getSessionId?.(readyContext.ctx) ===
|
|
714
|
+
readyContext.sessionId &&
|
|
715
|
+
ports.registration.isContextActive?.(readyContext.ctx) !== false,
|
|
716
|
+
);
|
|
620
717
|
const prepareContext = async (ctx: TContext): Promise<void> => {
|
|
621
718
|
const generation = ports.registrationState.getGeneration();
|
|
622
719
|
const sessionGeneration = ports.registration.getSessionGeneration?.();
|
|
623
720
|
const sessionId = ports.registration.getSessionId?.(ctx);
|
|
624
721
|
readyContext = undefined;
|
|
625
|
-
if (
|
|
626
|
-
|
|
722
|
+
if (
|
|
723
|
+
!generation ||
|
|
724
|
+
(sessionId !== undefined &&
|
|
725
|
+
ports.registrationState.getSessionId(sessionId) !== sessionId)
|
|
726
|
+
)
|
|
727
|
+
throw new Error(
|
|
728
|
+
"Telegram follower session change requires acknowledged leader registration.",
|
|
729
|
+
);
|
|
627
730
|
await ports.registration.onRegistered?.(ctx);
|
|
628
|
-
if (
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
731
|
+
if (
|
|
732
|
+
generation &&
|
|
733
|
+
ports.registrationState.getGeneration() === generation &&
|
|
734
|
+
ports.registration.getSessionGeneration?.() === sessionGeneration &&
|
|
735
|
+
ports.registration.getSessionId?.(ctx) === sessionId &&
|
|
736
|
+
ports.registration.isContextActive?.(ctx) !== false
|
|
737
|
+
)
|
|
738
|
+
readyContext = { generation, ctx, sessionGeneration, sessionId };
|
|
632
739
|
};
|
|
633
740
|
const sharedRuntimeDeps = {
|
|
634
741
|
instanceId: ports.instanceId,
|
|
@@ -665,12 +772,20 @@ export function createTelegramBusFollowerRuntimeAssembly<
|
|
|
665
772
|
const sessionGeneration = ports.registration.getSessionGeneration?.();
|
|
666
773
|
if (ports.registration.isContextActive?.(ctx) === false) return;
|
|
667
774
|
await baseRegistration.setContext(ctx);
|
|
668
|
-
if (
|
|
669
|
-
|
|
775
|
+
if (
|
|
776
|
+
ports.registration.getSessionGeneration?.() !== sessionGeneration ||
|
|
777
|
+
ports.registration.isContextActive?.(ctx) === false
|
|
778
|
+
)
|
|
779
|
+
return;
|
|
670
780
|
if (!isReadyContext(ctx)) await prepareContext(ctx);
|
|
671
781
|
},
|
|
672
782
|
};
|
|
673
|
-
return {
|
|
783
|
+
return {
|
|
784
|
+
receiver,
|
|
785
|
+
registration,
|
|
786
|
+
getReadySessionId: () =>
|
|
787
|
+
isReadyContext() ? readyContext?.sessionId : undefined,
|
|
788
|
+
};
|
|
674
789
|
}
|
|
675
790
|
|
|
676
791
|
export interface TelegramBusFollowerRestoreContext {
|
|
@@ -694,122 +809,622 @@ export function createTelegramBusFollowerRestoreContextGetter<TContext>(deps: {
|
|
|
694
809
|
getLeaderState: () => TelegramLockState;
|
|
695
810
|
getAuthenticatedSecret: () => string | undefined;
|
|
696
811
|
getLeaderProtocol: () => TelegramBusProtocolIdentity | undefined;
|
|
812
|
+
capability?:
|
|
813
|
+
| typeof TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE
|
|
814
|
+
| typeof TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_APPLY
|
|
815
|
+
| typeof TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY;
|
|
697
816
|
}) {
|
|
698
817
|
return (ctx: TContext): TelegramBusFollowerRestoreContext | undefined => {
|
|
699
818
|
if (!deps.isContextCurrent(ctx)) return undefined;
|
|
700
819
|
const leader = deps.getLeaderState();
|
|
701
820
|
const secret = deps.getAuthenticatedSecret();
|
|
702
821
|
const leaderProtocol = deps.getLeaderProtocol();
|
|
703
|
-
if (
|
|
704
|
-
|
|
705
|
-
|
|
822
|
+
if (
|
|
823
|
+
!leaderProtocol ||
|
|
824
|
+
!hasTelegramBusCapability(
|
|
825
|
+
leaderProtocol,
|
|
826
|
+
deps.capability ?? TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE,
|
|
827
|
+
)
|
|
828
|
+
)
|
|
829
|
+
return undefined;
|
|
830
|
+
if (
|
|
831
|
+
leader.kind !== "active-elsewhere" ||
|
|
832
|
+
!secret ||
|
|
833
|
+
leader.lock.busSecret !== secret ||
|
|
834
|
+
!leader.lock.instanceId ||
|
|
835
|
+
leader.lock.leaderEpoch === undefined
|
|
836
|
+
)
|
|
837
|
+
return undefined;
|
|
706
838
|
const profileBindingKey = deps.getProfileBindingKey();
|
|
707
839
|
const operatorUserId = deps.getOperatorUserId();
|
|
708
840
|
const sessionId = deps.getSessionId(ctx);
|
|
709
841
|
const cwd = deps.getCwd(ctx);
|
|
710
842
|
const generation = deps.getGeneration();
|
|
711
|
-
if (
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
843
|
+
if (
|
|
844
|
+
!profileBindingKey ||
|
|
845
|
+
!sessionId ||
|
|
846
|
+
!cwd ||
|
|
847
|
+
typeof operatorUserId !== "number" ||
|
|
848
|
+
!Number.isSafeInteger(operatorUserId) ||
|
|
849
|
+
operatorUserId <= 0 ||
|
|
850
|
+
!Number.isSafeInteger(generation) ||
|
|
851
|
+
generation < 0 ||
|
|
852
|
+
!deps.isContextCurrent(ctx)
|
|
853
|
+
)
|
|
854
|
+
return undefined;
|
|
855
|
+
return {
|
|
856
|
+
executor: {
|
|
857
|
+
instanceId: leader.lock.instanceId,
|
|
858
|
+
leaderEpoch: String(leader.lock.leaderEpoch),
|
|
859
|
+
},
|
|
860
|
+
profileBindingKey,
|
|
861
|
+
operatorUserId,
|
|
862
|
+
sessionId,
|
|
863
|
+
cwd,
|
|
864
|
+
generation,
|
|
865
|
+
leaderProtocol: structuredClone(leaderProtocol),
|
|
866
|
+
};
|
|
716
867
|
};
|
|
717
868
|
}
|
|
718
869
|
|
|
719
|
-
/**
|
|
720
|
-
export function createTelegramBusFollowerWorkspaceRestoreHandler<
|
|
870
|
+
/** Shared canonical/local target owner and scoped live-carrier hooks; caller authenticates transport, no canonical writes or cleanup. */
|
|
871
|
+
export function createTelegramBusFollowerWorkspaceRestoreHandler<
|
|
872
|
+
TContext,
|
|
873
|
+
>(deps: {
|
|
721
874
|
instanceId: string;
|
|
722
875
|
/** Captures actual Pi lifetime and authenticated leader/profile authority, never request-derived values. */
|
|
723
|
-
getContextAuthority: (
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
876
|
+
getContextAuthority: (
|
|
877
|
+
ctx: TContext,
|
|
878
|
+
) => TelegramBusFollowerRestoreContext | undefined;
|
|
879
|
+
readRestoreIntent: (
|
|
880
|
+
operationId: string,
|
|
881
|
+
profileBindingKey: string,
|
|
882
|
+
) => TelegramWorkspaceRestoreIntent | undefined;
|
|
883
|
+
readLiveRebindIntent?: (
|
|
884
|
+
operationId: string,
|
|
885
|
+
profileBindingKey: string,
|
|
886
|
+
) => Threads.TelegramWorkspaceLiveRebindIntent | undefined;
|
|
887
|
+
topicTargetStore: Pick<
|
|
888
|
+
Threads.TelegramTopicTargetStore,
|
|
889
|
+
"load" | "withWorkspaceRestoreSnapshot"
|
|
890
|
+
> &
|
|
891
|
+
Partial<
|
|
892
|
+
Pick<Threads.TelegramTopicTargetStore, "withWorkspaceLiveRebindSnapshot">
|
|
893
|
+
>;
|
|
894
|
+
registrationState: Pick<
|
|
895
|
+
TelegramBusFollowerRegistrationState,
|
|
896
|
+
| "getTarget"
|
|
897
|
+
| "getSlot"
|
|
898
|
+
| "getGeneration"
|
|
899
|
+
| "setRegistered"
|
|
900
|
+
| "getLeaderProtocol"
|
|
901
|
+
>;
|
|
902
|
+
getWorkspaceAdmission: NonNullable<
|
|
903
|
+
TelegramBusFollowerWorkspaceAdmissionDeps["getWorkspaceAdmission"]
|
|
904
|
+
>;
|
|
728
905
|
recordRuntimeEvent?: TelegramBusFollowerWorkspaceAdmissionDeps["recordRuntimeEvent"];
|
|
729
906
|
}) {
|
|
730
|
-
|
|
907
|
+
const handler = async (
|
|
908
|
+
input: {
|
|
909
|
+
operationId: string;
|
|
910
|
+
registrationGeneration: string;
|
|
911
|
+
mode: "apply" | "inspect";
|
|
912
|
+
liveRebind?: {
|
|
913
|
+
sourceUpdateIds: readonly number[];
|
|
914
|
+
isCurrent(): boolean;
|
|
915
|
+
/** Captured prepared-branch target; equality must precede local apply or release grants. */
|
|
916
|
+
expectedTarget?: TelegramTarget & { threadId: number };
|
|
917
|
+
release?: (canRelease: () => boolean) => void;
|
|
918
|
+
observeWork?: (
|
|
919
|
+
oldTarget: TelegramTarget & { threadId: number },
|
|
920
|
+
) => void;
|
|
921
|
+
};
|
|
922
|
+
},
|
|
923
|
+
ctx: TContext,
|
|
924
|
+
) => {
|
|
925
|
+
const scope = input.liveRebind,
|
|
926
|
+
ports = scope && {
|
|
927
|
+
isCurrent: scope.isCurrent,
|
|
928
|
+
release: scope.release,
|
|
929
|
+
observeWork: scope.observeWork,
|
|
930
|
+
};
|
|
931
|
+
const suppliedTarget = scope?.expectedTarget,
|
|
932
|
+
expectedTarget = suppliedTarget && { ...suppliedTarget };
|
|
933
|
+
const sourceUpdateIds =
|
|
934
|
+
scope && [...scope.sourceUpdateIds].sort((a, b) => a - b);
|
|
935
|
+
const liveCurrent = scope && ports!.isCurrent.bind(scope),
|
|
936
|
+
release = scope && ports!.release?.bind(scope),
|
|
937
|
+
workObserver = scope && ports!.observeWork?.bind(scope);
|
|
731
938
|
const captured = deps.getContextAuthority(ctx);
|
|
732
939
|
if (!captured) throw new Error("Stale Telegram follower Restore context.");
|
|
733
940
|
const authority = structuredClone(captured);
|
|
734
|
-
const request = structuredClone(
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
941
|
+
const request = structuredClone({
|
|
942
|
+
operationId: input.operationId,
|
|
943
|
+
registrationGeneration: input.registrationGeneration,
|
|
944
|
+
mode: input.mode,
|
|
945
|
+
});
|
|
946
|
+
const readIntent = () =>
|
|
947
|
+
liveCurrent
|
|
948
|
+
? deps.readLiveRebindIntent?.(
|
|
949
|
+
request.operationId,
|
|
950
|
+
authority.profileBindingKey,
|
|
951
|
+
)
|
|
952
|
+
: deps.readRestoreIntent(
|
|
953
|
+
request.operationId,
|
|
954
|
+
authority.profileBindingKey,
|
|
955
|
+
);
|
|
956
|
+
let sampleWork: (() => void) | undefined;
|
|
957
|
+
if (suppliedTarget !== undefined && !expectedTarget)
|
|
958
|
+
throw new Error("Invalid prepared live-rebind target.");
|
|
959
|
+
if (
|
|
960
|
+
expectedTarget &&
|
|
961
|
+
(!Number.isSafeInteger(expectedTarget.chatId) ||
|
|
962
|
+
expectedTarget.chatId !== authority.operatorUserId ||
|
|
963
|
+
!Number.isSafeInteger(expectedTarget.threadId) ||
|
|
964
|
+
expectedTarget.threadId <= 0)
|
|
965
|
+
)
|
|
966
|
+
throw new Error("Invalid prepared live-rebind target.");
|
|
967
|
+
if (
|
|
968
|
+
(release || workObserver) &&
|
|
969
|
+
!hasTelegramBusCapability(
|
|
970
|
+
authority.leaderProtocol,
|
|
971
|
+
TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SETTLE,
|
|
972
|
+
)
|
|
973
|
+
)
|
|
974
|
+
throw new Error("Live-rebind settlement capability is unavailable.");
|
|
975
|
+
if (
|
|
976
|
+
liveCurrent &&
|
|
977
|
+
!hasTelegramBusCapability(
|
|
978
|
+
authority.leaderProtocol,
|
|
979
|
+
TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_APPLY,
|
|
980
|
+
)
|
|
981
|
+
)
|
|
982
|
+
throw new Error("Live-rebind apply capability is unavailable.");
|
|
983
|
+
if (
|
|
984
|
+
!authority.profileBindingKey ||
|
|
985
|
+
!authority.sessionId ||
|
|
986
|
+
!authority.cwd ||
|
|
987
|
+
!authority.executor?.instanceId ||
|
|
988
|
+
!authority.executor.leaderEpoch ||
|
|
989
|
+
!(typeof authority.generation === "string"
|
|
990
|
+
? authority.generation.length > 0
|
|
991
|
+
: Number.isSafeInteger(authority.generation) &&
|
|
992
|
+
authority.generation >= 0) ||
|
|
993
|
+
!Number.isSafeInteger(authority.operatorUserId) ||
|
|
994
|
+
authority.operatorUserId <= 0 ||
|
|
995
|
+
!request.operationId ||
|
|
996
|
+
!request.registrationGeneration ||
|
|
997
|
+
!["apply", "inspect"].includes(request.mode)
|
|
998
|
+
)
|
|
741
999
|
throw new Error("Invalid Telegram follower Restore authority.");
|
|
742
1000
|
const assertCurrent = (): void => {
|
|
743
|
-
if (
|
|
744
|
-
|
|
1001
|
+
if (
|
|
1002
|
+
!isDeepStrictEqual(deps.getContextAuthority(ctx), authority) ||
|
|
1003
|
+
liveCurrent?.() === false ||
|
|
1004
|
+
(scope &&
|
|
1005
|
+
(input.liveRebind !== scope ||
|
|
1006
|
+
Object.entries(ports!).some(
|
|
1007
|
+
([key, value]) => Reflect.get(scope, key) !== value,
|
|
1008
|
+
))) ||
|
|
1009
|
+
deps.registrationState.getGeneration() !==
|
|
1010
|
+
request.registrationGeneration
|
|
1011
|
+
)
|
|
745
1012
|
throw new Error("Stale Telegram follower Restore authority.");
|
|
746
1013
|
};
|
|
747
1014
|
assertCurrent();
|
|
748
|
-
if (!deps.getWorkspaceAdmission)
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
1015
|
+
if (!deps.getWorkspaceAdmission)
|
|
1016
|
+
throw new Error("Telegram Workspace admission authority is unavailable.");
|
|
1017
|
+
const result = await runTelegramBusFollowerWorkspaceMutation(
|
|
1018
|
+
deps,
|
|
1019
|
+
"workspace.restore-follower-target",
|
|
1020
|
+
async () => {
|
|
1021
|
+
assertCurrent();
|
|
1022
|
+
const intent = readIntent();
|
|
1023
|
+
assertCurrent();
|
|
1024
|
+
if (
|
|
1025
|
+
!intent ||
|
|
1026
|
+
intent.request.operationId !== request.operationId ||
|
|
1027
|
+
(liveCurrent
|
|
1028
|
+
? !("kind" in intent) ||
|
|
1029
|
+
intent.kind !== "live-rebind" ||
|
|
1030
|
+
!isDeepStrictEqual(
|
|
1031
|
+
[...intent.request.source.updateIds].sort((a, b) => a - b),
|
|
1032
|
+
sourceUpdateIds,
|
|
1033
|
+
) ||
|
|
1034
|
+
(request.mode === "apply"
|
|
1035
|
+
? intent.phase !== "rebound"
|
|
1036
|
+
: !["rebound", "released", "finished"].includes(intent.phase))
|
|
1037
|
+
: "kind" in intent ||
|
|
1038
|
+
(intent.phase !== "recipient-issued" &&
|
|
1039
|
+
intent.phase !== "ready")) ||
|
|
1040
|
+
(expectedTarget &&
|
|
1041
|
+
!isDeepStrictEqual(intent.request.target, expectedTarget)) ||
|
|
755
1042
|
!isDeepStrictEqual(intent.executor, authority.executor) ||
|
|
756
|
-
intent.operatorUserId !== authority.operatorUserId ||
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
1043
|
+
intent.operatorUserId !== authority.operatorUserId ||
|
|
1044
|
+
!intent.recipient ||
|
|
1045
|
+
((request.mode === "apply" || liveCurrent) &&
|
|
1046
|
+
(intent.recipient.kind !== "follower" ||
|
|
1047
|
+
intent.recipient.instanceId !== deps.instanceId ||
|
|
1048
|
+
intent.recipient.generation !==
|
|
1049
|
+
request.registrationGeneration)) ||
|
|
1050
|
+
intent.recipient.sessionId !== authority.sessionId ||
|
|
1051
|
+
intent.request.binding.sessionId !== authority.sessionId ||
|
|
1052
|
+
intent.request.binding.cwd !==
|
|
1053
|
+
WorkspaceIdentity.normalizeTelegramWorkspacePath(authority.cwd)
|
|
1054
|
+
)
|
|
1055
|
+
throw new Error(
|
|
1056
|
+
"Telegram follower Restore intent does not match this recipient.",
|
|
1057
|
+
);
|
|
1058
|
+
const expected = structuredClone(intent);
|
|
1059
|
+
await deps.topicTargetStore.load();
|
|
768
1060
|
assertCurrent();
|
|
769
|
-
const
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
1061
|
+
const observe = (
|
|
1062
|
+
canonical: Readonly<
|
|
1063
|
+
Pick<
|
|
1064
|
+
Threads.TelegramTopicTargetFile,
|
|
1065
|
+
"threads" | "workspaceBindings"
|
|
1066
|
+
>
|
|
1067
|
+
>,
|
|
1068
|
+
): TelegramBusWorkspaceRestoreObservation => {
|
|
1069
|
+
if (!isDeepStrictEqual(readIntent(), expected))
|
|
1070
|
+
throw new Error("Telegram follower Restore intent changed.");
|
|
1071
|
+
assertCurrent();
|
|
1072
|
+
const { binding, target } = expected.request;
|
|
1073
|
+
const bindings = (canonical.workspaceBindings ?? []).filter(
|
|
1074
|
+
(value) => value.bindingKey === binding.bindingKey,
|
|
1075
|
+
);
|
|
1076
|
+
const snapshot = canonical.threads;
|
|
1077
|
+
const records = snapshot.filter(
|
|
1078
|
+
(value) => value.instanceId === deps.instanceId,
|
|
1079
|
+
);
|
|
1080
|
+
const owners = snapshot.filter(
|
|
1081
|
+
(value) =>
|
|
1082
|
+
value.status === "active" &&
|
|
1083
|
+
(value.slot === binding.slot ||
|
|
1084
|
+
isDeepStrictEqual(value.target, target)),
|
|
1085
|
+
);
|
|
1086
|
+
if (
|
|
1087
|
+
bindings.length !== 1 ||
|
|
1088
|
+
records.length !== 1 ||
|
|
1089
|
+
owners.length !== 1 ||
|
|
1090
|
+
bindings[0]?.cwd !== binding.cwd ||
|
|
1091
|
+
bindings[0]?.sessionId !== binding.sessionId ||
|
|
1092
|
+
bindings[0]?.slot !== binding.slot ||
|
|
1093
|
+
bindings[0]?.inactiveSinceMs !== undefined ||
|
|
1094
|
+
!isDeepStrictEqual(bindings[0]?.target, target) ||
|
|
1095
|
+
records[0]?.status !== "active" ||
|
|
778
1096
|
records[0]?.owner?.kind !== "manual-follower" ||
|
|
779
|
-
records[0]?.slot !== binding.slot ||
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
if (
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
1097
|
+
records[0]?.slot !== binding.slot ||
|
|
1098
|
+
!isDeepStrictEqual(records[0]?.target, target) ||
|
|
1099
|
+
deps.registrationState.getSlot() !== binding.slot
|
|
1100
|
+
)
|
|
1101
|
+
throw new Error(
|
|
1102
|
+
"Telegram follower Restore canonical binding is not committed.",
|
|
1103
|
+
);
|
|
1104
|
+
const localTarget = deps.registrationState.getTarget();
|
|
1105
|
+
const alreadyReady = isDeepStrictEqual(localTarget, target);
|
|
1106
|
+
if (!alreadyReady && !isDeepStrictEqual(localTarget, binding.target))
|
|
1107
|
+
throw new Error("Telegram follower Restore local target changed.");
|
|
1108
|
+
assertCurrent();
|
|
1109
|
+
if (!alreadyReady && request.mode === "apply") {
|
|
1110
|
+
if (
|
|
1111
|
+
expected.phase !== (liveCurrent ? "rebound" : "recipient-issued")
|
|
1112
|
+
)
|
|
1113
|
+
throw new Error("Telegram follower Restore readiness regressed.");
|
|
1114
|
+
deps.registrationState.setRegistered(true, target, {
|
|
1115
|
+
slot: binding.slot,
|
|
1116
|
+
threadName: records[0]?.threadName,
|
|
1117
|
+
generation: request.registrationGeneration,
|
|
1118
|
+
leaderProtocol: deps.registrationState.getLeaderProtocol(),
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
assertCurrent();
|
|
1122
|
+
const observedTarget = deps.registrationState.getTarget();
|
|
1123
|
+
if (
|
|
1124
|
+
!observedTarget ||
|
|
1125
|
+
(!isDeepStrictEqual(observedTarget, target) &&
|
|
1126
|
+
!isDeepStrictEqual(observedTarget, binding.target))
|
|
1127
|
+
)
|
|
1128
|
+
throw new Error("Telegram follower Restore local target changed.");
|
|
1129
|
+
const ready = isDeepStrictEqual(observedTarget, target);
|
|
1130
|
+
assertCurrent();
|
|
1131
|
+
return {
|
|
1132
|
+
operationId: request.operationId,
|
|
1133
|
+
recipient: {
|
|
1134
|
+
kind: "follower" as const,
|
|
1135
|
+
instanceId: deps.instanceId,
|
|
1136
|
+
sessionId: authority.sessionId,
|
|
1137
|
+
generation: request.registrationGeneration,
|
|
1138
|
+
},
|
|
1139
|
+
target: {
|
|
1140
|
+
chatId: observedTarget.chatId,
|
|
1141
|
+
threadId: observedTarget.threadId!,
|
|
1142
|
+
},
|
|
1143
|
+
slot: binding.slot!,
|
|
1144
|
+
ready,
|
|
1145
|
+
};
|
|
1146
|
+
};
|
|
1147
|
+
let result: TelegramBusWorkspaceRestoreObservation | undefined;
|
|
1148
|
+
deps.topicTargetStore.withWorkspaceRestoreSnapshot(
|
|
1149
|
+
expected,
|
|
1150
|
+
(snapshot) => {
|
|
1151
|
+
result = observe(snapshot);
|
|
1152
|
+
},
|
|
1153
|
+
);
|
|
1154
|
+
if (!result)
|
|
1155
|
+
throw new Error(
|
|
1156
|
+
"Telegram follower Restore observation was not completed.",
|
|
1157
|
+
);
|
|
1158
|
+
if (release || workObserver) {
|
|
1159
|
+
if (
|
|
1160
|
+
!("kind" in expected) ||
|
|
1161
|
+
expected.kind !== "live-rebind" ||
|
|
1162
|
+
expected.phase !== "released" ||
|
|
1163
|
+
!result.ready ||
|
|
1164
|
+
request.mode !== "inspect"
|
|
1165
|
+
)
|
|
1166
|
+
throw new Error(
|
|
1167
|
+
"Live-rebind dispatch has no canonical released/local-applied grant.",
|
|
1168
|
+
);
|
|
1169
|
+
// Journal reads stay outside the canonical transaction; each barrier check reobserves only Workspace authority.
|
|
1170
|
+
if (workObserver)
|
|
1171
|
+
sampleWork = () => {
|
|
1172
|
+
deps.topicTargetStore.withWorkspaceRestoreSnapshot(
|
|
1173
|
+
expected,
|
|
1174
|
+
(snapshot) => {
|
|
1175
|
+
if (!observe(snapshot).ready)
|
|
1176
|
+
throw new Error(
|
|
1177
|
+
"Live-rebind work observation lost local target application.",
|
|
1178
|
+
);
|
|
1179
|
+
workObserver(expected.request.binding.target);
|
|
1180
|
+
assertCurrent();
|
|
1181
|
+
},
|
|
1182
|
+
);
|
|
1183
|
+
};
|
|
1184
|
+
release?.(() => {
|
|
1185
|
+
assertCurrent();
|
|
1186
|
+
let ready = false;
|
|
1187
|
+
deps.topicTargetStore.withWorkspaceRestoreSnapshot(
|
|
1188
|
+
expected,
|
|
1189
|
+
(snapshot) => {
|
|
1190
|
+
ready = observe(snapshot).ready;
|
|
1191
|
+
},
|
|
1192
|
+
);
|
|
1193
|
+
assertCurrent();
|
|
1194
|
+
return ready;
|
|
1195
|
+
});
|
|
792
1196
|
}
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
return result;
|
|
807
|
-
});
|
|
1197
|
+
return result;
|
|
1198
|
+
},
|
|
1199
|
+
);
|
|
1200
|
+
assertCurrent();
|
|
1201
|
+
if (
|
|
1202
|
+
!isDeepStrictEqual(deps.registrationState.getTarget(), result.target) ||
|
|
1203
|
+
deps.registrationState.getSlot() !== result.slot
|
|
1204
|
+
)
|
|
1205
|
+
throw new Error(
|
|
1206
|
+
"Telegram follower Restore local registration changed after admission release.",
|
|
1207
|
+
);
|
|
1208
|
+
// Work can change while admission releases; sample only after that await under a fresh canonical snapshot.
|
|
1209
|
+
sampleWork?.();
|
|
808
1210
|
assertCurrent();
|
|
809
|
-
if (!isDeepStrictEqual(deps.registrationState.getTarget(), result.target) || deps.registrationState.getSlot() !== result.slot)
|
|
810
|
-
throw new Error("Telegram follower Restore local registration changed after admission release.");
|
|
811
1211
|
return result;
|
|
812
1212
|
};
|
|
1213
|
+
return Object.assign(handler, {
|
|
1214
|
+
/** Capture availability only; asserting current recipient requires canonical released/local-applied ownership. */
|
|
1215
|
+
prepareLiveCommandRecipient(
|
|
1216
|
+
input: {
|
|
1217
|
+
operationId: string;
|
|
1218
|
+
registrationGeneration: string;
|
|
1219
|
+
sessionId: string;
|
|
1220
|
+
sourceUpdateIds: readonly number[];
|
|
1221
|
+
target: TelegramTarget & { threadId: number };
|
|
1222
|
+
},
|
|
1223
|
+
ctx: TContext,
|
|
1224
|
+
):
|
|
1225
|
+
| Pick<
|
|
1226
|
+
TelegramBusFollowerCommandOwner<TContext>,
|
|
1227
|
+
"isCurrent" | "assertRecipientCurrent"
|
|
1228
|
+
>
|
|
1229
|
+
| undefined {
|
|
1230
|
+
const expected = structuredClone(input),
|
|
1231
|
+
getAuthority = deps.getContextAuthority,
|
|
1232
|
+
read = deps.readLiveRebindIntent,
|
|
1233
|
+
store = deps.topicTargetStore;
|
|
1234
|
+
const snapshot = store.withWorkspaceLiveRebindSnapshot,
|
|
1235
|
+
state = deps.registrationState;
|
|
1236
|
+
const statePorts = {
|
|
1237
|
+
getTarget: state.getTarget,
|
|
1238
|
+
getSlot: state.getSlot,
|
|
1239
|
+
getGeneration: state.getGeneration,
|
|
1240
|
+
getLeaderProtocol: state.getLeaderProtocol,
|
|
1241
|
+
};
|
|
1242
|
+
const instanceId = deps.instanceId,
|
|
1243
|
+
authority = structuredClone(getAuthority.call(deps, ctx)),
|
|
1244
|
+
oldTarget = structuredClone(state.getTarget()),
|
|
1245
|
+
slot = state.getSlot();
|
|
1246
|
+
if (
|
|
1247
|
+
!authority ||
|
|
1248
|
+
!read ||
|
|
1249
|
+
!snapshot ||
|
|
1250
|
+
!instanceId ||
|
|
1251
|
+
!slot ||
|
|
1252
|
+
!expected.operationId?.trim() ||
|
|
1253
|
+
!expected.registrationGeneration?.trim() ||
|
|
1254
|
+
expected.sessionId !== authority.sessionId ||
|
|
1255
|
+
expected.sourceUpdateIds.length !== 1 ||
|
|
1256
|
+
!Number.isSafeInteger(expected.sourceUpdateIds[0]) ||
|
|
1257
|
+
expected.sourceUpdateIds[0]! < 0 ||
|
|
1258
|
+
typeof authority.generation !== "number" ||
|
|
1259
|
+
!Number.isSafeInteger(authority.generation) ||
|
|
1260
|
+
authority.generation < 0 ||
|
|
1261
|
+
!authority.profileBindingKey?.trim() ||
|
|
1262
|
+
!authority.cwd?.trim() ||
|
|
1263
|
+
!authority.executor?.instanceId?.trim() ||
|
|
1264
|
+
!authority.executor.leaderEpoch?.trim() ||
|
|
1265
|
+
!Number.isSafeInteger(authority.operatorUserId) ||
|
|
1266
|
+
authority.operatorUserId <= 0 ||
|
|
1267
|
+
expected.target.chatId !== authority.operatorUserId ||
|
|
1268
|
+
!Number.isSafeInteger(expected.target.threadId) ||
|
|
1269
|
+
expected.target.threadId <= 0 ||
|
|
1270
|
+
!oldTarget ||
|
|
1271
|
+
oldTarget.chatId !== expected.target.chatId ||
|
|
1272
|
+
!Number.isSafeInteger(oldTarget.threadId) ||
|
|
1273
|
+
oldTarget.threadId! <= 0
|
|
1274
|
+
)
|
|
1275
|
+
return undefined;
|
|
1276
|
+
const portsCurrent = () =>
|
|
1277
|
+
deps.instanceId === instanceId &&
|
|
1278
|
+
deps.getContextAuthority === getAuthority &&
|
|
1279
|
+
deps.readLiveRebindIntent === read &&
|
|
1280
|
+
deps.topicTargetStore === store &&
|
|
1281
|
+
store.withWorkspaceLiveRebindSnapshot === snapshot &&
|
|
1282
|
+
deps.registrationState === state &&
|
|
1283
|
+
Object.entries(statePorts).every(
|
|
1284
|
+
([key, value]) => Reflect.get(state, key) === value,
|
|
1285
|
+
);
|
|
1286
|
+
const isCurrent = () => {
|
|
1287
|
+
try {
|
|
1288
|
+
if (
|
|
1289
|
+
!portsCurrent() ||
|
|
1290
|
+
!isDeepStrictEqual(getAuthority.call(deps, ctx), authority) ||
|
|
1291
|
+
state.getGeneration() !== expected.registrationGeneration ||
|
|
1292
|
+
state.getSlot() !== slot ||
|
|
1293
|
+
!isDeepStrictEqual(
|
|
1294
|
+
state.getLeaderProtocol(),
|
|
1295
|
+
authority.leaderProtocol,
|
|
1296
|
+
) ||
|
|
1297
|
+
![
|
|
1298
|
+
...TELEGRAM_BUS_LIVE_REBIND_CAPABILITIES,
|
|
1299
|
+
...TELEGRAM_BUS_HELD_COMMAND_CAPABILITIES,
|
|
1300
|
+
].every((cap) =>
|
|
1301
|
+
hasTelegramBusCapability(authority.leaderProtocol, cap),
|
|
1302
|
+
) ||
|
|
1303
|
+
![oldTarget, expected.target].some((target) =>
|
|
1304
|
+
isDeepStrictEqual(state.getTarget(), target),
|
|
1305
|
+
)
|
|
1306
|
+
)
|
|
1307
|
+
return false;
|
|
1308
|
+
return (
|
|
1309
|
+
portsCurrent() &&
|
|
1310
|
+
isDeepStrictEqual(getAuthority.call(deps, ctx), authority) &&
|
|
1311
|
+
state.getGeneration() === expected.registrationGeneration &&
|
|
1312
|
+
state.getSlot() === slot &&
|
|
1313
|
+
isDeepStrictEqual(
|
|
1314
|
+
state.getLeaderProtocol(),
|
|
1315
|
+
authority.leaderProtocol,
|
|
1316
|
+
) &&
|
|
1317
|
+
[oldTarget, expected.target].some((target) =>
|
|
1318
|
+
isDeepStrictEqual(state.getTarget(), target),
|
|
1319
|
+
) &&
|
|
1320
|
+
portsCurrent()
|
|
1321
|
+
);
|
|
1322
|
+
} catch {
|
|
1323
|
+
return false;
|
|
1324
|
+
}
|
|
1325
|
+
};
|
|
1326
|
+
if (!isCurrent()) return undefined;
|
|
1327
|
+
return {
|
|
1328
|
+
isCurrent,
|
|
1329
|
+
assertRecipientCurrent() {
|
|
1330
|
+
if (!isCurrent())
|
|
1331
|
+
throw new Error("Selected command recipient authority changed.");
|
|
1332
|
+
const value = read.call(
|
|
1333
|
+
deps,
|
|
1334
|
+
expected.operationId,
|
|
1335
|
+
authority.profileBindingKey,
|
|
1336
|
+
);
|
|
1337
|
+
if (
|
|
1338
|
+
!value ||
|
|
1339
|
+
value.kind !== "live-rebind" ||
|
|
1340
|
+
value.phase !== "released" ||
|
|
1341
|
+
value.cleanup ||
|
|
1342
|
+
value.request.operationId !== expected.operationId ||
|
|
1343
|
+
value.recipient.kind !== "follower" ||
|
|
1344
|
+
value.recipient.instanceId !== instanceId ||
|
|
1345
|
+
value.recipient.sessionId !== expected.sessionId ||
|
|
1346
|
+
value.recipient.generation !== expected.registrationGeneration ||
|
|
1347
|
+
!isDeepStrictEqual(value.executor, authority.executor) ||
|
|
1348
|
+
value.operatorUserId !== authority.operatorUserId ||
|
|
1349
|
+
!isDeepStrictEqual(
|
|
1350
|
+
value.request.source.updateIds,
|
|
1351
|
+
expected.sourceUpdateIds,
|
|
1352
|
+
) ||
|
|
1353
|
+
!isDeepStrictEqual(value.request.target, expected.target) ||
|
|
1354
|
+
!isDeepStrictEqual(value.request.binding.target, oldTarget) ||
|
|
1355
|
+
value.request.owner.instanceId !== instanceId ||
|
|
1356
|
+
value.request.owner.owner?.kind !== "manual-follower" ||
|
|
1357
|
+
value.request.owner.slot !== slot ||
|
|
1358
|
+
!isDeepStrictEqual(value.request.owner.target, oldTarget) ||
|
|
1359
|
+
value.request.binding.cwd !==
|
|
1360
|
+
WorkspaceIdentity.normalizeTelegramWorkspacePath(authority.cwd) ||
|
|
1361
|
+
value.request.binding.sessionId !== expected.sessionId ||
|
|
1362
|
+
value.request.binding.slot !== slot ||
|
|
1363
|
+
!isCurrent()
|
|
1364
|
+
)
|
|
1365
|
+
throw new Error(
|
|
1366
|
+
"Selected command recipient has no exact released intent.",
|
|
1367
|
+
);
|
|
1368
|
+
const intent = structuredClone(value);
|
|
1369
|
+
let confirmed = false;
|
|
1370
|
+
snapshot.call(store, intent, (canonical) => {
|
|
1371
|
+
if (
|
|
1372
|
+
!isCurrent() ||
|
|
1373
|
+
!isDeepStrictEqual(
|
|
1374
|
+
read.call(
|
|
1375
|
+
deps,
|
|
1376
|
+
expected.operationId,
|
|
1377
|
+
authority.profileBindingKey,
|
|
1378
|
+
),
|
|
1379
|
+
intent,
|
|
1380
|
+
)
|
|
1381
|
+
)
|
|
1382
|
+
return;
|
|
1383
|
+
const bindings = (canonical.workspaceBindings ?? []).filter(
|
|
1384
|
+
(row) => row.bindingKey === intent.request.binding.bindingKey,
|
|
1385
|
+
);
|
|
1386
|
+
const owners = canonical.threads.filter(
|
|
1387
|
+
(row) =>
|
|
1388
|
+
row.status === "active" &&
|
|
1389
|
+
(row.slot === slot ||
|
|
1390
|
+
isDeepStrictEqual(row.target, expected.target)),
|
|
1391
|
+
);
|
|
1392
|
+
const row = bindings[0],
|
|
1393
|
+
owner = owners[0];
|
|
1394
|
+
confirmed =
|
|
1395
|
+
bindings.length === 1 &&
|
|
1396
|
+
row?.sessionId === expected.sessionId &&
|
|
1397
|
+
row.cwd === intent.request.binding.cwd &&
|
|
1398
|
+
row.slot === slot &&
|
|
1399
|
+
row.workspaceKey === intent.request.binding.workspaceKey &&
|
|
1400
|
+
row.inactiveSinceMs === undefined &&
|
|
1401
|
+
isDeepStrictEqual(row.target, expected.target) &&
|
|
1402
|
+
owners.length === 1 &&
|
|
1403
|
+
owner?.instanceId === instanceId &&
|
|
1404
|
+
owner.owner?.kind === "manual-follower" &&
|
|
1405
|
+
owner.slot === slot &&
|
|
1406
|
+
owner.profileKey === intent.request.owner.profileKey &&
|
|
1407
|
+
isDeepStrictEqual(owner.owner, intent.request.owner.owner) &&
|
|
1408
|
+
isDeepStrictEqual(owner.target, expected.target) &&
|
|
1409
|
+
isDeepStrictEqual(state.getTarget(), expected.target) &&
|
|
1410
|
+
isCurrent() &&
|
|
1411
|
+
isDeepStrictEqual(
|
|
1412
|
+
read.call(
|
|
1413
|
+
deps,
|
|
1414
|
+
expected.operationId,
|
|
1415
|
+
authority.profileBindingKey,
|
|
1416
|
+
),
|
|
1417
|
+
intent,
|
|
1418
|
+
);
|
|
1419
|
+
});
|
|
1420
|
+
if (!confirmed || !isCurrent())
|
|
1421
|
+
throw new Error(
|
|
1422
|
+
"Selected command canonical/local recipient changed.",
|
|
1423
|
+
);
|
|
1424
|
+
},
|
|
1425
|
+
};
|
|
1426
|
+
},
|
|
1427
|
+
});
|
|
813
1428
|
}
|
|
814
1429
|
|
|
815
1430
|
export function createTelegramBusFollowerClientRuntime<
|
|
@@ -819,8 +1434,7 @@ export function createTelegramBusFollowerClientRuntime<
|
|
|
819
1434
|
TMessage = unknown,
|
|
820
1435
|
>(deps: TelegramBusFollowerClientRuntimeDeps<TMessage>) {
|
|
821
1436
|
const createRequestId = createTelegramBusRequestIdFactory(deps.instanceId);
|
|
822
|
-
const timeoutMs =
|
|
823
|
-
deps.timeoutMs ?? TELEGRAM_BUS_FOLLOWER_CLIENT_TIMEOUT_MS;
|
|
1437
|
+
const timeoutMs = deps.timeoutMs ?? TELEGRAM_BUS_FOLLOWER_CLIENT_TIMEOUT_MS;
|
|
824
1438
|
const sharedClientDeps = {
|
|
825
1439
|
socketPath: deps.socketPath,
|
|
826
1440
|
createRequestId,
|
|
@@ -849,8 +1463,7 @@ export function createTelegramBusFollowerClientRuntime<
|
|
|
849
1463
|
>({
|
|
850
1464
|
...sharedClientDeps,
|
|
851
1465
|
getAuthSecret: deps.getForwardingAuthSecret,
|
|
852
|
-
getForwardCommentBatchPosition:
|
|
853
|
-
deps.getForwardCommentBatchPosition,
|
|
1466
|
+
getForwardCommentBatchPosition: deps.getForwardCommentBatchPosition,
|
|
854
1467
|
validateForwardOwnership: deps.validateForwardOwnership,
|
|
855
1468
|
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
856
1469
|
}),
|
|
@@ -865,34 +1478,21 @@ export function createTelegramBusFollowerClientRuntime<
|
|
|
865
1478
|
|
|
866
1479
|
export function createTelegramBusFollowerQueueHandoffClient(
|
|
867
1480
|
deps: TelegramBusFollowerApiCallerDeps,
|
|
868
|
-
): (
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
donorProcessId: number;
|
|
872
|
-
donorProcessBirthId: string;
|
|
873
|
-
donorSessionGeneration: number;
|
|
874
|
-
donorAcquisitionId: string;
|
|
875
|
-
donorAcquiredAtMs: number;
|
|
876
|
-
handoffToken: string;
|
|
877
|
-
payload: TelegramQueueHandoffPayload;
|
|
878
|
-
}) => Promise<TelegramQueueHandoffStageResult> {
|
|
1481
|
+
): (
|
|
1482
|
+
input: TelegramBusFollowerQueueHandoffOffer,
|
|
1483
|
+
) => Promise<TelegramQueueHandoffStageResult> {
|
|
879
1484
|
const getNowMs = deps.getNowMs ?? Date.now;
|
|
880
|
-
const timeoutMs =
|
|
881
|
-
deps.timeoutMs ?? TELEGRAM_BUS_FOLLOWER_CLIENT_TIMEOUT_MS;
|
|
1485
|
+
const timeoutMs = deps.timeoutMs ?? TELEGRAM_BUS_FOLLOWER_CLIENT_TIMEOUT_MS;
|
|
882
1486
|
return async (input) => {
|
|
883
1487
|
const registration = await resolveTelegramBusFollowerRegistration(
|
|
884
1488
|
deps,
|
|
885
1489
|
timeoutMs,
|
|
886
1490
|
);
|
|
887
1491
|
const socketPath = resolveTelegramBusSocketPath(deps.socketPath);
|
|
888
|
-
const response = await
|
|
1492
|
+
const response = await sendTelegramBusOperation(
|
|
889
1493
|
socketPath,
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
endpoint: socketPath,
|
|
893
|
-
operation: "operation",
|
|
894
|
-
}),
|
|
895
|
-
envelope: {
|
|
1494
|
+
registration.remainingTimeoutMs,
|
|
1495
|
+
{
|
|
896
1496
|
kind: "follower.offerQueueHandoff",
|
|
897
1497
|
requestId: deps.createRequestId(),
|
|
898
1498
|
auth: deps.getAuthSecret?.(),
|
|
@@ -901,7 +1501,7 @@ export function createTelegramBusFollowerQueueHandoffClient(
|
|
|
901
1501
|
...input,
|
|
902
1502
|
sentAtMs: getNowMs(),
|
|
903
1503
|
},
|
|
904
|
-
|
|
1504
|
+
);
|
|
905
1505
|
const queueOwner =
|
|
906
1506
|
response?.kind === "bus.ack" && isRecord(response.result)
|
|
907
1507
|
? parseTelegramUpdateJournalQueueOwner(response.result.queueOwner)
|
|
@@ -921,7 +1521,8 @@ export function createTelegramBusFollowerQueueHandoffClient(
|
|
|
921
1521
|
input.payload.admissionReceipts[0].sourceUpdateIds.length &&
|
|
922
1522
|
response.result.sourceUpdateIds.every(
|
|
923
1523
|
(updateId, index) =>
|
|
924
|
-
updateId ===
|
|
1524
|
+
updateId ===
|
|
1525
|
+
input.payload.admissionReceipts[0]!.sourceUpdateIds[index],
|
|
925
1526
|
) &&
|
|
926
1527
|
queueOwner
|
|
927
1528
|
) {
|
|
@@ -934,7 +1535,7 @@ export function createTelegramBusFollowerQueueHandoffClient(
|
|
|
934
1535
|
}
|
|
935
1536
|
throw new Error(
|
|
936
1537
|
response?.kind === "bus.ack"
|
|
937
|
-
? response.message ?? "Telegram queue handoff was rejected."
|
|
1538
|
+
? (response.message ?? "Telegram queue handoff was rejected.")
|
|
938
1539
|
: "Telegram queue handoff did not return an acknowledgement.",
|
|
939
1540
|
);
|
|
940
1541
|
};
|
|
@@ -949,8 +1550,7 @@ function createTelegramBusAgentMessageClient(
|
|
|
949
1550
|
routeMessage: (message: TelegramBusAgentMessage) => Promise<void>;
|
|
950
1551
|
} {
|
|
951
1552
|
const getNowMs = deps.getNowMs ?? Date.now;
|
|
952
|
-
const timeoutMs =
|
|
953
|
-
deps.timeoutMs ?? TELEGRAM_BUS_FOLLOWER_CLIENT_TIMEOUT_MS;
|
|
1553
|
+
const timeoutMs = deps.timeoutMs ?? TELEGRAM_BUS_FOLLOWER_CLIENT_TIMEOUT_MS;
|
|
954
1554
|
const request = async (
|
|
955
1555
|
envelope:
|
|
956
1556
|
| Extract<TelegramBusEnvelope, { kind: "follower.resolveAgentTarget" }>
|
|
@@ -958,19 +1558,15 @@ function createTelegramBusAgentMessageClient(
|
|
|
958
1558
|
requestTimeoutMs = timeoutMs,
|
|
959
1559
|
): Promise<unknown> => {
|
|
960
1560
|
const socketPath = resolveTelegramBusSocketPath(deps.socketPath);
|
|
961
|
-
const response = await
|
|
1561
|
+
const response = await sendTelegramBusOperation(
|
|
962
1562
|
socketPath,
|
|
963
|
-
|
|
964
|
-
retry: getTelegramBusTransportRetryPolicy({
|
|
965
|
-
endpoint: socketPath,
|
|
966
|
-
operation: "operation",
|
|
967
|
-
}),
|
|
1563
|
+
requestTimeoutMs,
|
|
968
1564
|
envelope,
|
|
969
|
-
|
|
1565
|
+
);
|
|
970
1566
|
if (response?.kind === "bus.ack" && response.ok) return response.result;
|
|
971
1567
|
throw new Error(
|
|
972
1568
|
response?.kind === "bus.ack"
|
|
973
|
-
? response.message ?? "Telegram bus agent message failed."
|
|
1569
|
+
? (response.message ?? "Telegram bus agent message failed.")
|
|
974
1570
|
: "Telegram bus agent message did not return an acknowledgement.",
|
|
975
1571
|
);
|
|
976
1572
|
};
|
|
@@ -991,13 +1587,16 @@ function createTelegramBusAgentMessageClient(
|
|
|
991
1587
|
return {
|
|
992
1588
|
async resolveTarget(selector) {
|
|
993
1589
|
const registration = await registrationFields();
|
|
994
|
-
const result = await request(
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1590
|
+
const result = await request(
|
|
1591
|
+
{
|
|
1592
|
+
kind: "follower.resolveAgentTarget",
|
|
1593
|
+
requestId: deps.createRequestId(),
|
|
1594
|
+
...registration.fields,
|
|
1595
|
+
selector,
|
|
1596
|
+
sentAtMs: getNowMs(),
|
|
1597
|
+
},
|
|
1598
|
+
registration.remainingTimeoutMs,
|
|
1599
|
+
);
|
|
1001
1600
|
if (!result || typeof result !== "object" || Array.isArray(result)) {
|
|
1002
1601
|
throw new Error("Telegram bus returned an invalid agent target.");
|
|
1003
1602
|
}
|
|
@@ -1012,39 +1611,241 @@ function createTelegramBusAgentMessageClient(
|
|
|
1012
1611
|
},
|
|
1013
1612
|
async routeMessage(message) {
|
|
1014
1613
|
const registration = await registrationFields();
|
|
1015
|
-
await request(
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1614
|
+
await request(
|
|
1615
|
+
{
|
|
1616
|
+
kind: "follower.routeAgentMessage",
|
|
1617
|
+
requestId: deps.createRequestId(),
|
|
1618
|
+
...registration.fields,
|
|
1619
|
+
message,
|
|
1620
|
+
sentAtMs: getNowMs(),
|
|
1621
|
+
},
|
|
1622
|
+
registration.remainingTimeoutMs,
|
|
1623
|
+
);
|
|
1022
1624
|
},
|
|
1023
1625
|
};
|
|
1024
1626
|
}
|
|
1025
1627
|
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1628
|
+
/** Private selected text-effect caller; not a guarded ordinary API adapter or follower command admission grant. */
|
|
1629
|
+
export function createTelegramBusFollowerSelectedMenuCaller<TContext>(deps: {
|
|
1630
|
+
client: TelegramBusFollowerApiCallerDeps;
|
|
1631
|
+
protocolIdentity: TelegramBusProtocolIdentity;
|
|
1632
|
+
recipient: {
|
|
1633
|
+
getContextAuthority(
|
|
1634
|
+
ctx: TContext,
|
|
1635
|
+
): TelegramBusFollowerRestoreContext | undefined;
|
|
1636
|
+
getJournalBindingKey(ctx: TContext): string | undefined;
|
|
1637
|
+
getProcessIdentity(): { processId: number; processBirthId: string };
|
|
1638
|
+
registrationState: Pick<
|
|
1639
|
+
TelegramBusFollowerRegistrationState,
|
|
1640
|
+
"getTarget" | "getSlot" | "getGeneration" | "getLeaderProtocol"
|
|
1641
|
+
>;
|
|
1642
|
+
};
|
|
1643
|
+
}) {
|
|
1644
|
+
return async (input: {
|
|
1645
|
+
ctx: TContext;
|
|
1646
|
+
operationId: string;
|
|
1647
|
+
registrationGeneration: string;
|
|
1648
|
+
effect: TelegramBusSelectedMenuTextEffect;
|
|
1649
|
+
assertAuthority: () => void;
|
|
1650
|
+
}): Promise<TelegramBusSelectedMenuDeliveryObservation> => {
|
|
1651
|
+
let dispatched = false,
|
|
1652
|
+
method = "sendMessage";
|
|
1653
|
+
try {
|
|
1654
|
+
const client = { ...deps.client },
|
|
1655
|
+
recipient = deps.recipient,
|
|
1656
|
+
ctx = input.ctx,
|
|
1657
|
+
assertAuthority = input.assertAuthority;
|
|
1658
|
+
const getAuthority = recipient.getContextAuthority,
|
|
1659
|
+
getJournal = recipient.getJournalBindingKey,
|
|
1660
|
+
getProcess = recipient.getProcessIdentity;
|
|
1661
|
+
const state = recipient.registrationState,
|
|
1662
|
+
statePorts = {
|
|
1663
|
+
getTarget: state.getTarget,
|
|
1664
|
+
getSlot: state.getSlot,
|
|
1665
|
+
getGeneration: state.getGeneration,
|
|
1666
|
+
getLeaderProtocol: state.getLeaderProtocol,
|
|
1667
|
+
};
|
|
1668
|
+
const authority = structuredClone(getAuthority.call(recipient, ctx));
|
|
1669
|
+
const journalBindingKey = getJournal.call(recipient, ctx),
|
|
1670
|
+
process = structuredClone(getProcess.call(recipient)),
|
|
1671
|
+
target = structuredClone(state.getTarget()),
|
|
1672
|
+
slot = state.getSlot();
|
|
1673
|
+
const protocol = structuredClone(deps.protocolIdentity),
|
|
1674
|
+
leaderProtocol = structuredClone(state.getLeaderProtocol()),
|
|
1675
|
+
auth = client.getAuthSecret?.();
|
|
1676
|
+
const endpoint = resolveTelegramBusSocketPath(client.socketPath),
|
|
1677
|
+
timeoutMs = client.timeoutMs ?? TELEGRAM_BUS_FOLLOWER_CLIENT_TIMEOUT_MS;
|
|
1678
|
+
if (
|
|
1679
|
+
!authority ||
|
|
1680
|
+
typeof authority.generation !== "number" ||
|
|
1681
|
+
typeof assertAuthority !== "function" ||
|
|
1682
|
+
!slot ||
|
|
1683
|
+
!auth ||
|
|
1684
|
+
!Number.isFinite(timeoutMs) ||
|
|
1685
|
+
timeoutMs <= 0
|
|
1686
|
+
)
|
|
1687
|
+
throw new Error("Selected-menu local owner is unavailable.");
|
|
1688
|
+
const decoded = parseTelegramBusEnvelope(
|
|
1689
|
+
JSON.stringify({
|
|
1690
|
+
kind: "follower.deliverSelectedMenu",
|
|
1691
|
+
requestId: client.createRequestId(),
|
|
1692
|
+
auth,
|
|
1693
|
+
instanceId: client.instanceId,
|
|
1694
|
+
operationId: input.operationId,
|
|
1695
|
+
registrationGeneration: input.registrationGeneration,
|
|
1696
|
+
recipient: {
|
|
1697
|
+
sessionId: authority.sessionId,
|
|
1698
|
+
sessionGeneration: authority.generation,
|
|
1699
|
+
...process,
|
|
1700
|
+
profileKey: authority.profileBindingKey,
|
|
1701
|
+
journalBindingKey,
|
|
1702
|
+
target,
|
|
1703
|
+
},
|
|
1704
|
+
executor: authority.executor,
|
|
1705
|
+
operatorUserId: authority.operatorUserId,
|
|
1706
|
+
effect: structuredClone(input.effect),
|
|
1707
|
+
sentAtMs: (client.getNowMs ?? Date.now)(),
|
|
1708
|
+
}),
|
|
1709
|
+
);
|
|
1710
|
+
if (decoded?.kind !== "follower.deliverSelectedMenu")
|
|
1711
|
+
throw new Error("Selected-menu local description is invalid.");
|
|
1712
|
+
const expected = decoded;
|
|
1713
|
+
method =
|
|
1714
|
+
expected.effect.kind === "send-text"
|
|
1715
|
+
? "sendMessage"
|
|
1716
|
+
: "editMessageText";
|
|
1717
|
+
const assertCurrent = (allowPending = false) => {
|
|
1718
|
+
assertAuthority();
|
|
1719
|
+
const local = getAuthority.call(recipient, ctx),
|
|
1720
|
+
currentGeneration = client.getRegistrationGeneration();
|
|
1721
|
+
if (
|
|
1722
|
+
!isDeepStrictEqual(deps.client, client) ||
|
|
1723
|
+
deps.recipient !== recipient ||
|
|
1724
|
+
recipient.getContextAuthority !== getAuthority ||
|
|
1725
|
+
recipient.getJournalBindingKey !== getJournal ||
|
|
1726
|
+
recipient.getProcessIdentity !== getProcess ||
|
|
1727
|
+
recipient.registrationState !== state ||
|
|
1728
|
+
!Object.entries(statePorts).every(
|
|
1729
|
+
([key, value]) => state[key as keyof typeof statePorts] === value,
|
|
1730
|
+
) ||
|
|
1731
|
+
!isDeepStrictEqual(local, authority) ||
|
|
1732
|
+
getJournal.call(recipient, ctx) !== journalBindingKey ||
|
|
1733
|
+
!isDeepStrictEqual(getProcess.call(recipient), process) ||
|
|
1734
|
+
!isDeepStrictEqual(state.getTarget(), target) ||
|
|
1735
|
+
state.getSlot() !== slot ||
|
|
1736
|
+
state.getGeneration() !== expected.registrationGeneration ||
|
|
1737
|
+
(!allowPending &&
|
|
1738
|
+
currentGeneration !== expected.registrationGeneration) ||
|
|
1739
|
+
(currentGeneration !== undefined &&
|
|
1740
|
+
currentGeneration !== expected.registrationGeneration) ||
|
|
1741
|
+
client.getAuthSecret?.() !== auth ||
|
|
1742
|
+
resolveTelegramBusSocketPath(client.socketPath) !== endpoint ||
|
|
1743
|
+
!isDeepStrictEqual(deps.protocolIdentity, protocol) ||
|
|
1744
|
+
!isDeepStrictEqual(state.getLeaderProtocol(), leaderProtocol) ||
|
|
1745
|
+
!getTelegramBusProtocolCompatibility({
|
|
1746
|
+
local: protocol,
|
|
1747
|
+
remote: leaderProtocol,
|
|
1748
|
+
}).compatible ||
|
|
1749
|
+
!hasTelegramBusCapability(
|
|
1750
|
+
protocol,
|
|
1751
|
+
TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY,
|
|
1752
|
+
) ||
|
|
1753
|
+
!hasTelegramBusCapability(
|
|
1754
|
+
leaderProtocol,
|
|
1755
|
+
TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY,
|
|
1756
|
+
)
|
|
1757
|
+
)
|
|
1758
|
+
throw new Error("Selected-menu local authority changed.");
|
|
1759
|
+
if (!isDeepStrictEqual(getAuthority.call(recipient, ctx), authority))
|
|
1760
|
+
throw new Error(
|
|
1761
|
+
"Selected-menu context changed after local observations.",
|
|
1762
|
+
);
|
|
1763
|
+
assertAuthority();
|
|
1764
|
+
};
|
|
1765
|
+
assertCurrent(true);
|
|
1766
|
+
const registration = await resolveTelegramBusFollowerRegistration(
|
|
1767
|
+
client,
|
|
1768
|
+
timeoutMs,
|
|
1769
|
+
);
|
|
1770
|
+
if (registration.generation !== expected.registrationGeneration)
|
|
1771
|
+
throw new Error("Selected-menu registration changed.");
|
|
1772
|
+
assertCurrent();
|
|
1773
|
+
dispatched = true;
|
|
1774
|
+
const response = await sendTelegramBusLocalEnvelope({
|
|
1775
|
+
socketPath: endpoint,
|
|
1776
|
+
envelope: expected,
|
|
1777
|
+
timeoutMs: registration.remainingTimeoutMs,
|
|
1778
|
+
retry: { attempts: 1, delayMs: 0 },
|
|
1779
|
+
assertAuthority: () => assertCurrent(),
|
|
1780
|
+
});
|
|
1781
|
+
assertCurrent();
|
|
1782
|
+
if (
|
|
1783
|
+
response?.kind !== "bus.ack" ||
|
|
1784
|
+
response.requestId !== expected.requestId ||
|
|
1785
|
+
!response.ok ||
|
|
1786
|
+
response.error !== undefined ||
|
|
1787
|
+
(response.protocol !== undefined &&
|
|
1788
|
+
!isDeepStrictEqual(response.protocol, leaderProtocol)) ||
|
|
1789
|
+
!isRecord(response.result)
|
|
1790
|
+
)
|
|
1791
|
+
throw new Error("Selected-menu acknowledgement is unconfirmed.");
|
|
1792
|
+
const result = response.result;
|
|
1793
|
+
if (
|
|
1794
|
+
!isDeepStrictEqual(Object.keys(result).sort(), [
|
|
1795
|
+
"effect",
|
|
1796
|
+
"messageId",
|
|
1797
|
+
"operationId",
|
|
1798
|
+
"recipient",
|
|
1799
|
+
"registrationGeneration",
|
|
1800
|
+
]) ||
|
|
1801
|
+
result.operationId !== expected.operationId ||
|
|
1802
|
+
result.registrationGeneration !== expected.registrationGeneration ||
|
|
1803
|
+
result.effect !== expected.effect.kind ||
|
|
1804
|
+
!isDeepStrictEqual(result.recipient, expected.recipient) ||
|
|
1805
|
+
typeof result.messageId !== "number" ||
|
|
1806
|
+
!Number.isSafeInteger(result.messageId) ||
|
|
1807
|
+
result.messageId <= 0 ||
|
|
1808
|
+
(expected.effect.kind === "edit-text" &&
|
|
1809
|
+
result.messageId !== expected.effect.messageId)
|
|
1810
|
+
)
|
|
1811
|
+
throw new Error("Selected-menu recipient/result is unconfirmed.");
|
|
1812
|
+
assertCurrent();
|
|
1813
|
+
return {
|
|
1814
|
+
operationId: expected.operationId,
|
|
1815
|
+
recipient: structuredClone(expected.recipient),
|
|
1816
|
+
registrationGeneration: expected.registrationGeneration,
|
|
1817
|
+
effect: expected.effect.kind,
|
|
1818
|
+
messageId: result.messageId,
|
|
1819
|
+
};
|
|
1820
|
+
} catch (error) {
|
|
1821
|
+
if (
|
|
1822
|
+
!dispatched ||
|
|
1823
|
+
(error instanceof TelegramBusLocalAuthorityError &&
|
|
1824
|
+
!error.requestIssued)
|
|
1825
|
+
)
|
|
1826
|
+
throw new TelegramApiAuthorityError(false);
|
|
1827
|
+
throw new TelegramApiCommitUnknownError(method, error);
|
|
1828
|
+
}
|
|
1829
|
+
};
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
export function createTelegramBusFollowerApiCaller(
|
|
1833
|
+
deps: TelegramBusFollowerApiCallerDeps,
|
|
1834
|
+
): (method: string, args: unknown[]) => Promise<unknown> {
|
|
1835
|
+
const getNowMs = deps.getNowMs ?? Date.now;
|
|
1836
|
+
const timeoutMs = deps.timeoutMs ?? TELEGRAM_BUS_FOLLOWER_CLIENT_TIMEOUT_MS;
|
|
1837
|
+
return async (method, args) => {
|
|
1838
|
+
const registration = await resolveTelegramBusFollowerRegistration(
|
|
1839
|
+
deps,
|
|
1840
|
+
timeoutMs,
|
|
1036
1841
|
);
|
|
1037
1842
|
const socketPath = resolveTelegramBusSocketPath(deps.socketPath);
|
|
1038
1843
|
let response: TelegramBusEnvelope | undefined;
|
|
1039
1844
|
try {
|
|
1040
|
-
response = await
|
|
1845
|
+
response = await sendTelegramBusOperation(
|
|
1041
1846
|
socketPath,
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
endpoint: socketPath,
|
|
1045
|
-
operation: "operation",
|
|
1046
|
-
}),
|
|
1047
|
-
envelope: {
|
|
1847
|
+
registration.remainingTimeoutMs,
|
|
1848
|
+
{
|
|
1048
1849
|
kind: "follower.callApi",
|
|
1049
1850
|
requestId: deps.createRequestId(),
|
|
1050
1851
|
auth: deps.getAuthSecret?.(),
|
|
@@ -1054,7 +1855,7 @@ export function createTelegramBusFollowerApiCaller(
|
|
|
1054
1855
|
args,
|
|
1055
1856
|
sentAtMs: getNowMs(),
|
|
1056
1857
|
},
|
|
1057
|
-
|
|
1858
|
+
);
|
|
1058
1859
|
} catch (error) {
|
|
1059
1860
|
const apiMethod =
|
|
1060
1861
|
(method === "call" || method === "callMultipart") &&
|
|
@@ -1101,9 +1902,7 @@ export function createTelegramBusFollowerApiCaller(
|
|
|
1101
1902
|
async function resolveTelegramBusFollowerRegistration(
|
|
1102
1903
|
deps: Pick<
|
|
1103
1904
|
TelegramBusFollowerApiCallerDeps,
|
|
1104
|
-
| "
|
|
1105
|
-
| "waitForRegistrationGeneration"
|
|
1106
|
-
| "getNowMs"
|
|
1905
|
+
"getRegistrationGeneration" | "waitForRegistrationGeneration" | "getNowMs"
|
|
1107
1906
|
>,
|
|
1108
1907
|
timeoutMs: number,
|
|
1109
1908
|
): Promise<{ generation: string; remainingTimeoutMs: number }> {
|
|
@@ -1111,24 +1910,17 @@ async function resolveTelegramBusFollowerRegistration(
|
|
|
1111
1910
|
if (current) return { generation: current, remainingTimeoutMs: timeoutMs };
|
|
1112
1911
|
const getNowMs = deps.getNowMs ?? Date.now;
|
|
1113
1912
|
const startedAtMs = getNowMs();
|
|
1114
|
-
const restored = await deps.waitForRegistrationGeneration?.(
|
|
1115
|
-
|
|
1913
|
+
const restored = await deps.waitForRegistrationGeneration?.(timeoutMs);
|
|
1914
|
+
const remainingTimeoutMs = Math.max(
|
|
1915
|
+
0,
|
|
1916
|
+
timeoutMs - (getNowMs() - startedAtMs),
|
|
1116
1917
|
);
|
|
1117
|
-
const remainingTimeoutMs = Math.max(0, timeoutMs - (getNowMs() - startedAtMs));
|
|
1118
1918
|
if (restored && remainingTimeoutMs > 0) {
|
|
1119
1919
|
return { generation: restored, remainingTimeoutMs };
|
|
1120
1920
|
}
|
|
1121
1921
|
throw new Error("Telegram bus follower is not registered.");
|
|
1122
1922
|
}
|
|
1123
1923
|
|
|
1124
|
-
function isTelegramStaleContextError(error: unknown): boolean {
|
|
1125
|
-
return (
|
|
1126
|
-
error instanceof Error &&
|
|
1127
|
-
(error.message.includes("stale after session") ||
|
|
1128
|
-
error.message.includes("stale ctx"))
|
|
1129
|
-
);
|
|
1130
|
-
}
|
|
1131
|
-
|
|
1132
1924
|
export function createTelegramBusFollowerSessionReplacementSuspender(
|
|
1133
1925
|
deps: TelegramBusFollowerSessionReplacementSuspenderDeps,
|
|
1134
1926
|
): (preserveTarget?: boolean) => Promise<void> {
|
|
@@ -1248,7 +2040,9 @@ export function createTelegramBusFollowerSessionRefreshHook<TContext>(
|
|
|
1248
2040
|
try {
|
|
1249
2041
|
await deps.registrationRuntime.setContext(ctx);
|
|
1250
2042
|
} catch (error) {
|
|
1251
|
-
deps.recordRuntimeEvent("bus", error, {
|
|
2043
|
+
deps.recordRuntimeEvent("bus", error, {
|
|
2044
|
+
phase: "follower-session-refresh",
|
|
2045
|
+
});
|
|
1252
2046
|
return;
|
|
1253
2047
|
}
|
|
1254
2048
|
if (deps.isSessionActive && !deps.isSessionActive(ctx)) return;
|
|
@@ -1309,13 +2103,23 @@ export function createTelegramBusFollowerRegistrationState(
|
|
|
1309
2103
|
getThreadName: () => threadName,
|
|
1310
2104
|
getDisplayTitle: () => displayTitle,
|
|
1311
2105
|
setDisplayTitle(title, expectedGeneration) {
|
|
1312
|
-
if (
|
|
1313
|
-
|
|
2106
|
+
if (
|
|
2107
|
+
!registered ||
|
|
2108
|
+
!generation ||
|
|
2109
|
+
expectedGeneration !== generation ||
|
|
2110
|
+
!title.trim() ||
|
|
2111
|
+
title.length > 128 ||
|
|
2112
|
+
displayTitle === title
|
|
2113
|
+
)
|
|
2114
|
+
return false;
|
|
1314
2115
|
displayTitle = title;
|
|
1315
2116
|
return true;
|
|
1316
2117
|
},
|
|
1317
2118
|
getGeneration: () => generation,
|
|
1318
|
-
getSessionId: currentSessionId =>
|
|
2119
|
+
getSessionId: (currentSessionId) =>
|
|
2120
|
+
registered && generation && currentSessionId === sessionId
|
|
2121
|
+
? sessionId
|
|
2122
|
+
: undefined,
|
|
1319
2123
|
beginRecovery: () => {
|
|
1320
2124
|
if (activeRecoveryEpoch !== undefined) return activeRecoveryEpoch;
|
|
1321
2125
|
activeRecoveryEpoch = ++recoveryEpoch;
|
|
@@ -1326,7 +2130,9 @@ export function createTelegramBusFollowerRegistrationState(
|
|
|
1326
2130
|
activeRecoveryEpoch = undefined;
|
|
1327
2131
|
if (epoch !== undefined) settleGenerationWaiters(undefined, epoch);
|
|
1328
2132
|
},
|
|
1329
|
-
waitForGeneration: (
|
|
2133
|
+
waitForGeneration: (
|
|
2134
|
+
timeoutMs = TELEGRAM_BUS_FOLLOWER_REGISTRATION_WAIT_MS,
|
|
2135
|
+
) => {
|
|
1330
2136
|
if (generation) return Promise.resolve(generation);
|
|
1331
2137
|
const epoch = activeRecoveryEpoch;
|
|
1332
2138
|
if (epoch === undefined) return Promise.resolve(undefined);
|
|
@@ -1353,21 +2159,33 @@ export function createTelegramBusFollowerRegistrationState(
|
|
|
1353
2159
|
).sort();
|
|
1354
2160
|
},
|
|
1355
2161
|
setRegistered: (next, nextTarget, metadata) => {
|
|
1356
|
-
const retainedDisplayTitle =
|
|
2162
|
+
const retainedDisplayTitle =
|
|
2163
|
+
next &&
|
|
2164
|
+
registered &&
|
|
1357
2165
|
generation === metadata?.generation &&
|
|
1358
|
-
target?.chatId === nextTarget?.chatId &&
|
|
1359
|
-
|
|
1360
|
-
|
|
2166
|
+
target?.chatId === nextTarget?.chatId &&
|
|
2167
|
+
target?.threadId === nextTarget?.threadId
|
|
2168
|
+
? displayTitle
|
|
2169
|
+
: undefined;
|
|
2170
|
+
const retainedSessionId =
|
|
2171
|
+
next && registered && generation === metadata?.generation
|
|
2172
|
+
? sessionId
|
|
2173
|
+
: undefined;
|
|
1361
2174
|
const availabilityChanged = registered !== next;
|
|
1362
2175
|
registered = next;
|
|
1363
2176
|
target = next ? (nextTarget ? { ...nextTarget } : undefined) : undefined;
|
|
1364
2177
|
slot = next ? metadata?.slot : undefined;
|
|
1365
2178
|
threadName = next ? metadata?.threadName : undefined;
|
|
1366
|
-
displayTitle =
|
|
1367
|
-
|
|
1368
|
-
|
|
2179
|
+
displayTitle =
|
|
2180
|
+
next &&
|
|
2181
|
+
nextTarget &&
|
|
2182
|
+
metadata?.generation &&
|
|
2183
|
+
metadata.displayTitle?.trim() &&
|
|
2184
|
+
metadata.displayTitle.length <= 128
|
|
2185
|
+
? metadata.displayTitle
|
|
2186
|
+
: retainedDisplayTitle;
|
|
1369
2187
|
generation = next ? metadata?.generation : undefined;
|
|
1370
|
-
sessionId = next ? metadata?.sessionId ?? retainedSessionId : undefined;
|
|
2188
|
+
sessionId = next ? (metadata?.sessionId ?? retainedSessionId) : undefined;
|
|
1371
2189
|
leaderProtocol =
|
|
1372
2190
|
next && metadata?.leaderProtocol
|
|
1373
2191
|
? {
|
|
@@ -1403,7 +2221,7 @@ export function createTelegramBusFollowerHeartbeatRecoveryHandler<TContext>(
|
|
|
1403
2221
|
try {
|
|
1404
2222
|
deps.updateStatus(ctx);
|
|
1405
2223
|
} catch (error) {
|
|
1406
|
-
if (!
|
|
2224
|
+
if (!isPiStaleContextError(error)) throw error;
|
|
1407
2225
|
deps.recordRuntimeEvent("bus", error, {
|
|
1408
2226
|
phase: "follower-stale-context-status",
|
|
1409
2227
|
});
|
|
@@ -1606,19 +2424,14 @@ export function createTelegramBusFollowerHeartbeatRecoveryHandler<TContext>(
|
|
|
1606
2424
|
scheduleRecovery(error, ctx, initialBinding);
|
|
1607
2425
|
return;
|
|
1608
2426
|
}
|
|
1609
|
-
await attemptPreferredPromotion(
|
|
1610
|
-
error,
|
|
1611
|
-
ctx,
|
|
1612
|
-
initialBinding,
|
|
1613
|
-
graceState,
|
|
1614
|
-
);
|
|
2427
|
+
await attemptPreferredPromotion(error, ctx, initialBinding, graceState);
|
|
1615
2428
|
return;
|
|
1616
2429
|
}
|
|
1617
2430
|
await attemptPreferredPromotion(error, ctx, initialBinding, state);
|
|
1618
2431
|
} catch (promotionError) {
|
|
1619
2432
|
deps.setLifecyclePhase(undefined);
|
|
1620
2433
|
safeUpdateStatus(ctx);
|
|
1621
|
-
if (
|
|
2434
|
+
if (isPiStaleContextError(promotionError)) {
|
|
1622
2435
|
deps.recordRuntimeEvent("bus", promotionError, {
|
|
1623
2436
|
phase: "follower-heartbeat-stale-context",
|
|
1624
2437
|
});
|
|
@@ -1661,11 +2474,26 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
1661
2474
|
let activeLeaderSocketPath: string | undefined;
|
|
1662
2475
|
let activeAuthSecret: string | undefined;
|
|
1663
2476
|
let activeRegistrationGeneration: string | undefined;
|
|
1664
|
-
let registrationAttempt:
|
|
2477
|
+
let registrationAttempt:
|
|
2478
|
+
| {
|
|
2479
|
+
ctx: TContext;
|
|
2480
|
+
sessionGeneration: number | undefined;
|
|
2481
|
+
sessionId: string | undefined;
|
|
2482
|
+
}
|
|
2483
|
+
| undefined;
|
|
1665
2484
|
let activeContext: TContext | undefined;
|
|
1666
2485
|
let lastKnownTarget: TelegramTarget | undefined;
|
|
1667
2486
|
let lastKnownSlot: string | undefined;
|
|
1668
2487
|
let lastKnownThreadName: string | undefined;
|
|
2488
|
+
// A leader result is accepted only for the same endpoint and registration generation that issued the request.
|
|
2489
|
+
const isLeaderSessionCurrent = (
|
|
2490
|
+
socketPath: string | undefined,
|
|
2491
|
+
generation: string | undefined,
|
|
2492
|
+
): boolean =>
|
|
2493
|
+
activeLeaderSocketPath === socketPath &&
|
|
2494
|
+
activeRegistrationGeneration === generation &&
|
|
2495
|
+
(!deps.registrationState ||
|
|
2496
|
+
deps.registrationState.getGeneration() === generation);
|
|
1669
2497
|
const stopHeartbeat = () => {
|
|
1670
2498
|
if (!heartbeatInterval) return;
|
|
1671
2499
|
clearInterval(heartbeatInterval);
|
|
@@ -1705,14 +2533,10 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
1705
2533
|
activeRegistrationGeneration === registrationGeneration &&
|
|
1706
2534
|
activeContext === heartbeatContext;
|
|
1707
2535
|
try {
|
|
1708
|
-
const response = await
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
endpoint: leaderSocketPath,
|
|
1713
|
-
operation: "operation",
|
|
1714
|
-
}),
|
|
1715
|
-
envelope: {
|
|
2536
|
+
const response = await sendTelegramBusOperation(
|
|
2537
|
+
leaderSocketPath,
|
|
2538
|
+
heartbeatTimeoutMs,
|
|
2539
|
+
{
|
|
1716
2540
|
kind: "follower.heartbeat",
|
|
1717
2541
|
requestId: deps.createRequestId(),
|
|
1718
2542
|
auth: activeAuthSecret,
|
|
@@ -1720,7 +2544,7 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
1720
2544
|
registrationGeneration,
|
|
1721
2545
|
sentAtMs: getNowMs(),
|
|
1722
2546
|
},
|
|
1723
|
-
|
|
2547
|
+
);
|
|
1724
2548
|
if (!isCurrentHeartbeat()) return;
|
|
1725
2549
|
if (response?.kind === "bus.ack" && response.ok) {
|
|
1726
2550
|
const heartbeatResult = isRecord(response.result)
|
|
@@ -1732,9 +2556,14 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
1732
2556
|
)
|
|
1733
2557
|
: [];
|
|
1734
2558
|
deps.registrationState?.setEligibleElectionSlots(slots);
|
|
1735
|
-
if (
|
|
1736
|
-
|
|
1737
|
-
|
|
2559
|
+
if (
|
|
2560
|
+
typeof heartbeatResult?.displayTitle === "string" &&
|
|
2561
|
+
deps.registrationState?.setDisplayTitle(
|
|
2562
|
+
heartbeatResult.displayTitle,
|
|
2563
|
+
registrationGeneration,
|
|
2564
|
+
) &&
|
|
2565
|
+
heartbeatContext
|
|
2566
|
+
) {
|
|
1738
2567
|
try {
|
|
1739
2568
|
deps.onDisplayTitleChanged?.(heartbeatContext);
|
|
1740
2569
|
} catch {
|
|
@@ -1801,8 +2630,10 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
1801
2630
|
const sessionId = deps.getSessionId?.(ctx);
|
|
1802
2631
|
const attempt = { ctx, sessionGeneration, sessionId };
|
|
1803
2632
|
registrationAttempt = attempt;
|
|
1804
|
-
const isCurrentRequest = () =>
|
|
1805
|
-
|
|
2633
|
+
const isCurrentRequest = () =>
|
|
2634
|
+
registrationAttempt === attempt &&
|
|
2635
|
+
deps.getSessionGeneration?.() === sessionGeneration &&
|
|
2636
|
+
deps.getSessionId?.(ctx) === sessionId &&
|
|
1806
2637
|
deps.isContextActive?.(ctx) !== false;
|
|
1807
2638
|
const abandonOwnedRequest = async (): Promise<void> => {
|
|
1808
2639
|
if (registrationAttempt !== attempt) return;
|
|
@@ -1816,6 +2647,18 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
1816
2647
|
deps.setActiveAuthSecret?.(undefined);
|
|
1817
2648
|
await deps.stopReceiving?.();
|
|
1818
2649
|
};
|
|
2650
|
+
// Failed registration releases its leader session but keeps the attempt owner for caller-visible errors.
|
|
2651
|
+
const failRegistration = async (
|
|
2652
|
+
clearGeneration = false,
|
|
2653
|
+
): Promise<void> => {
|
|
2654
|
+
stopHeartbeat();
|
|
2655
|
+
activeLeaderSocketPath = undefined;
|
|
2656
|
+
activeAuthSecret = undefined;
|
|
2657
|
+
if (clearGeneration) activeRegistrationGeneration = undefined;
|
|
2658
|
+
deps.registrationState?.setRegistered(false);
|
|
2659
|
+
deps.setActiveAuthSecret?.(undefined);
|
|
2660
|
+
await deps.stopReceiving?.();
|
|
2661
|
+
};
|
|
1819
2662
|
const pendingHandoff = options
|
|
1820
2663
|
? undefined
|
|
1821
2664
|
: getTelegramFollowerSessionHandoff();
|
|
@@ -1827,17 +2670,22 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
1827
2670
|
previousInstanceId: pendingHandoff.instanceId,
|
|
1828
2671
|
}
|
|
1829
2672
|
: undefined;
|
|
1830
|
-
const registrationOptions:
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
2673
|
+
const registrationOptions:
|
|
2674
|
+
| {
|
|
2675
|
+
target?: TelegramTarget;
|
|
2676
|
+
previousInstanceId?: string;
|
|
2677
|
+
restoreWorkspace?: boolean;
|
|
2678
|
+
}
|
|
2679
|
+
| undefined = options ?? pendingHandoffOptions;
|
|
1835
2680
|
const leaderSocketPath =
|
|
1836
2681
|
leader.busSocketPath ??
|
|
1837
2682
|
deps.getLeaderSocketPath?.() ??
|
|
1838
2683
|
getTelegramBusSocketPath();
|
|
1839
2684
|
await deps.startReceiving?.();
|
|
1840
|
-
if (!isCurrentRequest()) {
|
|
2685
|
+
if (!isCurrentRequest()) {
|
|
2686
|
+
await abandonOwnedRequest();
|
|
2687
|
+
return false;
|
|
2688
|
+
}
|
|
1841
2689
|
activeAuthSecret = deps.getLeaderAuthSecret
|
|
1842
2690
|
? deps.getLeaderAuthSecret(leader)
|
|
1843
2691
|
: leader?.busSecret;
|
|
@@ -1903,48 +2751,46 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
1903
2751
|
},
|
|
1904
2752
|
});
|
|
1905
2753
|
} catch (error) {
|
|
1906
|
-
if (!isCurrentRequest()) {
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
deps.setActiveAuthSecret?.(undefined);
|
|
1912
|
-
await deps.stopReceiving?.();
|
|
2754
|
+
if (!isCurrentRequest()) {
|
|
2755
|
+
await abandonOwnedRequest();
|
|
2756
|
+
return false;
|
|
2757
|
+
}
|
|
2758
|
+
await failRegistration();
|
|
1913
2759
|
throw error;
|
|
1914
2760
|
}
|
|
1915
|
-
if (!isCurrentRequest()) {
|
|
2761
|
+
if (!isCurrentRequest()) {
|
|
2762
|
+
await abandonOwnedRequest();
|
|
2763
|
+
return false;
|
|
2764
|
+
}
|
|
1916
2765
|
const compatibility = getTelegramBusProtocolCompatibility({
|
|
1917
2766
|
local: deps.protocolIdentity,
|
|
1918
2767
|
remote: response?.kind === "bus.ack" ? response.protocol : undefined,
|
|
1919
2768
|
});
|
|
1920
2769
|
// An identity-less error ACK is a rejection/transport failure, not evidence of an incompatible leader.
|
|
1921
|
-
const unidentifiedRejection =
|
|
2770
|
+
const unidentifiedRejection =
|
|
2771
|
+
response?.kind === "bus.ack" &&
|
|
2772
|
+
!response.ok &&
|
|
2773
|
+
response.protocol === undefined;
|
|
1922
2774
|
if (!compatibility.compatible && !unidentifiedRejection) {
|
|
1923
|
-
|
|
1924
|
-
activeLeaderSocketPath = undefined;
|
|
1925
|
-
activeAuthSecret = undefined;
|
|
1926
|
-
deps.registrationState?.setRegistered(false);
|
|
1927
|
-
deps.setActiveAuthSecret?.(undefined);
|
|
1928
|
-
await deps.stopReceiving?.();
|
|
2775
|
+
await failRegistration();
|
|
1929
2776
|
throw new Error(
|
|
1930
2777
|
`Incompatible Telegram bus leader protocol: ${compatibility.reason}.`,
|
|
1931
2778
|
);
|
|
1932
2779
|
}
|
|
1933
2780
|
if (response?.kind === "bus.ack" && !response.ok) {
|
|
1934
|
-
|
|
1935
|
-
activeLeaderSocketPath = undefined;
|
|
1936
|
-
activeAuthSecret = undefined;
|
|
1937
|
-
deps.registrationState?.setRegistered(false);
|
|
1938
|
-
deps.setActiveAuthSecret?.(undefined);
|
|
1939
|
-
await deps.stopReceiving?.();
|
|
2781
|
+
await failRegistration();
|
|
1940
2782
|
if (
|
|
1941
2783
|
registrationOptions?.restoreWorkspace &&
|
|
1942
2784
|
response.error?.code === "workspace-binding-unavailable"
|
|
1943
2785
|
) {
|
|
1944
|
-
deps.recordRuntimeEvent?.(
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
2786
|
+
deps.recordRuntimeEvent?.(
|
|
2787
|
+
"bus",
|
|
2788
|
+
"Telegram follower auto-connect refused by leader: Workspace binding unavailable.",
|
|
2789
|
+
{
|
|
2790
|
+
phase: "follower-auto-connect-skip",
|
|
2791
|
+
reason: "leader-binding-unavailable",
|
|
2792
|
+
},
|
|
2793
|
+
);
|
|
1948
2794
|
return false;
|
|
1949
2795
|
}
|
|
1950
2796
|
throw new Error(
|
|
@@ -1958,9 +2804,7 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
1958
2804
|
...registrationResult,
|
|
1959
2805
|
generation: registrationGeneration,
|
|
1960
2806
|
sessionId,
|
|
1961
|
-
...(response.protocol
|
|
1962
|
-
? { leaderProtocol: response.protocol }
|
|
1963
|
-
: {}),
|
|
2807
|
+
...(response.protocol ? { leaderProtocol: response.protocol } : {}),
|
|
1964
2808
|
});
|
|
1965
2809
|
lastKnownTarget = registrationResult.target;
|
|
1966
2810
|
lastKnownSlot = registrationResult.slot;
|
|
@@ -1971,19 +2815,22 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
1971
2815
|
try {
|
|
1972
2816
|
await deps.onRegistered?.(ctx);
|
|
1973
2817
|
} catch (error) {
|
|
1974
|
-
if (!isCurrentRequest()) {
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
deps.registrationState?.setRegistered(false);
|
|
1980
|
-
deps.setActiveAuthSecret?.(undefined);
|
|
1981
|
-
await deps.stopReceiving?.();
|
|
2818
|
+
if (!isCurrentRequest()) {
|
|
2819
|
+
await abandonOwnedRequest();
|
|
2820
|
+
return false;
|
|
2821
|
+
}
|
|
2822
|
+
await failRegistration(true);
|
|
1982
2823
|
throw error;
|
|
1983
2824
|
}
|
|
1984
|
-
if (!isCurrentRequest()) {
|
|
2825
|
+
if (!isCurrentRequest()) {
|
|
2826
|
+
await abandonOwnedRequest();
|
|
2827
|
+
return false;
|
|
2828
|
+
}
|
|
1985
2829
|
await requestHeartbeat();
|
|
1986
|
-
if (!isCurrentRequest()) {
|
|
2830
|
+
if (!isCurrentRequest()) {
|
|
2831
|
+
await abandonOwnedRequest();
|
|
2832
|
+
return false;
|
|
2833
|
+
}
|
|
1987
2834
|
startHeartbeat(leaderSocketPath);
|
|
1988
2835
|
if (
|
|
1989
2836
|
pendingHandoffOptions &&
|
|
@@ -1994,51 +2841,82 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
1994
2841
|
}
|
|
1995
2842
|
return true;
|
|
1996
2843
|
}
|
|
1997
|
-
|
|
1998
|
-
activeLeaderSocketPath = undefined;
|
|
1999
|
-
activeAuthSecret = undefined;
|
|
2000
|
-
deps.registrationState?.setRegistered(false);
|
|
2001
|
-
deps.setActiveAuthSecret?.(undefined);
|
|
2002
|
-
await deps.stopReceiving?.();
|
|
2844
|
+
await failRegistration();
|
|
2003
2845
|
return false;
|
|
2004
2846
|
},
|
|
2005
2847
|
setContext(ctx) {
|
|
2006
|
-
if (
|
|
2007
|
-
|
|
2008
|
-
|
|
2848
|
+
if (
|
|
2849
|
+
registrationAttempt &&
|
|
2850
|
+
(registrationAttempt.ctx !== ctx ||
|
|
2851
|
+
registrationAttempt.sessionGeneration !==
|
|
2852
|
+
deps.getSessionGeneration?.() ||
|
|
2853
|
+
registrationAttempt.sessionId !== deps.getSessionId?.(ctx))
|
|
2854
|
+
)
|
|
2855
|
+
registrationAttempt = undefined;
|
|
2009
2856
|
activeContext = ctx;
|
|
2010
2857
|
},
|
|
2011
2858
|
async setThreadDisplayMode(mode) {
|
|
2012
2859
|
const socketPath = activeLeaderSocketPath;
|
|
2013
2860
|
const generation = activeRegistrationGeneration;
|
|
2014
2861
|
const auth = activeAuthSecret;
|
|
2015
|
-
if (
|
|
2862
|
+
if (
|
|
2863
|
+
!socketPath ||
|
|
2864
|
+
!generation ||
|
|
2865
|
+
!deps.registrationState?.isRegistered()
|
|
2866
|
+
) {
|
|
2016
2867
|
throw new Error("Telegram follower is not registered with the leader.");
|
|
2017
2868
|
}
|
|
2018
|
-
const requiredCapability =
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2869
|
+
const requiredCapability =
|
|
2870
|
+
mode === "directory-snake" || mode === "directory-title"
|
|
2871
|
+
? TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT
|
|
2872
|
+
: TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE;
|
|
2873
|
+
if (
|
|
2874
|
+
!hasTelegramBusCapability(deps.protocolIdentity, requiredCapability) ||
|
|
2875
|
+
!hasTelegramBusCapability(
|
|
2876
|
+
deps.registrationState.getLeaderProtocol(),
|
|
2877
|
+
requiredCapability,
|
|
2878
|
+
)
|
|
2879
|
+
) {
|
|
2880
|
+
throw new Error(
|
|
2881
|
+
"The Telegram peers do not support this Thread display setting. Update or restart both instances.",
|
|
2882
|
+
);
|
|
2024
2883
|
}
|
|
2025
2884
|
const requestId = deps.createRequestId();
|
|
2026
2885
|
const response = await sendTelegramBusLocalEnvelope({
|
|
2027
|
-
socketPath,
|
|
2886
|
+
socketPath,
|
|
2887
|
+
timeoutMs: registrationTimeoutMs,
|
|
2028
2888
|
envelope: {
|
|
2029
|
-
kind: "follower.setThreadDisplayMode",
|
|
2030
|
-
|
|
2889
|
+
kind: "follower.setThreadDisplayMode",
|
|
2890
|
+
requestId,
|
|
2891
|
+
auth,
|
|
2892
|
+
instanceId: deps.instanceId,
|
|
2893
|
+
registrationGeneration: generation,
|
|
2894
|
+
mode,
|
|
2031
2895
|
},
|
|
2032
2896
|
});
|
|
2033
|
-
if (
|
|
2034
|
-
|
|
2035
|
-
|
|
2897
|
+
if (
|
|
2898
|
+
activeLeaderSocketPath !== socketPath ||
|
|
2899
|
+
activeRegistrationGeneration !== generation ||
|
|
2900
|
+
activeAuthSecret !== auth ||
|
|
2901
|
+
deps.registrationState.getGeneration() !== generation
|
|
2902
|
+
) {
|
|
2903
|
+
throw new Error(
|
|
2904
|
+
"Telegram Thread display setting completed for a stale registration.",
|
|
2905
|
+
);
|
|
2036
2906
|
}
|
|
2037
|
-
if (
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2907
|
+
if (
|
|
2908
|
+
response?.kind !== "bus.ack" ||
|
|
2909
|
+
!response.ok ||
|
|
2910
|
+
response.requestId !== requestId ||
|
|
2911
|
+
!isRecord(response.result) ||
|
|
2912
|
+
response.result.mode !== mode
|
|
2913
|
+
) {
|
|
2914
|
+
throw new Error(
|
|
2915
|
+
response?.kind === "bus.ack"
|
|
2916
|
+
? (response.message ??
|
|
2917
|
+
"Telegram Thread display setting was rejected.")
|
|
2918
|
+
: "Telegram Thread display setting was not acknowledged.",
|
|
2919
|
+
);
|
|
2042
2920
|
}
|
|
2043
2921
|
},
|
|
2044
2922
|
async renameThread(target, threadName) {
|
|
@@ -2059,14 +2937,10 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
2059
2937
|
const expectedLeaderSocketPath = activeLeaderSocketPath;
|
|
2060
2938
|
const expectedRegistrationGeneration = activeRegistrationGeneration;
|
|
2061
2939
|
const expectedAuthSecret = activeAuthSecret;
|
|
2062
|
-
const response = await
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
endpoint: expectedLeaderSocketPath,
|
|
2067
|
-
operation: "operation",
|
|
2068
|
-
}),
|
|
2069
|
-
envelope: {
|
|
2940
|
+
const response = await sendTelegramBusOperation(
|
|
2941
|
+
expectedLeaderSocketPath,
|
|
2942
|
+
registrationTimeoutMs,
|
|
2943
|
+
{
|
|
2070
2944
|
kind: "follower.renameThread",
|
|
2071
2945
|
requestId: deps.createRequestId(),
|
|
2072
2946
|
auth: expectedAuthSecret,
|
|
@@ -2076,20 +2950,20 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
2076
2950
|
threadName,
|
|
2077
2951
|
sentAtMs: getNowMs(),
|
|
2078
2952
|
},
|
|
2079
|
-
|
|
2953
|
+
);
|
|
2080
2954
|
if (response?.kind !== "bus.ack" || !response.ok) {
|
|
2081
2955
|
throw new Error(
|
|
2082
2956
|
response?.kind === "bus.ack"
|
|
2083
|
-
? (response.message ??
|
|
2957
|
+
? (response.message ??
|
|
2958
|
+
"Telegram Workspace Thread rename was rejected.")
|
|
2084
2959
|
: "Telegram Workspace Thread rename was not acknowledged.",
|
|
2085
2960
|
);
|
|
2086
2961
|
}
|
|
2087
2962
|
if (
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
expectedRegistrationGeneration)
|
|
2963
|
+
!isLeaderSessionCurrent(
|
|
2964
|
+
expectedLeaderSocketPath,
|
|
2965
|
+
expectedRegistrationGeneration,
|
|
2966
|
+
)
|
|
2093
2967
|
) {
|
|
2094
2968
|
throw new Error(
|
|
2095
2969
|
"Telegram Workspace Thread rename completed for a stale follower registration.",
|
|
@@ -2117,24 +2991,23 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
2117
2991
|
if (!activeLeaderSocketPath || !activeRegistrationGeneration) {
|
|
2118
2992
|
throw new Error("Telegram follower is not registered with the leader.");
|
|
2119
2993
|
}
|
|
2120
|
-
if (
|
|
2121
|
-
deps.registrationState
|
|
2122
|
-
|
|
2123
|
-
|
|
2994
|
+
if (
|
|
2995
|
+
deps.registrationState &&
|
|
2996
|
+
!hasTelegramBusCapability(
|
|
2997
|
+
deps.registrationState.getLeaderProtocol(),
|
|
2998
|
+
TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME,
|
|
2999
|
+
)
|
|
3000
|
+
) {
|
|
2124
3001
|
throw new Error(
|
|
2125
3002
|
"The active Telegram leader does not support Workspace Thread reset. Update or restart that Pi instance.",
|
|
2126
3003
|
);
|
|
2127
3004
|
}
|
|
2128
3005
|
const expectedLeaderSocketPath = activeLeaderSocketPath;
|
|
2129
3006
|
const expectedRegistrationGeneration = activeRegistrationGeneration;
|
|
2130
|
-
const response = await
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
endpoint: expectedLeaderSocketPath,
|
|
2135
|
-
operation: "operation",
|
|
2136
|
-
}),
|
|
2137
|
-
envelope: {
|
|
3007
|
+
const response = await sendTelegramBusOperation(
|
|
3008
|
+
expectedLeaderSocketPath,
|
|
3009
|
+
registrationTimeoutMs,
|
|
3010
|
+
{
|
|
2138
3011
|
kind: "follower.resetThreadName",
|
|
2139
3012
|
requestId: deps.createRequestId(),
|
|
2140
3013
|
auth: activeAuthSecret,
|
|
@@ -2143,21 +3016,30 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
2143
3016
|
target,
|
|
2144
3017
|
sentAtMs: getNowMs(),
|
|
2145
3018
|
},
|
|
2146
|
-
|
|
2147
|
-
const resetName =
|
|
2148
|
-
response
|
|
3019
|
+
);
|
|
3020
|
+
const resetName =
|
|
3021
|
+
response?.kind === "bus.ack" &&
|
|
3022
|
+
response.ok &&
|
|
3023
|
+
response.result &&
|
|
3024
|
+
typeof response.result === "object" &&
|
|
2149
3025
|
"threadName" in response.result &&
|
|
2150
3026
|
typeof response.result.threadName === "string"
|
|
2151
|
-
|
|
3027
|
+
? response.result.threadName
|
|
3028
|
+
: undefined;
|
|
2152
3029
|
if (!resetName) {
|
|
2153
|
-
throw new Error(
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
3030
|
+
throw new Error(
|
|
3031
|
+
response?.kind === "bus.ack"
|
|
3032
|
+
? (response.message ??
|
|
3033
|
+
"Telegram Workspace Thread reset was rejected.")
|
|
3034
|
+
: "Telegram Workspace Thread reset was not acknowledged.",
|
|
3035
|
+
);
|
|
3036
|
+
}
|
|
3037
|
+
if (
|
|
3038
|
+
!isLeaderSessionCurrent(
|
|
3039
|
+
expectedLeaderSocketPath,
|
|
3040
|
+
expectedRegistrationGeneration,
|
|
3041
|
+
)
|
|
3042
|
+
) {
|
|
2161
3043
|
throw new Error(
|
|
2162
3044
|
"Telegram Workspace Thread reset completed for a stale follower registration.",
|
|
2163
3045
|
);
|
|
@@ -2195,17 +3077,14 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
2195
3077
|
const expectedRegistrationGeneration = activeRegistrationGeneration;
|
|
2196
3078
|
const expectedAuthSecret = activeAuthSecret;
|
|
2197
3079
|
const requestId = deps.createRequestId();
|
|
2198
|
-
const response = await
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
kind: operation === "publish"
|
|
2207
|
-
? "follower.publishSessionReplacement"
|
|
2208
|
-
: "follower.settleSessionReplacement",
|
|
3080
|
+
const response = await sendTelegramBusOperation(
|
|
3081
|
+
expectedLeaderSocketPath,
|
|
3082
|
+
registrationTimeoutMs,
|
|
3083
|
+
{
|
|
3084
|
+
kind:
|
|
3085
|
+
operation === "publish"
|
|
3086
|
+
? "follower.publishSessionReplacement"
|
|
3087
|
+
: "follower.settleSessionReplacement",
|
|
2209
3088
|
requestId,
|
|
2210
3089
|
auth: expectedAuthSecret,
|
|
2211
3090
|
instanceId: deps.instanceId,
|
|
@@ -2213,11 +3092,13 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
2213
3092
|
intent,
|
|
2214
3093
|
sentAtMs: getNowMs(),
|
|
2215
3094
|
},
|
|
2216
|
-
|
|
3095
|
+
);
|
|
2217
3096
|
if (
|
|
2218
|
-
response?.kind !== "bus.ack" ||
|
|
3097
|
+
response?.kind !== "bus.ack" ||
|
|
3098
|
+
!response.ok ||
|
|
2219
3099
|
response.requestId !== requestId ||
|
|
2220
|
-
!isRecord(response.result) ||
|
|
3100
|
+
!isRecord(response.result) ||
|
|
3101
|
+
response.result.committed !== true
|
|
2221
3102
|
) {
|
|
2222
3103
|
throw new Error(
|
|
2223
3104
|
response?.kind === "bus.ack"
|
|
@@ -2226,12 +3107,11 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
2226
3107
|
);
|
|
2227
3108
|
}
|
|
2228
3109
|
if (
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
expectedRegistrationGeneration)
|
|
3110
|
+
!isLeaderSessionCurrent(
|
|
3111
|
+
expectedLeaderSocketPath,
|
|
3112
|
+
expectedRegistrationGeneration,
|
|
3113
|
+
) ||
|
|
3114
|
+
activeAuthSecret !== expectedAuthSecret
|
|
2235
3115
|
) {
|
|
2236
3116
|
throw new Error(
|
|
2237
3117
|
"Telegram session replacement completed for a stale follower registration.",
|
|
@@ -2243,14 +3123,10 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
2243
3123
|
if (!activeLeaderSocketPath || !activeRegistrationGeneration) {
|
|
2244
3124
|
return false;
|
|
2245
3125
|
}
|
|
2246
|
-
const response = await
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
endpoint: activeLeaderSocketPath,
|
|
2251
|
-
operation: "operation",
|
|
2252
|
-
}),
|
|
2253
|
-
envelope: {
|
|
3126
|
+
const response = await sendTelegramBusOperation(
|
|
3127
|
+
activeLeaderSocketPath,
|
|
3128
|
+
registrationTimeoutMs,
|
|
3129
|
+
{
|
|
2254
3130
|
kind: "follower.disconnect",
|
|
2255
3131
|
requestId: deps.createRequestId(),
|
|
2256
3132
|
auth: activeAuthSecret,
|
|
@@ -2258,7 +3134,7 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
2258
3134
|
registrationGeneration: activeRegistrationGeneration,
|
|
2259
3135
|
sentAtMs: getNowMs(),
|
|
2260
3136
|
},
|
|
2261
|
-
|
|
3137
|
+
);
|
|
2262
3138
|
if (response?.kind === "bus.ack" && response.ok) return true;
|
|
2263
3139
|
throw new Error(
|
|
2264
3140
|
response?.kind === "bus.ack"
|
|
@@ -2283,28 +3159,72 @@ export function createTelegramBusFollowerPairedAdmission(deps: {
|
|
|
2283
3159
|
return (updates, publish) => {
|
|
2284
3160
|
if (updates.length !== 1) return { admitted: false };
|
|
2285
3161
|
const update = updates[0]!;
|
|
2286
|
-
const keys = Object.keys(update).filter(
|
|
2287
|
-
key
|
|
3162
|
+
const keys = Object.keys(update).filter(
|
|
3163
|
+
(key) =>
|
|
3164
|
+
key !== "update_id" &&
|
|
3165
|
+
key !== TELEGRAM_FOLLOWER_FORWARD_BATCH_POSITION_FIELD,
|
|
3166
|
+
);
|
|
2288
3167
|
if (keys.length !== 1) return { admitted: false };
|
|
2289
3168
|
const kind = keys[0];
|
|
2290
|
-
if (
|
|
2291
|
-
|
|
3169
|
+
if (
|
|
3170
|
+
kind !== "message" &&
|
|
3171
|
+
kind !== "edited_message" &&
|
|
3172
|
+
kind !== "callback_query" &&
|
|
3173
|
+
kind !== "message_reaction"
|
|
3174
|
+
)
|
|
3175
|
+
return { admitted: false };
|
|
2292
3176
|
const position = update[TELEGRAM_FOLLOWER_FORWARD_BATCH_POSITION_FIELD];
|
|
2293
|
-
if (
|
|
2294
|
-
|
|
3177
|
+
if (
|
|
3178
|
+
position !== undefined &&
|
|
3179
|
+
(kind !== "message" || (position !== "comment" && position !== "forward"))
|
|
3180
|
+
)
|
|
3181
|
+
return { admitted: false };
|
|
2295
3182
|
const carrier = update[kind];
|
|
2296
|
-
if (
|
|
2297
|
-
|
|
2298
|
-
|
|
3183
|
+
if (
|
|
3184
|
+
!Number.isSafeInteger(update.update_id) ||
|
|
3185
|
+
update.update_id < 0 ||
|
|
3186
|
+
!isRecord(carrier) ||
|
|
3187
|
+
carrier.pi_telegram_source_update_id !== update.update_id ||
|
|
3188
|
+
carrier.sender_chat !== undefined ||
|
|
3189
|
+
carrier.actor_chat !== undefined
|
|
3190
|
+
)
|
|
3191
|
+
return { admitted: false };
|
|
2299
3192
|
const sender = kind === "message_reaction" ? carrier.user : carrier.from;
|
|
2300
|
-
if (
|
|
2301
|
-
|
|
3193
|
+
if (
|
|
3194
|
+
!isRecord(sender) ||
|
|
3195
|
+
typeof sender.id !== "number" ||
|
|
3196
|
+
!Number.isSafeInteger(sender.id) ||
|
|
3197
|
+
sender.id <= 0 ||
|
|
3198
|
+
sender.is_bot !== false
|
|
3199
|
+
)
|
|
3200
|
+
return { admitted: false };
|
|
2302
3201
|
return deps.configStore.withPairedUserAdmission(
|
|
2303
|
-
deps.profileName,
|
|
3202
|
+
deps.profileName,
|
|
3203
|
+
deps.tokenSha256,
|
|
3204
|
+
sender.id,
|
|
3205
|
+
publish,
|
|
3206
|
+
deps.assertExecutionCurrent,
|
|
2304
3207
|
);
|
|
2305
3208
|
};
|
|
2306
3209
|
}
|
|
2307
3210
|
|
|
3211
|
+
/** One authenticated local operation request with the shared operation retry policy; no replay semantics. */
|
|
3212
|
+
function sendTelegramBusOperation(
|
|
3213
|
+
socketPath: string,
|
|
3214
|
+
timeoutMs: number,
|
|
3215
|
+
envelope: TelegramBusEnvelope,
|
|
3216
|
+
) {
|
|
3217
|
+
return sendTelegramBusLocalEnvelope({
|
|
3218
|
+
socketPath,
|
|
3219
|
+
timeoutMs,
|
|
3220
|
+
envelope,
|
|
3221
|
+
retry: getTelegramBusTransportRetryPolicy({
|
|
3222
|
+
endpoint: socketPath,
|
|
3223
|
+
operation: "operation",
|
|
3224
|
+
}),
|
|
3225
|
+
});
|
|
3226
|
+
}
|
|
3227
|
+
|
|
2308
3228
|
export function prepareTelegramBusFollowerJournaledUpdateForExecution<
|
|
2309
3229
|
TUpdate extends { message?: unknown } & Record<string, unknown>,
|
|
2310
3230
|
>(
|
|
@@ -2330,7 +3250,713 @@ export function prepareTelegramBusFollowerJournaledUpdateForExecution<
|
|
|
2330
3250
|
return prepared;
|
|
2331
3251
|
}
|
|
2332
3252
|
|
|
2333
|
-
|
|
3253
|
+
type LiveRebindCarrierEnvelope = Pick<
|
|
3254
|
+
Extract<TelegramBusEnvelope, { kind: "leader.settleLiveRebind" }>,
|
|
3255
|
+
| "operationId"
|
|
3256
|
+
| "recipientInstanceId"
|
|
3257
|
+
| "recipientSessionId"
|
|
3258
|
+
| "recipientRegistrationGeneration"
|
|
3259
|
+
| "recipientBindingKey"
|
|
3260
|
+
| "sourceUpdateIds"
|
|
3261
|
+
| "selectedCommand"
|
|
3262
|
+
| "preparedSource"
|
|
3263
|
+
>;
|
|
3264
|
+
|
|
3265
|
+
/** Copy the captured command target for the native owner; ordinary input supplies no target constraint. */
|
|
3266
|
+
function copyLiveRebindCommandTarget(
|
|
3267
|
+
saved: TelegramBusLiveRebindSaveObservation,
|
|
3268
|
+
): { expectedTarget?: TelegramTarget & { threadId: number } } {
|
|
3269
|
+
return saved.selectedCommand
|
|
3270
|
+
? { expectedTarget: { ...saved.selectedCommand.target } }
|
|
3271
|
+
: {};
|
|
3272
|
+
}
|
|
3273
|
+
|
|
3274
|
+
/** Exact operation/recipient/source agreement between a retained save and a later request; identity only, never currency. */
|
|
3275
|
+
function matchesLiveRebindCarrier(
|
|
3276
|
+
saved: TelegramBusLiveRebindSaveObservation,
|
|
3277
|
+
envelope: LiveRebindCarrierEnvelope,
|
|
3278
|
+
): boolean {
|
|
3279
|
+
return (
|
|
3280
|
+
saved.operationId === envelope.operationId &&
|
|
3281
|
+
saved.recipient.instanceId === envelope.recipientInstanceId &&
|
|
3282
|
+
saved.recipient.sessionId === envelope.recipientSessionId &&
|
|
3283
|
+
saved.recipient.generation === envelope.recipientRegistrationGeneration &&
|
|
3284
|
+
saved.recipient.bindingKey === envelope.recipientBindingKey &&
|
|
3285
|
+
isDeepStrictEqual(
|
|
3286
|
+
saved.sourceUpdateIds,
|
|
3287
|
+
[...envelope.sourceUpdateIds].sort((a, b) => a - b),
|
|
3288
|
+
)
|
|
3289
|
+
);
|
|
3290
|
+
}
|
|
3291
|
+
|
|
3292
|
+
/** Exact branch agreement: a command record requires its saved proof; ordinary input refuses any proof. */
|
|
3293
|
+
function matchesLiveRebindBranch(
|
|
3294
|
+
saved: TelegramBusLiveRebindSaveObservation,
|
|
3295
|
+
status: boolean,
|
|
3296
|
+
envelope: LiveRebindCarrierEnvelope,
|
|
3297
|
+
): boolean {
|
|
3298
|
+
return (
|
|
3299
|
+
isDeepStrictEqual(saved.selectedCommand, envelope.selectedCommand) &&
|
|
3300
|
+
(status
|
|
3301
|
+
? isDeepStrictEqual(saved.preparedSource, envelope.preparedSource)
|
|
3302
|
+
: envelope.preparedSource === undefined)
|
|
3303
|
+
);
|
|
3304
|
+
}
|
|
3305
|
+
|
|
3306
|
+
/** Existing Commands/menu assembly preflights the copied input and captures independent recipient/transport owners. */
|
|
3307
|
+
export interface TelegramBusFollowerCommandOwner<TContext> {
|
|
3308
|
+
isCurrent(): boolean;
|
|
3309
|
+
assertRecipientCurrent(): void;
|
|
3310
|
+
prepare(
|
|
3311
|
+
readiness: TelegramLiveSourceCompletionReadiness,
|
|
3312
|
+
ctx: TContext,
|
|
3313
|
+
input: {
|
|
3314
|
+
target: TelegramTarget & { threadId: number };
|
|
3315
|
+
assertSourceCurrent(): void;
|
|
3316
|
+
assertRecipientCurrent(): void;
|
|
3317
|
+
},
|
|
3318
|
+
): TelegramPreparedHeldCommand | undefined;
|
|
3319
|
+
}
|
|
3320
|
+
|
|
3321
|
+
/** One current warm preparation, not a prompt queue or restart recovery record. */
|
|
3322
|
+
export function createTelegramBusFollowerLiveRebindRuntime<TContext>(deps: {
|
|
3323
|
+
getAdmission(
|
|
3324
|
+
ctx: TContext,
|
|
3325
|
+
):
|
|
3326
|
+
| Pick<
|
|
3327
|
+
TelegramUpdateAdmissionLifecycleRuntime<TContext>,
|
|
3328
|
+
"prepareLiveInput" | "appendBatch" | "getJournalBindingKey"
|
|
3329
|
+
>
|
|
3330
|
+
| undefined;
|
|
3331
|
+
applyTarget?: (
|
|
3332
|
+
...args: Parameters<
|
|
3333
|
+
ReturnType<
|
|
3334
|
+
typeof createTelegramBusFollowerWorkspaceRestoreHandler<TContext>
|
|
3335
|
+
>
|
|
3336
|
+
>
|
|
3337
|
+
) => ReturnType<
|
|
3338
|
+
ReturnType<
|
|
3339
|
+
typeof createTelegramBusFollowerWorkspaceRestoreHandler<TContext>
|
|
3340
|
+
>
|
|
3341
|
+
>;
|
|
3342
|
+
observeWork?: (
|
|
3343
|
+
oldTarget: TelegramTarget & { threadId: number },
|
|
3344
|
+
ctx: TContext,
|
|
3345
|
+
) => TelegramBusLiveRebindWorkState;
|
|
3346
|
+
/** Pure preflight: undefined refuses before hold/append; this port never activates future recipient authority. */
|
|
3347
|
+
getCommandOwner?: (
|
|
3348
|
+
input: Readonly<
|
|
3349
|
+
Omit<
|
|
3350
|
+
Extract<TelegramBusEnvelope, { kind: "leader.prepareLiveRebind" }>,
|
|
3351
|
+
"kind" | "auth" | "requestId" | "sentAtMs"
|
|
3352
|
+
>
|
|
3353
|
+
>,
|
|
3354
|
+
ctx: TContext,
|
|
3355
|
+
) => TelegramBusFollowerCommandOwner<TContext> | undefined;
|
|
3356
|
+
}) {
|
|
3357
|
+
type Status = {
|
|
3358
|
+
owner: TelegramBusFollowerCommandOwner<TContext>;
|
|
3359
|
+
current(): boolean;
|
|
3360
|
+
prepare: TelegramBusFollowerCommandOwner<TContext>["prepare"];
|
|
3361
|
+
recipient: () => void;
|
|
3362
|
+
attempted?: true;
|
|
3363
|
+
plan?: TelegramPreparedHeldCommand;
|
|
3364
|
+
ports?: Pick<
|
|
3365
|
+
TelegramPreparedHeldCommand,
|
|
3366
|
+
"bindCarrier" | "execute" | "inspectCompletion"
|
|
3367
|
+
>;
|
|
3368
|
+
result?: "completed" | "unknown";
|
|
3369
|
+
};
|
|
3370
|
+
let retained:
|
|
3371
|
+
| {
|
|
3372
|
+
operationId: string;
|
|
3373
|
+
digest: string;
|
|
3374
|
+
current(): boolean;
|
|
3375
|
+
held: TelegramLiveInputPreparation;
|
|
3376
|
+
ctx: TContext;
|
|
3377
|
+
observation: TelegramBusLiveRebindSaveObservation;
|
|
3378
|
+
applyIssued?: true;
|
|
3379
|
+
applying?: true;
|
|
3380
|
+
settling?: true;
|
|
3381
|
+
outcome?: TelegramBusLiveRebindSettlementObservation;
|
|
3382
|
+
status?: Status;
|
|
3383
|
+
}
|
|
3384
|
+
| undefined;
|
|
3385
|
+
const runtime = {
|
|
3386
|
+
save(
|
|
3387
|
+
envelope: Extract<
|
|
3388
|
+
TelegramBusEnvelope,
|
|
3389
|
+
{ kind: "leader.prepareLiveRebind" }
|
|
3390
|
+
>,
|
|
3391
|
+
ctx: TContext,
|
|
3392
|
+
isCurrent: () => boolean,
|
|
3393
|
+
): TelegramBusLiveRebindSaveObservation {
|
|
3394
|
+
const captured = structuredClone(envelope),
|
|
3395
|
+
selectedCommand = captured.selectedCommand;
|
|
3396
|
+
const heldMarker = selectedCommand,
|
|
3397
|
+
getCommandOwner = heldMarker ? deps.getCommandOwner : undefined,
|
|
3398
|
+
applyOwner = deps.applyTarget;
|
|
3399
|
+
if (
|
|
3400
|
+
heldMarker !== undefined &&
|
|
3401
|
+
(!getCommandOwner || !applyOwner || captured.updates.length !== 1)
|
|
3402
|
+
)
|
|
3403
|
+
throw new Error("Selected command native consumption is unavailable.");
|
|
3404
|
+
const admission = deps.getAdmission(ctx),
|
|
3405
|
+
prepareInput = admission?.prepareLiveInput,
|
|
3406
|
+
append = admission?.appendBatch;
|
|
3407
|
+
let status =
|
|
3408
|
+
retained?.operationId === captured.operationId
|
|
3409
|
+
? retained.status
|
|
3410
|
+
: undefined;
|
|
3411
|
+
const current = () =>
|
|
3412
|
+
isCurrent() &&
|
|
3413
|
+
deps.getAdmission(ctx) === admission &&
|
|
3414
|
+
admission?.getJournalBindingKey() === captured.recipientBindingKey &&
|
|
3415
|
+
(!heldMarker ||
|
|
3416
|
+
(deps.getCommandOwner === getCommandOwner &&
|
|
3417
|
+
deps.applyTarget === applyOwner &&
|
|
3418
|
+
admission?.prepareLiveInput === prepareInput &&
|
|
3419
|
+
admission?.appendBatch === append &&
|
|
3420
|
+
!!status?.current()));
|
|
3421
|
+
if (
|
|
3422
|
+
retained?.status &&
|
|
3423
|
+
retained.operationId !== captured.operationId &&
|
|
3424
|
+
retained.outcome?.status === "released" &&
|
|
3425
|
+
(retained.status.result !== "completed" ||
|
|
3426
|
+
!retained.status.ports?.inspectCompletion.call(retained.status.plan))
|
|
3427
|
+
)
|
|
3428
|
+
throw new Error(
|
|
3429
|
+
"Released held command is unconfirmed; no carrier replacement.",
|
|
3430
|
+
);
|
|
3431
|
+
if (heldMarker && !status) {
|
|
3432
|
+
const {
|
|
3433
|
+
recipientInstanceId,
|
|
3434
|
+
recipientRegistrationGeneration,
|
|
3435
|
+
recipientSessionId,
|
|
3436
|
+
recipientBindingKey,
|
|
3437
|
+
operationId,
|
|
3438
|
+
updates,
|
|
3439
|
+
} = captured;
|
|
3440
|
+
const owner = getCommandOwner!(
|
|
3441
|
+
structuredClone({
|
|
3442
|
+
recipientInstanceId,
|
|
3443
|
+
recipientRegistrationGeneration,
|
|
3444
|
+
recipientSessionId,
|
|
3445
|
+
recipientBindingKey,
|
|
3446
|
+
operationId,
|
|
3447
|
+
updates,
|
|
3448
|
+
selectedCommand,
|
|
3449
|
+
}),
|
|
3450
|
+
ctx,
|
|
3451
|
+
);
|
|
3452
|
+
if (
|
|
3453
|
+
!owner ||
|
|
3454
|
+
typeof owner.isCurrent !== "function" ||
|
|
3455
|
+
typeof owner.prepare !== "function" ||
|
|
3456
|
+
typeof owner.assertRecipientCurrent !== "function"
|
|
3457
|
+
)
|
|
3458
|
+
throw new Error("Selected command preparation owner is unavailable.");
|
|
3459
|
+
const ports = {
|
|
3460
|
+
isCurrent: owner.isCurrent,
|
|
3461
|
+
prepare: owner.prepare,
|
|
3462
|
+
assertRecipientCurrent: owner.assertRecipientCurrent,
|
|
3463
|
+
};
|
|
3464
|
+
status = {
|
|
3465
|
+
owner,
|
|
3466
|
+
current: () =>
|
|
3467
|
+
Object.entries(ports).every(
|
|
3468
|
+
([key, value]) => Reflect.get(owner, key) === value,
|
|
3469
|
+
) &&
|
|
3470
|
+
(!status?.plan ||
|
|
3471
|
+
Object.entries(status.ports!).every(
|
|
3472
|
+
([key, value]) => Reflect.get(status!.plan!, key) === value,
|
|
3473
|
+
)) &&
|
|
3474
|
+
ports.isCurrent.call(owner),
|
|
3475
|
+
prepare: ports.prepare,
|
|
3476
|
+
recipient: ports.assertRecipientCurrent,
|
|
3477
|
+
};
|
|
3478
|
+
}
|
|
3479
|
+
if (!admission?.prepareLiveInput || !current())
|
|
3480
|
+
throw new Error("Live-rebind recipient admission is unavailable.");
|
|
3481
|
+
const digest = createHash("sha256")
|
|
3482
|
+
.update(JSON.stringify(captured.updates))
|
|
3483
|
+
.digest("hex");
|
|
3484
|
+
if (retained?.status && !retained.current())
|
|
3485
|
+
throw new Error(
|
|
3486
|
+
"Prepared command authority changed; no replacement adoption.",
|
|
3487
|
+
);
|
|
3488
|
+
if (
|
|
3489
|
+
retained &&
|
|
3490
|
+
(!retained.current() ||
|
|
3491
|
+
(retained.operationId !== envelope.operationId &&
|
|
3492
|
+
(retained.outcome?.status === "released" ||
|
|
3493
|
+
retained.outcome?.status === "discarded")))
|
|
3494
|
+
)
|
|
3495
|
+
retained = undefined;
|
|
3496
|
+
if (
|
|
3497
|
+
heldMarker &&
|
|
3498
|
+
retained &&
|
|
3499
|
+
(retained.ctx !== ctx ||
|
|
3500
|
+
retained.observation.recipient.instanceId !==
|
|
3501
|
+
captured.recipientInstanceId ||
|
|
3502
|
+
retained.observation.recipient.sessionId !==
|
|
3503
|
+
captured.recipientSessionId ||
|
|
3504
|
+
retained.observation.recipient.generation !==
|
|
3505
|
+
captured.recipientRegistrationGeneration ||
|
|
3506
|
+
retained.observation.recipient.bindingKey !==
|
|
3507
|
+
captured.recipientBindingKey)
|
|
3508
|
+
)
|
|
3509
|
+
throw new Error("Selected command saved recipient changed.");
|
|
3510
|
+
if (
|
|
3511
|
+
retained &&
|
|
3512
|
+
(retained.operationId !== captured.operationId ||
|
|
3513
|
+
retained.digest !== digest ||
|
|
3514
|
+
!isDeepStrictEqual(
|
|
3515
|
+
retained.observation.selectedCommand,
|
|
3516
|
+
selectedCommand,
|
|
3517
|
+
))
|
|
3518
|
+
)
|
|
3519
|
+
throw new Error(
|
|
3520
|
+
"Another live-rebind input or branch is already prepared.",
|
|
3521
|
+
);
|
|
3522
|
+
if (!retained) {
|
|
3523
|
+
const held = admission.prepareLiveInput(
|
|
3524
|
+
ctx,
|
|
3525
|
+
captured.updates.map((update) => update.update_id),
|
|
3526
|
+
current,
|
|
3527
|
+
);
|
|
3528
|
+
if (!held)
|
|
3529
|
+
throw new Error("Live-rebind recipient input cannot be prepared.");
|
|
3530
|
+
if (heldMarker && !held.canPrepareStatusConsumption?.()) {
|
|
3531
|
+
held.cancelEmpty();
|
|
3532
|
+
throw new Error(
|
|
3533
|
+
"Selected command native consumption is unavailable.",
|
|
3534
|
+
);
|
|
3535
|
+
}
|
|
3536
|
+
retained = {
|
|
3537
|
+
operationId: captured.operationId,
|
|
3538
|
+
digest,
|
|
3539
|
+
current,
|
|
3540
|
+
held,
|
|
3541
|
+
ctx,
|
|
3542
|
+
...(status ? { status } : {}),
|
|
3543
|
+
observation: {
|
|
3544
|
+
operationId: captured.operationId,
|
|
3545
|
+
recipient: {
|
|
3546
|
+
instanceId: captured.recipientInstanceId,
|
|
3547
|
+
sessionId: captured.recipientSessionId,
|
|
3548
|
+
generation: captured.recipientRegistrationGeneration,
|
|
3549
|
+
bindingKey: captured.recipientBindingKey,
|
|
3550
|
+
},
|
|
3551
|
+
sourceUpdateIds: [...held.sourceUpdateIds].sort((a, b) => a - b),
|
|
3552
|
+
...(selectedCommand
|
|
3553
|
+
? { selectedCommand: structuredClone(selectedCommand) }
|
|
3554
|
+
: {}),
|
|
3555
|
+
status: "saved",
|
|
3556
|
+
},
|
|
3557
|
+
};
|
|
3558
|
+
try {
|
|
3559
|
+
admission.appendBatch(captured.updates);
|
|
3560
|
+
} catch (error) {
|
|
3561
|
+
if (!held.confirmSaved()) throw error;
|
|
3562
|
+
}
|
|
3563
|
+
}
|
|
3564
|
+
if (!current() || !retained.held.confirmSaved())
|
|
3565
|
+
throw new Error("Live-rebind recipient save is unconfirmed.");
|
|
3566
|
+
if (status && !status.plan) {
|
|
3567
|
+
if (status.attempted)
|
|
3568
|
+
throw new Error(
|
|
3569
|
+
"Selected command preparation is unconfirmed; no replay.",
|
|
3570
|
+
);
|
|
3571
|
+
status.attempted = true;
|
|
3572
|
+
const record = retained,
|
|
3573
|
+
readiness = record.held.prepareSourceCompletion?.();
|
|
3574
|
+
const assertSourceCurrent = () => {
|
|
3575
|
+
if (
|
|
3576
|
+
retained !== record ||
|
|
3577
|
+
!record.current() ||
|
|
3578
|
+
!record.held.isOwnerCurrent()
|
|
3579
|
+
)
|
|
3580
|
+
throw new Error("Selected command source owner changed.");
|
|
3581
|
+
};
|
|
3582
|
+
const plan =
|
|
3583
|
+
readiness &&
|
|
3584
|
+
status.prepare.call(status.owner, readiness, ctx, {
|
|
3585
|
+
target: { ...heldMarker!.target },
|
|
3586
|
+
assertSourceCurrent,
|
|
3587
|
+
assertRecipientCurrent: status.recipient,
|
|
3588
|
+
});
|
|
3589
|
+
if (
|
|
3590
|
+
!plan ||
|
|
3591
|
+
!current() ||
|
|
3592
|
+
!record.held.confirmSaved() ||
|
|
3593
|
+
!isDeepStrictEqual(plan.source, readiness!.snapshot.source) ||
|
|
3594
|
+
[plan.bindCarrier, plan.execute, plan.inspectCompletion].some(
|
|
3595
|
+
(port) => typeof port !== "function",
|
|
3596
|
+
)
|
|
3597
|
+
)
|
|
3598
|
+
throw new Error("Selected command plan is unavailable.");
|
|
3599
|
+
status.plan = plan;
|
|
3600
|
+
status.ports = {
|
|
3601
|
+
bindCarrier: plan.bindCarrier,
|
|
3602
|
+
execute: plan.execute,
|
|
3603
|
+
inspectCompletion: plan.inspectCompletion,
|
|
3604
|
+
};
|
|
3605
|
+
const continuation = {
|
|
3606
|
+
source: plan.source,
|
|
3607
|
+
assertCurrent() {
|
|
3608
|
+
assertSourceCurrent();
|
|
3609
|
+
if (record.outcome?.status !== "released")
|
|
3610
|
+
throw new Error(
|
|
3611
|
+
"Selected command has no confirmed peer release.",
|
|
3612
|
+
);
|
|
3613
|
+
status!.recipient.call(status!.owner);
|
|
3614
|
+
},
|
|
3615
|
+
bindCarrier: status.ports.bindCarrier.bind(plan),
|
|
3616
|
+
async execute() {
|
|
3617
|
+
status!.result = "unknown";
|
|
3618
|
+
const completed = await status!.ports!.execute.call(plan);
|
|
3619
|
+
if (completed && record.current() && record.held.isOwnerCurrent())
|
|
3620
|
+
status!.result = "completed";
|
|
3621
|
+
return completed;
|
|
3622
|
+
},
|
|
3623
|
+
};
|
|
3624
|
+
if (
|
|
3625
|
+
!record.held.prepareStatusConsumption?.(continuation) ||
|
|
3626
|
+
!current()
|
|
3627
|
+
) {
|
|
3628
|
+
delete status.plan;
|
|
3629
|
+
delete status.ports;
|
|
3630
|
+
throw new Error("Selected command consumption is unconfirmed.");
|
|
3631
|
+
}
|
|
3632
|
+
record.observation.preparedSource = { ...plan.source };
|
|
3633
|
+
}
|
|
3634
|
+
return structuredClone(retained.observation);
|
|
3635
|
+
},
|
|
3636
|
+
async apply(
|
|
3637
|
+
envelope: Extract<
|
|
3638
|
+
TelegramBusEnvelope,
|
|
3639
|
+
{ kind: "leader.applyLiveRebind" }
|
|
3640
|
+
>,
|
|
3641
|
+
ctx: TContext,
|
|
3642
|
+
isCurrent: () => boolean,
|
|
3643
|
+
): Promise<TelegramBusLiveRebindApplyObservation> {
|
|
3644
|
+
const record = retained;
|
|
3645
|
+
if (envelope.selectedCommand !== undefined && !record?.status?.plan)
|
|
3646
|
+
throw new Error("Selected command native consumption is unavailable.");
|
|
3647
|
+
const current = () =>
|
|
3648
|
+
retained === record &&
|
|
3649
|
+
!!record &&
|
|
3650
|
+
record.ctx === ctx &&
|
|
3651
|
+
record.current() &&
|
|
3652
|
+
isCurrent() &&
|
|
3653
|
+
record.held.isCurrent() &&
|
|
3654
|
+
matchesLiveRebindBranch(record.observation, !!record.status, envelope);
|
|
3655
|
+
if (
|
|
3656
|
+
!record ||
|
|
3657
|
+
!current() ||
|
|
3658
|
+
!record.held.confirmSaved() ||
|
|
3659
|
+
!deps.applyTarget ||
|
|
3660
|
+
record.applying ||
|
|
3661
|
+
record.settling ||
|
|
3662
|
+
!matchesLiveRebindCarrier(record.observation, envelope)
|
|
3663
|
+
)
|
|
3664
|
+
throw new Error(
|
|
3665
|
+
"Live-rebind apply has no exact current saved carrier.",
|
|
3666
|
+
);
|
|
3667
|
+
const mode =
|
|
3668
|
+
envelope.mode === "apply" && !record.applyIssued ? "apply" : "inspect";
|
|
3669
|
+
if (mode === "apply") record.applyIssued = true;
|
|
3670
|
+
record.applying = true;
|
|
3671
|
+
try {
|
|
3672
|
+
const observation = await deps.applyTarget(
|
|
3673
|
+
{
|
|
3674
|
+
operationId: record.operationId,
|
|
3675
|
+
registrationGeneration: envelope.recipientRegistrationGeneration,
|
|
3676
|
+
mode,
|
|
3677
|
+
liveRebind: {
|
|
3678
|
+
sourceUpdateIds: record.observation.sourceUpdateIds,
|
|
3679
|
+
isCurrent: current,
|
|
3680
|
+
...copyLiveRebindCommandTarget(record.observation),
|
|
3681
|
+
},
|
|
3682
|
+
},
|
|
3683
|
+
ctx,
|
|
3684
|
+
);
|
|
3685
|
+
if (
|
|
3686
|
+
!current() ||
|
|
3687
|
+
!record.held.confirmSaved() ||
|
|
3688
|
+
observation.operationId !== record.operationId ||
|
|
3689
|
+
observation.recipient.instanceId !== envelope.recipientInstanceId ||
|
|
3690
|
+
observation.recipient.sessionId !== envelope.recipientSessionId ||
|
|
3691
|
+
observation.recipient.generation !==
|
|
3692
|
+
envelope.recipientRegistrationGeneration
|
|
3693
|
+
)
|
|
3694
|
+
throw new Error("Live-rebind apply observation changed.");
|
|
3695
|
+
return {
|
|
3696
|
+
...structuredClone(record.observation),
|
|
3697
|
+
target: { ...observation.target },
|
|
3698
|
+
slot: observation.slot,
|
|
3699
|
+
status: observation.ready ? "applied" : "saved",
|
|
3700
|
+
};
|
|
3701
|
+
} finally {
|
|
3702
|
+
delete record.applying;
|
|
3703
|
+
}
|
|
3704
|
+
},
|
|
3705
|
+
async settle(
|
|
3706
|
+
envelope: Extract<
|
|
3707
|
+
TelegramBusEnvelope,
|
|
3708
|
+
{ kind: "leader.settleLiveRebind" }
|
|
3709
|
+
>,
|
|
3710
|
+
ctx: TContext,
|
|
3711
|
+
isCurrent: () => boolean,
|
|
3712
|
+
): Promise<
|
|
3713
|
+
| TelegramBusLiveRebindSettlementObservation
|
|
3714
|
+
| TelegramBusLiveRebindWorkObservation
|
|
3715
|
+
| TelegramBusLiveRebindCommandObservation
|
|
3716
|
+
> {
|
|
3717
|
+
if (envelope.mode === "observe-command")
|
|
3718
|
+
return inspectCommandCompletion(envelope, ctx, isCurrent);
|
|
3719
|
+
const record = retained,
|
|
3720
|
+
status = record?.status,
|
|
3721
|
+
plan = status?.plan;
|
|
3722
|
+
if (envelope.selectedCommand !== undefined && !plan)
|
|
3723
|
+
throw new Error("Selected command native consumption is unavailable.");
|
|
3724
|
+
// Only the marked read-only held work sample may outlive the source worker, through the captured recipient owner.
|
|
3725
|
+
const commandWork =
|
|
3726
|
+
envelope.mode === "observe" && envelope.selectedCommand !== undefined;
|
|
3727
|
+
const recipientCurrent = () => {
|
|
3728
|
+
if (
|
|
3729
|
+
record?.status !== status ||
|
|
3730
|
+
status?.plan !== plan ||
|
|
3731
|
+
!status?.current()
|
|
3732
|
+
)
|
|
3733
|
+
return false;
|
|
3734
|
+
try {
|
|
3735
|
+
status.recipient.call(status.owner);
|
|
3736
|
+
return true;
|
|
3737
|
+
} catch {
|
|
3738
|
+
return false;
|
|
3739
|
+
}
|
|
3740
|
+
};
|
|
3741
|
+
const current = () =>
|
|
3742
|
+
retained === record &&
|
|
3743
|
+
!!record &&
|
|
3744
|
+
record.ctx === ctx &&
|
|
3745
|
+
record.current() &&
|
|
3746
|
+
isCurrent() &&
|
|
3747
|
+
(commandWork ? recipientCurrent() : record.held.isOwnerCurrent()) &&
|
|
3748
|
+
matchesLiveRebindBranch(record.observation, !!record.status, envelope);
|
|
3749
|
+
if (
|
|
3750
|
+
!record ||
|
|
3751
|
+
!current() ||
|
|
3752
|
+
record.applying ||
|
|
3753
|
+
record.settling ||
|
|
3754
|
+
!matchesLiveRebindCarrier(record.observation, envelope)
|
|
3755
|
+
)
|
|
3756
|
+
throw new Error("Live-rebind settlement has no exact current carrier.");
|
|
3757
|
+
if (envelope.mode === "observe") {
|
|
3758
|
+
if (
|
|
3759
|
+
record.outcome?.status !== "released" ||
|
|
3760
|
+
!deps.applyTarget ||
|
|
3761
|
+
!deps.observeWork ||
|
|
3762
|
+
!envelope.oldTarget
|
|
3763
|
+
)
|
|
3764
|
+
throw new Error(
|
|
3765
|
+
"Live-rebind work observation has no released carrier or observer.",
|
|
3766
|
+
);
|
|
3767
|
+
const oldTarget = { ...envelope.oldTarget };
|
|
3768
|
+
let work: TelegramBusLiveRebindWorkState | undefined;
|
|
3769
|
+
record.settling = true;
|
|
3770
|
+
try {
|
|
3771
|
+
await deps.applyTarget(
|
|
3772
|
+
{
|
|
3773
|
+
operationId: record.operationId,
|
|
3774
|
+
registrationGeneration: envelope.recipientRegistrationGeneration,
|
|
3775
|
+
mode: "inspect",
|
|
3776
|
+
liveRebind: {
|
|
3777
|
+
sourceUpdateIds: record.observation.sourceUpdateIds,
|
|
3778
|
+
isCurrent: current,
|
|
3779
|
+
...copyLiveRebindCommandTarget(record.observation),
|
|
3780
|
+
observeWork(canonicalOldTarget) {
|
|
3781
|
+
if (
|
|
3782
|
+
!current() ||
|
|
3783
|
+
!isDeepStrictEqual(canonicalOldTarget, oldTarget)
|
|
3784
|
+
)
|
|
3785
|
+
throw new Error("Live-rebind old work target changed.");
|
|
3786
|
+
work = structuredClone(
|
|
3787
|
+
deps.observeWork!(canonicalOldTarget, ctx),
|
|
3788
|
+
);
|
|
3789
|
+
if (
|
|
3790
|
+
!current() ||
|
|
3791
|
+
!isDeepStrictEqual(canonicalOldTarget, oldTarget)
|
|
3792
|
+
)
|
|
3793
|
+
throw new Error(
|
|
3794
|
+
"Live-rebind work observer authority or target changed.",
|
|
3795
|
+
);
|
|
3796
|
+
},
|
|
3797
|
+
},
|
|
3798
|
+
},
|
|
3799
|
+
ctx,
|
|
3800
|
+
);
|
|
3801
|
+
if (!current() || !work)
|
|
3802
|
+
throw new Error("Live-rebind work observation is unconfirmed.");
|
|
3803
|
+
// Unfinished or unconfirmed selected execution stays unknown; later settled delivery cannot clear it.
|
|
3804
|
+
if (commandWork && status!.result !== "completed")
|
|
3805
|
+
work.unknown = true;
|
|
3806
|
+
if (!current())
|
|
3807
|
+
throw new Error("Live-rebind work observer authority changed.");
|
|
3808
|
+
return {
|
|
3809
|
+
...structuredClone(record.observation),
|
|
3810
|
+
status: "observed",
|
|
3811
|
+
oldTarget,
|
|
3812
|
+
work,
|
|
3813
|
+
};
|
|
3814
|
+
} finally {
|
|
3815
|
+
delete record.settling;
|
|
3816
|
+
}
|
|
3817
|
+
}
|
|
3818
|
+
const action = envelope.mode;
|
|
3819
|
+
if (record.outcome) {
|
|
3820
|
+
if (record.outcome.action !== envelope.mode)
|
|
3821
|
+
throw new Error("Live-rebind input already has another disposition.");
|
|
3822
|
+
return structuredClone(record.outcome);
|
|
3823
|
+
}
|
|
3824
|
+
record.settling = true;
|
|
3825
|
+
const observation = (
|
|
3826
|
+
status: TelegramBusLiveRebindSettlementObservation["status"],
|
|
3827
|
+
): TelegramBusLiveRebindSettlementObservation => ({
|
|
3828
|
+
...structuredClone(record.observation),
|
|
3829
|
+
action,
|
|
3830
|
+
status,
|
|
3831
|
+
});
|
|
3832
|
+
try {
|
|
3833
|
+
if (envelope.mode === "discard") {
|
|
3834
|
+
record.outcome = observation("unknown");
|
|
3835
|
+
const status = record.held.discardSaved(current);
|
|
3836
|
+
record.outcome =
|
|
3837
|
+
status === "protected" ? undefined : observation(status);
|
|
3838
|
+
if (!current())
|
|
3839
|
+
throw new Error("Live-rebind disposal authority changed.");
|
|
3840
|
+
return observation(status);
|
|
3841
|
+
}
|
|
3842
|
+
if (
|
|
3843
|
+
!record.held.isCurrent() ||
|
|
3844
|
+
!record.held.confirmSaved() ||
|
|
3845
|
+
!deps.applyTarget
|
|
3846
|
+
)
|
|
3847
|
+
throw new Error("Live-rebind input cannot be released.");
|
|
3848
|
+
await deps.applyTarget(
|
|
3849
|
+
{
|
|
3850
|
+
operationId: record.operationId,
|
|
3851
|
+
registrationGeneration: envelope.recipientRegistrationGeneration,
|
|
3852
|
+
mode: "inspect",
|
|
3853
|
+
liveRebind: {
|
|
3854
|
+
sourceUpdateIds: record.observation.sourceUpdateIds,
|
|
3855
|
+
isCurrent: current,
|
|
3856
|
+
...copyLiveRebindCommandTarget(record.observation),
|
|
3857
|
+
release(canRelease) {
|
|
3858
|
+
record.outcome = observation("unknown");
|
|
3859
|
+
record.outcome = record.held.release(
|
|
3860
|
+
() => current() && canRelease(),
|
|
3861
|
+
)
|
|
3862
|
+
? observation("released")
|
|
3863
|
+
: undefined;
|
|
3864
|
+
},
|
|
3865
|
+
},
|
|
3866
|
+
},
|
|
3867
|
+
ctx,
|
|
3868
|
+
);
|
|
3869
|
+
if (!current())
|
|
3870
|
+
throw new Error("Live-rebind release authority changed.");
|
|
3871
|
+
return structuredClone(record.outcome ?? observation("protected"));
|
|
3872
|
+
} finally {
|
|
3873
|
+
delete record.settling;
|
|
3874
|
+
}
|
|
3875
|
+
},
|
|
3876
|
+
};
|
|
3877
|
+
async function inspectCommandCompletion(
|
|
3878
|
+
envelope: Extract<TelegramBusEnvelope, { kind: "leader.settleLiveRebind" }>,
|
|
3879
|
+
ctx: TContext,
|
|
3880
|
+
isCurrent: () => boolean,
|
|
3881
|
+
): Promise<TelegramBusLiveRebindCommandObservation> {
|
|
3882
|
+
const record = retained;
|
|
3883
|
+
const current = () =>
|
|
3884
|
+
retained === record &&
|
|
3885
|
+
!!record?.status?.plan &&
|
|
3886
|
+
record.ctx === ctx &&
|
|
3887
|
+
record.current() &&
|
|
3888
|
+
record.held.isOwnerCurrent() &&
|
|
3889
|
+
isCurrent() &&
|
|
3890
|
+
matchesLiveRebindBranch(record.observation, true, envelope);
|
|
3891
|
+
if (
|
|
3892
|
+
!record?.status?.plan ||
|
|
3893
|
+
!deps.applyTarget ||
|
|
3894
|
+
!current() ||
|
|
3895
|
+
record.outcome?.status !== "released" ||
|
|
3896
|
+
record.applying ||
|
|
3897
|
+
record.settling ||
|
|
3898
|
+
!matchesLiveRebindCarrier(record.observation, envelope)
|
|
3899
|
+
)
|
|
3900
|
+
throw new Error(
|
|
3901
|
+
"Selected command completion has no exact released carrier.",
|
|
3902
|
+
);
|
|
3903
|
+
record.settling = true;
|
|
3904
|
+
try {
|
|
3905
|
+
const assertRecipient = () => {
|
|
3906
|
+
record.status!.recipient.call(record.status!.owner);
|
|
3907
|
+
if (!current())
|
|
3908
|
+
throw new Error("Selected command completion recipient changed.");
|
|
3909
|
+
};
|
|
3910
|
+
assertRecipient();
|
|
3911
|
+
const observed = await deps.applyTarget(
|
|
3912
|
+
{
|
|
3913
|
+
operationId: record.operationId,
|
|
3914
|
+
registrationGeneration: record.observation.recipient.generation,
|
|
3915
|
+
mode: "inspect",
|
|
3916
|
+
liveRebind: {
|
|
3917
|
+
sourceUpdateIds: record.observation.sourceUpdateIds,
|
|
3918
|
+
expectedTarget: { ...record.observation.selectedCommand!.target },
|
|
3919
|
+
isCurrent: current,
|
|
3920
|
+
},
|
|
3921
|
+
},
|
|
3922
|
+
ctx,
|
|
3923
|
+
);
|
|
3924
|
+
if (!observed.ready || !current())
|
|
3925
|
+
throw new Error("Selected command completion target is unconfirmed.");
|
|
3926
|
+
assertRecipient();
|
|
3927
|
+
const sourceAck = record.status.ports!.inspectCompletion.call(
|
|
3928
|
+
record.status.plan,
|
|
3929
|
+
);
|
|
3930
|
+
assertRecipient();
|
|
3931
|
+
if (
|
|
3932
|
+
sourceAck &&
|
|
3933
|
+
!isDeepStrictEqual(sourceAck, record.observation.preparedSource)
|
|
3934
|
+
)
|
|
3935
|
+
throw new Error("Selected command completion exact ACK changed.");
|
|
3936
|
+
const settled = {
|
|
3937
|
+
preparedSource: { ...record.observation.preparedSource! },
|
|
3938
|
+
command: record.status.result ?? ("not-issued" as const),
|
|
3939
|
+
...(sourceAck ? { sourceAck: { ...sourceAck } } : {}),
|
|
3940
|
+
};
|
|
3941
|
+
const { selectedCommand } = record.observation;
|
|
3942
|
+
if (envelope.mode !== "observe-command" || !selectedCommand)
|
|
3943
|
+
throw new Error("Held completion mode does not match its marker.");
|
|
3944
|
+
return {
|
|
3945
|
+
...structuredClone(record.observation),
|
|
3946
|
+
status: "command-observed",
|
|
3947
|
+
selectedCommand: structuredClone(selectedCommand),
|
|
3948
|
+
...settled,
|
|
3949
|
+
};
|
|
3950
|
+
} finally {
|
|
3951
|
+
delete record.settling;
|
|
3952
|
+
}
|
|
3953
|
+
}
|
|
3954
|
+
return runtime;
|
|
3955
|
+
}
|
|
3956
|
+
|
|
3957
|
+
export function createTelegramBusFollowerDurableAdmissionRuntime<
|
|
3958
|
+
TContext,
|
|
3959
|
+
>(deps: {
|
|
2334
3960
|
journal: {
|
|
2335
3961
|
appendBatch(
|
|
2336
3962
|
updates: readonly ({ update_id: number } & Record<string, unknown>)[],
|
|
@@ -2344,10 +3970,17 @@ export function createTelegramBusFollowerDurableAdmissionRuntime<TContext>(deps:
|
|
|
2344
3970
|
// The journal forgets a delivery once its worker completes; this window absorbs a leader retry after a lost ACK.
|
|
2345
3971
|
const recentDeliveries = new Map<string, number>();
|
|
2346
3972
|
const getNowMs = deps.getNowMs ?? Date.now;
|
|
2347
|
-
const limit = deps.recentDeliveryLimit ?? {
|
|
3973
|
+
const limit = deps.recentDeliveryLimit ?? {
|
|
3974
|
+
maxAgeMs: 24 * 60 * 60 * 1000,
|
|
3975
|
+
maxEntries: 4096,
|
|
3976
|
+
};
|
|
2348
3977
|
const pruneRecentDeliveries = (nowMs: number): void => {
|
|
2349
3978
|
for (const [deliveryId, admittedAtMs] of recentDeliveries) {
|
|
2350
|
-
if (
|
|
3979
|
+
if (
|
|
3980
|
+
recentDeliveries.size <= limit.maxEntries &&
|
|
3981
|
+
nowMs - admittedAtMs < limit.maxAgeMs
|
|
3982
|
+
)
|
|
3983
|
+
break;
|
|
2351
3984
|
recentDeliveries.delete(deliveryId);
|
|
2352
3985
|
}
|
|
2353
3986
|
};
|
|
@@ -2355,11 +3988,14 @@ export function createTelegramBusFollowerDurableAdmissionRuntime<TContext>(deps:
|
|
|
2355
3988
|
async admit(envelope, ctx) {
|
|
2356
3989
|
const delivery = envelope.delivery;
|
|
2357
3990
|
if (!delivery) {
|
|
2358
|
-
throw new Error(
|
|
3991
|
+
throw new Error(
|
|
3992
|
+
"Telegram follower durable admission requires delivery identity.",
|
|
3993
|
+
);
|
|
2359
3994
|
}
|
|
2360
|
-
if (envelope.kind === "leader.wakeInputCustody")
|
|
2361
|
-
|
|
2362
|
-
|
|
3995
|
+
if (envelope.kind === "leader.wakeInputCustody")
|
|
3996
|
+
throw new Error(
|
|
3997
|
+
"Telegram follower custody wake cannot use executable-copy admission.",
|
|
3998
|
+
);
|
|
2363
3999
|
const expected = createTelegramBusFollowerDeliveryIdentity({
|
|
2364
4000
|
kind: envelope.kind,
|
|
2365
4001
|
recipientBindingKey: delivery.recipientBindingKey,
|
|
@@ -2378,7 +4014,9 @@ export function createTelegramBusFollowerDurableAdmissionRuntime<TContext>(deps:
|
|
|
2378
4014
|
!isRecord(carrier) ||
|
|
2379
4015
|
carrier.pi_telegram_source_update_id !== delivery.sourceUpdateId
|
|
2380
4016
|
) {
|
|
2381
|
-
throw new Error(
|
|
4017
|
+
throw new Error(
|
|
4018
|
+
"Telegram follower delivery source update id mismatch.",
|
|
4019
|
+
);
|
|
2382
4020
|
}
|
|
2383
4021
|
const update = {
|
|
2384
4022
|
update_id: delivery.sourceUpdateId,
|
|
@@ -2413,89 +4051,154 @@ export function createTelegramBusFollowerDurableAdmissionRuntime<TContext>(deps:
|
|
|
2413
4051
|
}
|
|
2414
4052
|
|
|
2415
4053
|
export interface TelegramBusFollowerInputCustodyBundle<TContext> {
|
|
2416
|
-
acceptHandoff(
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
4054
|
+
acceptHandoff(
|
|
4055
|
+
input: {
|
|
4056
|
+
sourceRecoveryKey: string;
|
|
4057
|
+
recipientBindingKey: string;
|
|
4058
|
+
source: {
|
|
4059
|
+
journalBindingKey: string;
|
|
4060
|
+
tokenSha256: string;
|
|
4061
|
+
updateId: number;
|
|
4062
|
+
};
|
|
4063
|
+
handoffId: string;
|
|
4064
|
+
},
|
|
4065
|
+
ctx: TContext,
|
|
4066
|
+
): unknown;
|
|
4067
|
+
wakeSource(
|
|
4068
|
+
input: TelegramBusFollowerDurableAdmissionResult & {
|
|
4069
|
+
recipientBindingKey: string;
|
|
4070
|
+
sourceRecoveryKey: string;
|
|
4071
|
+
sourceClaim: { acquisitionId: string; handoffId: string };
|
|
4072
|
+
},
|
|
4073
|
+
ctx: TContext,
|
|
4074
|
+
): void;
|
|
2427
4075
|
resolveForwardReference(input: {
|
|
2428
|
-
sourceUpdateId: number;
|
|
2429
|
-
|
|
2430
|
-
|
|
4076
|
+
sourceUpdateId: number;
|
|
4077
|
+
recipientBindingKey: string;
|
|
4078
|
+
}):
|
|
4079
|
+
| {
|
|
4080
|
+
sourceRecoveryKey: string;
|
|
4081
|
+
source: {
|
|
4082
|
+
updateId: number;
|
|
4083
|
+
owner: {
|
|
4084
|
+
acquisitionId: string;
|
|
4085
|
+
handoffId: string;
|
|
4086
|
+
};
|
|
4087
|
+
};
|
|
4088
|
+
}
|
|
4089
|
+
| undefined;
|
|
2431
4090
|
}
|
|
2432
4091
|
|
|
2433
4092
|
export function createTelegramBusFollowerInputCustodyPorts<TContext>(deps: {
|
|
2434
|
-
getInputCustodyBus():
|
|
4093
|
+
getInputCustodyBus():
|
|
4094
|
+
TelegramBusFollowerInputCustodyBundle<TContext> | undefined;
|
|
2435
4095
|
}) {
|
|
2436
4096
|
const getRequired = (): TelegramBusFollowerInputCustodyBundle<TContext> => {
|
|
2437
4097
|
const bundle = deps.getInputCustodyBus();
|
|
2438
|
-
if (!bundle)
|
|
4098
|
+
if (!bundle)
|
|
4099
|
+
throw new Error("Telegram input custody bus binding is unavailable.");
|
|
2439
4100
|
return bundle;
|
|
2440
4101
|
};
|
|
2441
4102
|
return {
|
|
2442
4103
|
isSourceReferenceAdmissionEnabled: () => Boolean(deps.getInputCustodyBus()),
|
|
2443
4104
|
handleInputCustodyHandoff(
|
|
2444
|
-
envelope: Extract<
|
|
4105
|
+
envelope: Extract<
|
|
4106
|
+
TelegramBusEnvelope,
|
|
4107
|
+
{ kind: "leader.offerInputCustodyHandoff" }
|
|
4108
|
+
>,
|
|
2445
4109
|
ctx: TContext,
|
|
2446
4110
|
) {
|
|
2447
|
-
return getRequired().acceptHandoff(
|
|
2448
|
-
|
|
2449
|
-
|
|
4111
|
+
return getRequired().acceptHandoff(
|
|
4112
|
+
{
|
|
4113
|
+
sourceRecoveryKey: envelope.sourceRecoveryKey,
|
|
4114
|
+
recipientBindingKey: envelope.recipientBindingKey,
|
|
4115
|
+
source: envelope.source,
|
|
4116
|
+
handoffId: envelope.handoffId,
|
|
4117
|
+
},
|
|
4118
|
+
ctx,
|
|
4119
|
+
);
|
|
2450
4120
|
},
|
|
2451
|
-
sourceReferenceAdmission:
|
|
2452
|
-
|
|
2453
|
-
|
|
4121
|
+
sourceReferenceAdmission:
|
|
4122
|
+
createTelegramBusFollowerSourceReferenceAdmissionRuntime<TContext>({
|
|
4123
|
+
wakeSource(input, ctx) {
|
|
4124
|
+
getRequired().wakeSource(input, ctx);
|
|
4125
|
+
},
|
|
4126
|
+
}),
|
|
2454
4127
|
resolveInputCustodyReference(input: {
|
|
2455
|
-
sourceUpdateId: number;
|
|
4128
|
+
sourceUpdateId: number;
|
|
4129
|
+
recipientBindingKey: string;
|
|
2456
4130
|
}) {
|
|
2457
4131
|
return deps.getInputCustodyBus()?.resolveForwardReference(input);
|
|
2458
4132
|
},
|
|
2459
4133
|
};
|
|
2460
4134
|
}
|
|
2461
4135
|
|
|
2462
|
-
export function createTelegramBusFollowerSourceReferenceAdmissionRuntime<
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
4136
|
+
export function createTelegramBusFollowerSourceReferenceAdmissionRuntime<
|
|
4137
|
+
TContext,
|
|
4138
|
+
>(deps: {
|
|
4139
|
+
wakeSource: (
|
|
4140
|
+
input: TelegramBusFollowerDurableAdmissionResult & {
|
|
4141
|
+
recipientBindingKey: string;
|
|
4142
|
+
sourceRecoveryKey: string;
|
|
4143
|
+
sourceClaim: { acquisitionId: string; handoffId: string };
|
|
4144
|
+
},
|
|
4145
|
+
ctx: TContext,
|
|
4146
|
+
) => Promise<void> | void;
|
|
2468
4147
|
}): TelegramBusFollowerDurableAdmissionPort<TContext> {
|
|
2469
4148
|
return {
|
|
2470
4149
|
async admit(envelope, ctx) {
|
|
2471
4150
|
const delivery = envelope.delivery;
|
|
2472
|
-
if (!delivery)
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
)
|
|
2481
|
-
|
|
4151
|
+
if (!delivery)
|
|
4152
|
+
throw new Error(
|
|
4153
|
+
"Telegram follower source-reference admission requires delivery identity.",
|
|
4154
|
+
);
|
|
4155
|
+
if (!delivery.sourceRecoveryKey)
|
|
4156
|
+
throw new Error(
|
|
4157
|
+
"Telegram follower source-reference admission requires a recovery key.",
|
|
4158
|
+
);
|
|
4159
|
+
if (!delivery.sourceClaim)
|
|
4160
|
+
throw new Error(
|
|
4161
|
+
"Telegram follower source-reference admission requires exact claim evidence.",
|
|
4162
|
+
);
|
|
4163
|
+
const expected = createTelegramBusFollowerDeliveryIdentity({
|
|
4164
|
+
kind: envelope.kind,
|
|
2482
4165
|
recipientBindingKey: delivery.recipientBindingKey,
|
|
2483
|
-
sourceUpdateId: delivery.sourceUpdateId
|
|
2484
|
-
|
|
2485
|
-
|
|
4166
|
+
sourceUpdateId: delivery.sourceUpdateId,
|
|
4167
|
+
});
|
|
4168
|
+
if (expected.deliveryId !== delivery.deliveryId)
|
|
4169
|
+
throw new Error(
|
|
4170
|
+
"Invalid Telegram follower source-reference delivery id.",
|
|
4171
|
+
);
|
|
4172
|
+
const carrier =
|
|
4173
|
+
envelope.kind === "leader.wakeInputCustody"
|
|
4174
|
+
? undefined
|
|
4175
|
+
: envelope.kind === "leader.forwardCallback"
|
|
4176
|
+
? envelope.query
|
|
4177
|
+
: envelope.kind === "leader.forwardReaction"
|
|
4178
|
+
? envelope.reactionUpdate
|
|
4179
|
+
: envelope.message;
|
|
4180
|
+
if (
|
|
4181
|
+
envelope.kind !== "leader.wakeInputCustody" &&
|
|
4182
|
+
(!isRecord(carrier) ||
|
|
4183
|
+
carrier.pi_telegram_source_update_id !== delivery.sourceUpdateId)
|
|
4184
|
+
)
|
|
4185
|
+
throw new Error(
|
|
4186
|
+
"Telegram follower source-reference update id mismatch.",
|
|
4187
|
+
);
|
|
4188
|
+
await deps.wakeSource(
|
|
4189
|
+
{
|
|
4190
|
+
deliveryId: delivery.deliveryId,
|
|
4191
|
+
sourceUpdateId: delivery.sourceUpdateId,
|
|
4192
|
+
recipientBindingKey: delivery.recipientBindingKey,
|
|
4193
|
+
sourceRecoveryKey: delivery.sourceRecoveryKey,
|
|
4194
|
+
sourceClaim: { ...delivery.sourceClaim },
|
|
4195
|
+
},
|
|
4196
|
+
ctx,
|
|
2486
4197
|
);
|
|
2487
|
-
|
|
2488
|
-
:
|
|
2489
|
-
: envelope.kind === "leader.forwardReaction" ? envelope.reactionUpdate : envelope.message;
|
|
2490
|
-
if (envelope.kind !== "leader.wakeInputCustody" &&
|
|
2491
|
-
(!isRecord(carrier) || carrier.pi_telegram_source_update_id !== delivery.sourceUpdateId))
|
|
2492
|
-
throw new Error("Telegram follower source-reference update id mismatch.");
|
|
2493
|
-
await deps.wakeSource({ deliveryId: delivery.deliveryId,
|
|
4198
|
+
return {
|
|
4199
|
+
deliveryId: delivery.deliveryId,
|
|
2494
4200
|
sourceUpdateId: delivery.sourceUpdateId,
|
|
2495
|
-
|
|
2496
|
-
sourceRecoveryKey: delivery.sourceRecoveryKey,
|
|
2497
|
-
sourceClaim: { ...delivery.sourceClaim } }, ctx);
|
|
2498
|
-
return { deliveryId: delivery.deliveryId, sourceUpdateId: delivery.sourceUpdateId };
|
|
4201
|
+
};
|
|
2499
4202
|
},
|
|
2500
4203
|
};
|
|
2501
4204
|
}
|
|
@@ -2527,50 +4230,184 @@ export function createTelegramBusForwardedUpdateReceiverRuntime<TContext>(
|
|
|
2527
4230
|
envelope.kind !== "leader.wakeInputCustody" &&
|
|
2528
4231
|
envelope.kind !== "leader.offerInputCustodyHandoff" &&
|
|
2529
4232
|
envelope.kind !== "leader.workspaceRestore" &&
|
|
4233
|
+
envelope.kind !== "leader.prepareLiveRebind" &&
|
|
4234
|
+
envelope.kind !== "leader.applyLiveRebind" &&
|
|
4235
|
+
envelope.kind !== "leader.settleLiveRebind" &&
|
|
2530
4236
|
envelope.kind !== "leader.offerQueueHandoff") ||
|
|
2531
4237
|
envelope.recipientInstanceId !== deps.instanceId
|
|
2532
4238
|
) {
|
|
2533
|
-
return
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
message: "Telegram bus receiver cannot handle this envelope.",
|
|
2538
|
-
};
|
|
4239
|
+
return rejectTelegramBusRequest(
|
|
4240
|
+
envelope.requestId,
|
|
4241
|
+
"Telegram bus receiver cannot handle this envelope.",
|
|
4242
|
+
);
|
|
2539
4243
|
}
|
|
2540
4244
|
const registrationGeneration = deps.getRegistrationGeneration();
|
|
2541
4245
|
if (
|
|
2542
4246
|
!registrationGeneration ||
|
|
2543
4247
|
envelope.recipientRegistrationGeneration !== registrationGeneration
|
|
2544
4248
|
) {
|
|
2545
|
-
return
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
4249
|
+
return rejectTelegramBusRequest(
|
|
4250
|
+
envelope.requestId,
|
|
4251
|
+
"Stale Telegram bus follower registration generation.",
|
|
4252
|
+
);
|
|
4253
|
+
}
|
|
4254
|
+
if (
|
|
4255
|
+
envelope.kind === "leader.prepareLiveRebind" ||
|
|
4256
|
+
envelope.kind === "leader.applyLiveRebind" ||
|
|
4257
|
+
envelope.kind === "leader.settleLiveRebind"
|
|
4258
|
+
) {
|
|
4259
|
+
const apply = envelope.kind !== "leader.prepareLiveRebind",
|
|
4260
|
+
settle = envelope.kind === "leader.settleLiveRebind";
|
|
4261
|
+
const commandInput = envelope.selectedCommand,
|
|
4262
|
+
commandEnabled = deps.isLiveRebindCommandSetEnabled;
|
|
4263
|
+
const required = [
|
|
4264
|
+
TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SAVE,
|
|
4265
|
+
...(apply ? [TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_APPLY] : []),
|
|
4266
|
+
...(settle ? [TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SETTLE] : []),
|
|
4267
|
+
...(commandInput
|
|
4268
|
+
? [
|
|
4269
|
+
TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_APPLY,
|
|
4270
|
+
TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SETTLE,
|
|
4271
|
+
TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_COMMAND_SET,
|
|
4272
|
+
TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY,
|
|
4273
|
+
]
|
|
4274
|
+
: []),
|
|
4275
|
+
];
|
|
4276
|
+
const ctx = deps.getContext(),
|
|
4277
|
+
sessionId = deps.getSessionId?.(),
|
|
4278
|
+
bindingKey = deps.getLiveRebindJournalBindingKey?.();
|
|
4279
|
+
const protocol = structuredClone(deps.getLeaderProtocol?.()),
|
|
4280
|
+
localProtocol = structuredClone(deps.getLocalProtocol?.());
|
|
4281
|
+
const current = () =>
|
|
4282
|
+
!!authSecret &&
|
|
4283
|
+
deps.getAuthSecret?.() === authSecret &&
|
|
4284
|
+
deps.getContext() === ctx &&
|
|
4285
|
+
deps.getRegistrationGeneration() === registrationGeneration &&
|
|
4286
|
+
deps.getSessionId?.() === sessionId &&
|
|
4287
|
+
deps.getLiveRebindJournalBindingKey?.() === bindingKey &&
|
|
4288
|
+
deps.isLiveRebindSaveEnabled?.() === true &&
|
|
4289
|
+
(!apply || deps.isLiveRebindApplyEnabled?.() === true) &&
|
|
4290
|
+
(!settle || deps.isLiveRebindSettleEnabled?.() === true) &&
|
|
4291
|
+
(!commandInput ||
|
|
4292
|
+
(typeof commandEnabled === "function" &&
|
|
4293
|
+
deps.isLiveRebindCommandSetEnabled === commandEnabled &&
|
|
4294
|
+
commandEnabled())) &&
|
|
4295
|
+
isDeepStrictEqual(deps.getLeaderProtocol?.(), protocol) &&
|
|
4296
|
+
isDeepStrictEqual(deps.getLocalProtocol?.(), localProtocol);
|
|
4297
|
+
if (
|
|
4298
|
+
!authSecret ||
|
|
4299
|
+
!ctx ||
|
|
4300
|
+
!bindingKey ||
|
|
4301
|
+
sessionId !== envelope.recipientSessionId ||
|
|
4302
|
+
bindingKey !== envelope.recipientBindingKey ||
|
|
4303
|
+
!required.every(
|
|
4304
|
+
(cap) =>
|
|
4305
|
+
hasTelegramBusCapability(protocol, cap) &&
|
|
4306
|
+
hasTelegramBusCapability(localProtocol, cap),
|
|
4307
|
+
) ||
|
|
4308
|
+
!localProtocol ||
|
|
4309
|
+
!getTelegramBusProtocolCompatibility({
|
|
4310
|
+
local: localProtocol,
|
|
4311
|
+
remote: protocol,
|
|
4312
|
+
}).compatible ||
|
|
4313
|
+
!current() ||
|
|
4314
|
+
(settle
|
|
4315
|
+
? !deps.handleLiveRebindSettle
|
|
4316
|
+
: apply
|
|
4317
|
+
? !deps.handleLiveRebindApply
|
|
4318
|
+
: !deps.handleLiveRebindSave)
|
|
4319
|
+
)
|
|
4320
|
+
return rejectTelegramBusRequest(
|
|
4321
|
+
envelope.requestId,
|
|
4322
|
+
"Live-rebind capability or recipient authority is unavailable.",
|
|
4323
|
+
);
|
|
4324
|
+
try {
|
|
4325
|
+
const result =
|
|
4326
|
+
envelope.kind === "leader.settleLiveRebind"
|
|
4327
|
+
? await deps.handleLiveRebindSettle!(envelope, ctx, current)
|
|
4328
|
+
: envelope.kind === "leader.applyLiveRebind"
|
|
4329
|
+
? await deps.handleLiveRebindApply!(envelope, ctx, current)
|
|
4330
|
+
: await deps.handleLiveRebindSave!(envelope, ctx, current);
|
|
4331
|
+
if (!current())
|
|
4332
|
+
throw new Error("Live-rebind receiver authority changed.");
|
|
4333
|
+
return {
|
|
4334
|
+
kind: "bus.ack",
|
|
4335
|
+
requestId: envelope.requestId,
|
|
4336
|
+
ok: true,
|
|
4337
|
+
result,
|
|
4338
|
+
};
|
|
4339
|
+
} catch (error) {
|
|
4340
|
+
deps.recordRuntimeEvent?.("bus", error, {
|
|
4341
|
+
phase: "follower-live-rebind-control",
|
|
4342
|
+
});
|
|
4343
|
+
return {
|
|
4344
|
+
kind: "bus.ack",
|
|
4345
|
+
requestId: envelope.requestId,
|
|
4346
|
+
ok: false,
|
|
4347
|
+
message:
|
|
4348
|
+
error instanceof Error
|
|
4349
|
+
? error.message
|
|
4350
|
+
: "Live-rebind request failed.",
|
|
4351
|
+
};
|
|
4352
|
+
}
|
|
2551
4353
|
}
|
|
2552
4354
|
if (envelope.kind === "leader.workspaceRestore") {
|
|
2553
4355
|
const ctx = deps.getContext();
|
|
2554
|
-
if (
|
|
2555
|
-
|
|
2556
|
-
|
|
4356
|
+
if (
|
|
4357
|
+
!authSecret ||
|
|
4358
|
+
!ctx ||
|
|
4359
|
+
!deps.isWorkspaceRestoreEnabled?.() ||
|
|
4360
|
+
!deps.handleWorkspaceRestore
|
|
4361
|
+
)
|
|
4362
|
+
return rejectTelegramBusRequest(
|
|
4363
|
+
envelope.requestId,
|
|
4364
|
+
"Workspace Restore capability is unavailable.",
|
|
4365
|
+
);
|
|
2557
4366
|
try {
|
|
2558
|
-
const result = await deps.handleWorkspaceRestore(
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
4367
|
+
const result = await deps.handleWorkspaceRestore(
|
|
4368
|
+
{
|
|
4369
|
+
operationId: envelope.operationId,
|
|
4370
|
+
registrationGeneration,
|
|
4371
|
+
mode: envelope.mode,
|
|
4372
|
+
},
|
|
4373
|
+
ctx,
|
|
4374
|
+
);
|
|
4375
|
+
if (
|
|
4376
|
+
deps.getAuthSecret?.() !== authSecret ||
|
|
4377
|
+
deps.getRegistrationGeneration() !== registrationGeneration ||
|
|
4378
|
+
deps.getContext() !== ctx ||
|
|
4379
|
+
!deps.isWorkspaceRestoreEnabled()
|
|
4380
|
+
)
|
|
4381
|
+
throw new Error("Workspace Restore receiver authority changed.");
|
|
4382
|
+
return {
|
|
4383
|
+
kind: "bus.ack",
|
|
4384
|
+
requestId: envelope.requestId,
|
|
4385
|
+
ok: true,
|
|
4386
|
+
result,
|
|
4387
|
+
};
|
|
2563
4388
|
} catch (error) {
|
|
2564
|
-
deps.recordRuntimeEvent?.("bus", error, {
|
|
2565
|
-
|
|
2566
|
-
|
|
4389
|
+
deps.recordRuntimeEvent?.("bus", error, {
|
|
4390
|
+
phase: "follower-workspace-restore",
|
|
4391
|
+
});
|
|
4392
|
+
return {
|
|
4393
|
+
kind: "bus.ack",
|
|
4394
|
+
requestId: envelope.requestId,
|
|
4395
|
+
ok: false,
|
|
4396
|
+
message:
|
|
4397
|
+
error instanceof Error
|
|
4398
|
+
? error.message
|
|
4399
|
+
: "Workspace Restore failed.",
|
|
4400
|
+
};
|
|
2567
4401
|
}
|
|
2568
4402
|
}
|
|
2569
|
-
if (
|
|
2570
|
-
envelope.
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
4403
|
+
if (
|
|
4404
|
+
envelope.kind === "leader.offerInputCustodyHandoff" &&
|
|
4405
|
+
envelope.recipientBindingKey !== deps.getRecipientBindingKey()
|
|
4406
|
+
)
|
|
4407
|
+
return rejectTelegramBusRequest(
|
|
4408
|
+
envelope.requestId,
|
|
4409
|
+
"Mismatched Telegram follower handoff recipient identity.",
|
|
4410
|
+
);
|
|
2574
4411
|
if (
|
|
2575
4412
|
envelope.kind !== "leader.offerQueueHandoff" &&
|
|
2576
4413
|
envelope.kind !== "leader.offerInputCustodyHandoff" &&
|
|
@@ -2578,48 +4415,58 @@ export function createTelegramBusForwardedUpdateReceiverRuntime<TContext>(
|
|
|
2578
4415
|
envelope.delivery.recipientBindingKey !==
|
|
2579
4416
|
deps.getRecipientBindingKey())
|
|
2580
4417
|
) {
|
|
2581
|
-
return
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
message: "Mismatched Telegram follower delivery identity.",
|
|
2586
|
-
};
|
|
4418
|
+
return rejectTelegramBusRequest(
|
|
4419
|
+
envelope.requestId,
|
|
4420
|
+
"Mismatched Telegram follower delivery identity.",
|
|
4421
|
+
);
|
|
2587
4422
|
}
|
|
2588
4423
|
const ctx = deps.getContext();
|
|
2589
4424
|
if (!ctx) {
|
|
2590
|
-
return
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
message: "Telegram bus follower has no active context.",
|
|
2595
|
-
};
|
|
4425
|
+
return rejectTelegramBusRequest(
|
|
4426
|
+
envelope.requestId,
|
|
4427
|
+
"Telegram bus follower has no active context.",
|
|
4428
|
+
);
|
|
2596
4429
|
}
|
|
2597
4430
|
try {
|
|
2598
4431
|
if (envelope.kind === "leader.offerInputCustodyHandoff") {
|
|
2599
|
-
if (!(deps.isSourceReferenceAdmissionEnabled?.() ?? false))
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
)
|
|
4432
|
+
if (!(deps.isSourceReferenceAdmissionEnabled?.() ?? false))
|
|
4433
|
+
throw new Error(
|
|
4434
|
+
"Telegram input custody handoff capability is not enabled.",
|
|
4435
|
+
);
|
|
4436
|
+
if (!deps.hasAuthenticatedSourceReferenceTransport?.())
|
|
4437
|
+
throw new Error(
|
|
4438
|
+
"Telegram input custody handoff requires authenticated transport.",
|
|
4439
|
+
);
|
|
4440
|
+
if (!deps.handleInputCustodyHandoff)
|
|
4441
|
+
throw new Error(
|
|
4442
|
+
"Telegram input custody handoff acceptance is unavailable.",
|
|
4443
|
+
);
|
|
2608
4444
|
const result = await deps.handleInputCustodyHandoff(envelope, ctx);
|
|
2609
|
-
return {
|
|
4445
|
+
return {
|
|
4446
|
+
kind: "bus.ack",
|
|
4447
|
+
requestId: envelope.requestId,
|
|
4448
|
+
ok: true,
|
|
4449
|
+
result,
|
|
4450
|
+
};
|
|
2610
4451
|
}
|
|
2611
4452
|
if (envelope.kind !== "leader.offerQueueHandoff") {
|
|
2612
|
-
const sourceReferenceEnabled =
|
|
4453
|
+
const sourceReferenceEnabled =
|
|
4454
|
+
envelope.kind === "leader.wakeInputCustody" ||
|
|
2613
4455
|
(deps.isSourceReferenceAdmissionEnabled?.() ?? false);
|
|
2614
|
-
if (
|
|
2615
|
-
|
|
4456
|
+
if (
|
|
4457
|
+
sourceReferenceEnabled &&
|
|
4458
|
+
!deps.hasAuthenticatedSourceReferenceTransport?.()
|
|
4459
|
+
)
|
|
4460
|
+
throw new Error(
|
|
2616
4461
|
"Telegram follower source-reference admission requires authenticated transport.",
|
|
2617
4462
|
);
|
|
2618
4463
|
const admission = sourceReferenceEnabled
|
|
2619
|
-
? deps.sourceReferenceAdmission
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
4464
|
+
? deps.sourceReferenceAdmission
|
|
4465
|
+
: deps.durableAdmission;
|
|
4466
|
+
if (!admission)
|
|
4467
|
+
throw new Error(
|
|
4468
|
+
"Telegram follower source-reference admission is enabled without a wake authority.",
|
|
4469
|
+
);
|
|
2623
4470
|
const receipt = await admission.admit(envelope, ctx);
|
|
2624
4471
|
return {
|
|
2625
4472
|
kind: "bus.ack",
|
|
@@ -2691,11 +4538,3 @@ function parseRegistrationResult(value: unknown): {
|
|
|
2691
4538
|
: {}),
|
|
2692
4539
|
};
|
|
2693
4540
|
}
|
|
2694
|
-
|
|
2695
|
-
function parseTarget(value: unknown): TelegramTarget | undefined {
|
|
2696
|
-
if (value === undefined) return undefined;
|
|
2697
|
-
if (!isRecord(value) || typeof value.chatId !== "number") return undefined;
|
|
2698
|
-
const target: TelegramTarget = { chatId: value.chatId };
|
|
2699
|
-
if (typeof value.threadId === "number") target.threadId = value.threadId;
|
|
2700
|
-
return target;
|
|
2701
|
-
}
|