@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/dist/lib/bus-leader.js
CHANGED
|
@@ -5,25 +5,21 @@
|
|
|
5
5
|
* leader activation hot-switching, local bus server startup, and stale follower pruning.
|
|
6
6
|
*/
|
|
7
7
|
import { isDeepStrictEqual } from "node:util";
|
|
8
|
+
import { getTelegramBusTransportRetryPolicy } from "./bus-transport.js";
|
|
9
|
+
import { createTelegramBusLocalServer, createUnauthorizedBusAck, rejectTelegramBusRequest, getTelegramBusEnvelopeTrafficClass, getTelegramBusFollowerSocketPath, getTelegramBusProtocolCompatibility, hasTelegramBusCapability, isTelegramBusEnvelopeAuthorized, parseTelegramBusEnvelope, sendTelegramBusLocalEnvelope, stripTelegramBusApiMetadata, TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT, TELEGRAM_BUS_CAPABILITY_DURABLE_FOLLOWER_ADMISSION, TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF, TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY, TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT, TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE, TELEGRAM_BUS_CAPABILITY_WORKSPACE_FOLLOWER_AUTO_CONNECT, TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE, TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME, } from "./bus.js";
|
|
10
|
+
import { escapeHtml } from "./rendering.js";
|
|
8
11
|
import * as Sync from "./sync.js";
|
|
12
|
+
import { parseTelegramIntegerId as asInteger, } from "./target.js";
|
|
13
|
+
import { getTelegramApiErrorRequestTarget, isTelegramApiCommitUnknownError, TelegramApiAuthorityError, } from "./telegram-api.js";
|
|
9
14
|
import { createTelegramThreadDisplayReconciler, resolveTelegramInitialWorkspaceDisplayName, resolveTelegramLiveWorkspaceBindingKeys, } from "./thread-display.js";
|
|
10
15
|
import * as ThreadNaming from "./thread-naming.js";
|
|
11
16
|
import * as ThreadReconciler from "./thread-reconciler.js";
|
|
12
|
-
import { getTelegramApiErrorRequestTarget, isTelegramApiCommitUnknownError, } from "./telegram-api.js";
|
|
13
17
|
import * as Threads from "./threads.js";
|
|
18
|
+
import { createTelegramWorkspaceAdmissionOperationId, runWithTelegramWorkspaceAdmissionsAsync, } from "./workspace-admission.js";
|
|
14
19
|
import * as WorkspaceIdentity from "./workspace-identity.js";
|
|
15
|
-
import { createTelegramBusLocalServer, createUnauthorizedBusAck, getTelegramBusEnvelopeTrafficClass, getTelegramBusProtocolCompatibility, hasTelegramBusCapability, isTelegramBusEnvelopeAuthorized, getTelegramBusFollowerSocketPath, sendTelegramBusLocalEnvelope, stripTelegramBusApiMetadata, TELEGRAM_BUS_CAPABILITY_DURABLE_FOLLOWER_ADMISSION, TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF, TELEGRAM_BUS_CAPABILITY_WORKSPACE_FOLLOWER_AUTO_CONNECT, TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE, TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME, TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE, TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT, TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT, } from "./bus.js";
|
|
16
|
-
import { getTelegramBusTransportRetryPolicy } from "./bus-transport.js";
|
|
17
20
|
import { createTelegramWorkspaceOperationRuntime, createTelegramWorkspaceSlotRotation, } from "./workspace-retirement.js";
|
|
18
|
-
import {
|
|
19
|
-
import { TELEGRAM_WORKSPACE_SLOTS, TelegramWorkspaceSlotUnavailableError } from "./workspace-slots.js";
|
|
21
|
+
import { TELEGRAM_WORKSPACE_SLOTS, TelegramWorkspaceSlotUnavailableError, } from "./workspace-slots.js";
|
|
20
22
|
export const TELEGRAM_BUS_FOLLOWER_STALE_AFTER_MS = 15_000;
|
|
21
|
-
function escapeHtml(text) {
|
|
22
|
-
return text
|
|
23
|
-
.replace(/&/g, "&")
|
|
24
|
-
.replace(/</g, "<")
|
|
25
|
-
.replace(/>/g, ">");
|
|
26
|
-
}
|
|
27
23
|
function formatTelegramBusInstanceLabel(input) {
|
|
28
24
|
const threadName = input.threadName?.trim();
|
|
29
25
|
if (threadName)
|
|
@@ -52,7 +48,9 @@ export function createTelegramBusFollowerSessionReplacementAuthority(deps) {
|
|
|
52
48
|
typeof intent.target.threadId !== "number" ||
|
|
53
49
|
follower.target?.chatId !== intent.target.chatId ||
|
|
54
50
|
follower.target.threadId !== intent.target.threadId ||
|
|
55
|
-
!cwd ||
|
|
51
|
+
!cwd ||
|
|
52
|
+
cwd !== intent.cwd ||
|
|
53
|
+
!sessionId ||
|
|
56
54
|
intent.profileName !== (deps.getTelegramProfile?.() ?? "default")) {
|
|
57
55
|
throw new Error("Telegram session replacement does not match the current follower registration.");
|
|
58
56
|
}
|
|
@@ -72,7 +70,8 @@ export function createTelegramBusFollowerSessionReplacementAuthority(deps) {
|
|
|
72
70
|
if (!isCurrent())
|
|
73
71
|
return false;
|
|
74
72
|
const binding = deps.store.getWorkspaceBindingByTarget(intent.target);
|
|
75
|
-
if (!binding ||
|
|
73
|
+
if (!binding ||
|
|
74
|
+
binding.cwd !== intent.cwd ||
|
|
76
75
|
binding.sessionId !== intent.sourceSessionId ||
|
|
77
76
|
binding.slot !== intent.slot ||
|
|
78
77
|
(binding.manualThreadName ?? binding.threadName) !== intent.threadName) {
|
|
@@ -91,8 +90,8 @@ export function createTelegramBusFollowerSessionReplacementAuthority(deps) {
|
|
|
91
90
|
await deps.store.load();
|
|
92
91
|
if (!isCurrent())
|
|
93
92
|
return false;
|
|
94
|
-
if (deps.store.getWorkspaceBindingByTarget(intent.target, sessionId)
|
|
95
|
-
intent.cwd) {
|
|
93
|
+
if (deps.store.getWorkspaceBindingByTarget(intent.target, sessionId)
|
|
94
|
+
?.cwd !== intent.cwd) {
|
|
96
95
|
throw new Error("Telegram session replacement successor binding is unavailable.");
|
|
97
96
|
}
|
|
98
97
|
return deps.store.removeSessionReplacementIntent(intent, isCurrent);
|
|
@@ -129,13 +128,19 @@ export function createTelegramBusLeaderRuntimeAssembly(deps) {
|
|
|
129
128
|
});
|
|
130
129
|
},
|
|
131
130
|
}).run;
|
|
132
|
-
const runWithWorkspaceCapacity = deps.workspaceRotation &&
|
|
133
|
-
deps.
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
131
|
+
const runWithWorkspaceCapacity = deps.workspaceRotation &&
|
|
132
|
+
deps.captureWorkspaceExternalProtection &&
|
|
133
|
+
deps.getCurrentLeaderEpoch
|
|
134
|
+
? createTelegramWorkspaceSlotRotation({
|
|
135
|
+
...deps.workspaceRotation,
|
|
136
|
+
store: deps.topicTargetStore,
|
|
137
|
+
getLeaderEpoch: deps.getCurrentLeaderEpoch,
|
|
138
|
+
getExternalProtection: deps.captureWorkspaceExternalProtection,
|
|
139
|
+
recordEvent(message, details) {
|
|
140
|
+
deps.recordRuntimeEvent("bus", message, details);
|
|
141
|
+
},
|
|
142
|
+
})
|
|
143
|
+
: undefined;
|
|
139
144
|
const captureLiveBindingKeys = (bindings) => resolveTelegramLiveWorkspaceBindingKeys(bindings, deps.topicTargetStore.getActiveByInstanceId(deps.instanceId)?.target, deps.runtime.followerRegistry.list());
|
|
140
145
|
const provisionerPorts = {
|
|
141
146
|
getAllowedUserId: deps.getAllowedUserId,
|
|
@@ -161,29 +166,34 @@ export function createTelegramBusLeaderRuntimeAssembly(deps) {
|
|
|
161
166
|
runWorkspaceOperation,
|
|
162
167
|
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
163
168
|
};
|
|
164
|
-
const display = deps.getThreadDisplayMode
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
169
|
+
const display = deps.getThreadDisplayMode
|
|
170
|
+
? createTelegramThreadDisplayReconciler({
|
|
171
|
+
store: deps.topicTargetStore,
|
|
172
|
+
getMode: deps.getThreadDisplayMode,
|
|
173
|
+
getProfileKey: () => deps.getTelegramProfile?.() ?? "default",
|
|
174
|
+
getLeaderEpoch: () => deps.getCurrentLeaderEpoch?.(),
|
|
175
|
+
captureLiveBindingKeys,
|
|
176
|
+
captureBindingAuthority(binding) {
|
|
177
|
+
const matches = (target) => target?.chatId === binding.target.chatId &&
|
|
178
|
+
target?.threadId === binding.target.threadId;
|
|
179
|
+
const leader = deps.topicTargetStore.getActiveByInstanceId(deps.instanceId);
|
|
180
|
+
if (leader && matches(leader.target)) {
|
|
181
|
+
return () => matches(deps.topicTargetStore.getActiveByInstanceId(deps.instanceId)
|
|
182
|
+
?.target);
|
|
183
|
+
}
|
|
184
|
+
const follower = deps.runtime.followerRegistry.getByTarget(binding.target);
|
|
185
|
+
if (!follower?.registrationGeneration)
|
|
186
|
+
return undefined;
|
|
187
|
+
const generation = follower.registrationGeneration;
|
|
188
|
+
return () => {
|
|
189
|
+
const current = deps.runtime.followerRegistry.get(follower.instanceId);
|
|
190
|
+
return (current?.registrationGeneration === generation &&
|
|
191
|
+
matches(current.target));
|
|
192
|
+
};
|
|
193
|
+
},
|
|
194
|
+
callApi: deps.callApi,
|
|
195
|
+
})
|
|
196
|
+
: undefined;
|
|
187
197
|
const reconcileThreadDisplayOperation = async () => {
|
|
188
198
|
const result = await display.reconcile();
|
|
189
199
|
deps.onThreadDisplayChanged?.();
|
|
@@ -208,7 +218,9 @@ export function createTelegramBusLeaderRuntimeAssembly(deps) {
|
|
|
208
218
|
if (!display || !startupDisplaySettled)
|
|
209
219
|
return;
|
|
210
220
|
void reconcileThreadDisplay().catch((error) => {
|
|
211
|
-
deps.recordRuntimeEvent("bus", error, {
|
|
221
|
+
deps.recordRuntimeEvent("bus", error, {
|
|
222
|
+
phase: "thread-display-reconcile",
|
|
223
|
+
});
|
|
212
224
|
});
|
|
213
225
|
};
|
|
214
226
|
const beginStartupDisplayStabilization = () => {
|
|
@@ -217,8 +229,7 @@ export function createTelegramBusLeaderRuntimeAssembly(deps) {
|
|
|
217
229
|
};
|
|
218
230
|
const armStartupDisplayReconciliation = () => {
|
|
219
231
|
const generation = startupDisplayGeneration;
|
|
220
|
-
const delayMs = deps.runtime.followerStaleAfterMs ??
|
|
221
|
-
TELEGRAM_BUS_FOLLOWER_STALE_AFTER_MS;
|
|
232
|
+
const delayMs = deps.runtime.followerStaleAfterMs ?? TELEGRAM_BUS_FOLLOWER_STALE_AFTER_MS;
|
|
222
233
|
startupDisplayTimer = setTimeout(() => {
|
|
223
234
|
if (startupDisplayGeneration !== generation)
|
|
224
235
|
return;
|
|
@@ -232,8 +243,10 @@ export function createTelegramBusLeaderRuntimeAssembly(deps) {
|
|
|
232
243
|
const applyThreadDisplayMode = (mode, isCurrent) => {
|
|
233
244
|
const epoch = deps.getCurrentLeaderEpoch?.();
|
|
234
245
|
const profile = deps.getTelegramProfile?.();
|
|
235
|
-
const current = () => epoch !== undefined &&
|
|
236
|
-
deps.
|
|
246
|
+
const current = () => epoch !== undefined &&
|
|
247
|
+
deps.getCurrentLeaderEpoch?.() === epoch &&
|
|
248
|
+
deps.getTelegramProfile?.() === profile &&
|
|
249
|
+
isCurrent();
|
|
237
250
|
const run = modeTail.then(() => runWorkspaceOperation({
|
|
238
251
|
operationId: createTelegramWorkspaceAdmissionOperationId(),
|
|
239
252
|
operationKind: "workspace.set-display-mode",
|
|
@@ -246,7 +259,10 @@ export function createTelegramBusLeaderRuntimeAssembly(deps) {
|
|
|
246
259
|
const requiredCapability = mode === "directory-snake" || mode === "directory-title"
|
|
247
260
|
? TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT
|
|
248
261
|
: TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE;
|
|
249
|
-
if (mode !== "names" &&
|
|
262
|
+
if (mode !== "names" &&
|
|
263
|
+
deps.runtime.followerRegistry
|
|
264
|
+
.list()
|
|
265
|
+
.some((follower) => !hasTelegramBusCapability(follower.protocol, requiredCapability)))
|
|
250
266
|
throw new Error("Update or restart all connected followers before changing Thread display mode.");
|
|
251
267
|
};
|
|
252
268
|
assertDisplayPeers();
|
|
@@ -284,121 +300,286 @@ export function createTelegramBusLeaderRuntimeAssembly(deps) {
|
|
|
284
300
|
const provisionFollowerTarget = createTelegramBusFollowerTargetProvisioner({
|
|
285
301
|
...provisionerPorts,
|
|
286
302
|
});
|
|
287
|
-
const renameLeaderThreadAdmitted = async (threadName, expectedTarget) => {
|
|
288
|
-
const
|
|
289
|
-
|
|
303
|
+
const renameLeaderThreadAdmitted = async (threadName, expectedTarget, assertRecipientAuthority) => {
|
|
304
|
+
const store = deps.topicTargetStore, callApi = deps.callApi;
|
|
305
|
+
const publishName = store.renameByTargetAndPersist.bind(store);
|
|
306
|
+
const persist = store.persist.bind(store);
|
|
307
|
+
const instanceId = deps.instanceId;
|
|
308
|
+
const getProfile = deps.getTelegramProfile, getUser = deps.getAllowedUserId;
|
|
309
|
+
const getEpoch = deps.getCurrentLeaderEpoch;
|
|
310
|
+
const profile = getProfile?.(), user = getUser();
|
|
311
|
+
const recipientAuthority = assertRecipientAuthority;
|
|
312
|
+
recipientAuthority?.();
|
|
313
|
+
const leaderEpoch = getEpoch?.();
|
|
314
|
+
if (getEpoch && leaderEpoch === undefined) {
|
|
290
315
|
throw new Error("Telegram Workspace Thread rename requires leader ownership.");
|
|
291
316
|
}
|
|
292
317
|
const record = Threads.findCurrentTelegramInstanceThreadRecord({
|
|
293
|
-
records:
|
|
294
|
-
instanceId
|
|
318
|
+
records: store.list(),
|
|
319
|
+
instanceId,
|
|
295
320
|
});
|
|
296
321
|
if (typeof record?.target.threadId !== "number") {
|
|
297
322
|
throw new Error("No Workspace Thread is bound to this Pi instance.");
|
|
298
323
|
}
|
|
299
|
-
if (expectedTarget &&
|
|
300
|
-
record.target.
|
|
324
|
+
if (expectedTarget &&
|
|
325
|
+
(record.target.chatId !== expectedTarget.chatId ||
|
|
326
|
+
record.target.threadId !== expectedTarget.threadId)) {
|
|
301
327
|
throw new Error("Telegram Workspace Thread rename target changed.");
|
|
302
328
|
}
|
|
329
|
+
const target = { ...record.target }, slot = record.slot, owner = structuredClone(record.owner);
|
|
330
|
+
const bindingIdentity = (binding) => ({
|
|
331
|
+
bindingKey: binding.bindingKey,
|
|
332
|
+
cwd: binding.cwd,
|
|
333
|
+
workspaceKey: binding.workspaceKey,
|
|
334
|
+
sessionId: binding.sessionId,
|
|
335
|
+
sessionKey: binding.sessionKey,
|
|
336
|
+
instanceSlot: binding.instanceSlot,
|
|
337
|
+
slot: binding.slot,
|
|
338
|
+
target: { ...binding.target },
|
|
339
|
+
});
|
|
340
|
+
const bindings = store
|
|
341
|
+
.listWorkspaceBindings()
|
|
342
|
+
.filter((binding) => binding.target.chatId === target.chatId &&
|
|
343
|
+
binding.target.threadId === target.threadId);
|
|
344
|
+
if (recipientAuthority &&
|
|
345
|
+
(bindings.length !== 1 ||
|
|
346
|
+
!bindings[0]?.sessionId ||
|
|
347
|
+
bindings[0].slot !== slot))
|
|
348
|
+
throw new Error("Telegram Workspace Thread rename recipient binding authority is unavailable.");
|
|
349
|
+
const capturedBindings = bindings.map(bindingIdentity);
|
|
350
|
+
const assertAuthority = () => {
|
|
351
|
+
recipientAuthority?.();
|
|
352
|
+
if ((getEpoch && getEpoch() !== leaderEpoch) ||
|
|
353
|
+
getProfile?.() !== profile ||
|
|
354
|
+
getUser() !== user ||
|
|
355
|
+
deps.topicTargetStore !== store ||
|
|
356
|
+
deps.instanceId !== instanceId)
|
|
357
|
+
throw new Error("Telegram Workspace Thread rename lost leader ownership.");
|
|
358
|
+
const current = Threads.findCurrentTelegramInstanceThreadRecord({
|
|
359
|
+
records: store.list(),
|
|
360
|
+
instanceId,
|
|
361
|
+
});
|
|
362
|
+
const currentBindings = store
|
|
363
|
+
.listWorkspaceBindings()
|
|
364
|
+
.filter((binding) => binding.target.chatId === target.chatId &&
|
|
365
|
+
binding.target.threadId === target.threadId)
|
|
366
|
+
.map(bindingIdentity);
|
|
367
|
+
if (current?.target.chatId !== target.chatId ||
|
|
368
|
+
current.target.threadId !== target.threadId ||
|
|
369
|
+
current.slot !== slot ||
|
|
370
|
+
current.status !== record.status ||
|
|
371
|
+
!isDeepStrictEqual(current.owner, owner) ||
|
|
372
|
+
!isDeepStrictEqual(currentBindings, capturedBindings))
|
|
373
|
+
throw new Error("Telegram Workspace Thread rename target changed or binding authority was lost.");
|
|
374
|
+
};
|
|
375
|
+
assertAuthority();
|
|
376
|
+
const current = () => {
|
|
377
|
+
try {
|
|
378
|
+
assertAuthority();
|
|
379
|
+
return true;
|
|
380
|
+
}
|
|
381
|
+
catch {
|
|
382
|
+
return false;
|
|
383
|
+
}
|
|
384
|
+
};
|
|
303
385
|
const rename = Threads.createTelegramTopicTargetRenamer({
|
|
304
|
-
store:
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
}
|
|
311
|
-
const current = Threads.findCurrentTelegramInstanceThreadRecord({
|
|
312
|
-
records: deps.topicTargetStore.list(),
|
|
313
|
-
instanceId: deps.instanceId,
|
|
314
|
-
});
|
|
315
|
-
if (current?.target.chatId !== record.target.chatId ||
|
|
316
|
-
current.target.threadId !== record.target.threadId) {
|
|
317
|
-
throw new Error("Telegram Workspace Thread rename target changed.");
|
|
386
|
+
store: recipientAuthority
|
|
387
|
+
? {
|
|
388
|
+
list: store.list.bind(store),
|
|
389
|
+
listWorkspaceBindings: store.listWorkspaceBindings.bind(store),
|
|
390
|
+
listPendingProvisions: store.listPendingProvisions.bind(store),
|
|
391
|
+
renameByTarget: (target, name, options) => publishName(target, name, { updateDisplayTitle: options?.updateDisplayTitle ?? true }, current),
|
|
318
392
|
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
target: {
|
|
323
|
-
chatId: record.target.chatId,
|
|
324
|
-
threadId: record.target.threadId,
|
|
325
|
-
},
|
|
326
|
-
threadName,
|
|
327
|
-
slot: record.slot,
|
|
393
|
+
: store,
|
|
394
|
+
callApi,
|
|
395
|
+
assertAuthority,
|
|
328
396
|
});
|
|
397
|
+
const renamed = await rename({ target, threadName, slot });
|
|
398
|
+
assertAuthority();
|
|
329
399
|
if (!renamed) {
|
|
330
|
-
throw new Error(ThreadNaming.getTelegramTopicThreadNameValidationError(threadName,
|
|
400
|
+
throw new Error(ThreadNaming.getTelegramTopicThreadNameValidationError(threadName, slot) ?? "Telegram Workspace Thread name is already reserved.");
|
|
331
401
|
}
|
|
332
|
-
|
|
402
|
+
if (!recipientAuthority)
|
|
403
|
+
await persist(current);
|
|
404
|
+
assertAuthority();
|
|
333
405
|
return renamed;
|
|
334
406
|
};
|
|
335
|
-
const
|
|
336
|
-
const
|
|
337
|
-
|
|
407
|
+
const prepareThreadNameReset = (expectedTarget, assertTargetCurrent, assertRecipientAuthority) => {
|
|
408
|
+
const target = { ...expectedTarget }, store = deps.topicTargetStore, callApi = deps.callApi;
|
|
409
|
+
const publishReset = store.clearManualNameByTargetAndPersist.bind(store);
|
|
410
|
+
const clearManualName = store.clearManualNameByTarget.bind(store), persist = store.persist.bind(store);
|
|
411
|
+
const getEpoch = deps.getCurrentLeaderEpoch, getProfile = deps.getTelegramProfile;
|
|
412
|
+
const getUser = deps.getAllowedUserId, getMode = deps.getThreadDisplayMode;
|
|
413
|
+
const recipientAuthority = assertRecipientAuthority, targetAuthority = assertTargetCurrent;
|
|
414
|
+
recipientAuthority?.();
|
|
415
|
+
const leaderEpoch = getEpoch?.(), profile = getProfile?.(), user = getUser(), instanceId = deps.instanceId;
|
|
416
|
+
const mode = getMode?.() ?? "letters";
|
|
417
|
+
if (getEpoch && leaderEpoch === undefined) {
|
|
338
418
|
throw new Error("Telegram Workspace Thread reset requires leader ownership.");
|
|
339
419
|
}
|
|
340
|
-
const
|
|
420
|
+
const matchingBindings = () => store
|
|
421
|
+
.listWorkspaceBindings()
|
|
422
|
+
.filter((candidate) => candidate.target.chatId === target.chatId &&
|
|
341
423
|
candidate.target.threadId === target.threadId);
|
|
424
|
+
const bindings = matchingBindings(), binding = bindings[0];
|
|
342
425
|
if (!binding)
|
|
343
426
|
throw new Error("No Workspace Thread binding is available to reset.");
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
427
|
+
const record = Threads.findCurrentTelegramInstanceThreadRecord({
|
|
428
|
+
records: store.list(),
|
|
429
|
+
instanceId,
|
|
430
|
+
});
|
|
431
|
+
if (recipientAuthority &&
|
|
432
|
+
(bindings.length !== 1 ||
|
|
433
|
+
!binding.sessionId ||
|
|
434
|
+
record?.status !== "active" ||
|
|
435
|
+
record.owner?.kind !== "leader" ||
|
|
436
|
+
record.owner.cwd !== binding.cwd ||
|
|
437
|
+
record.target.chatId !== target.chatId ||
|
|
438
|
+
record.target.threadId !== target.threadId ||
|
|
439
|
+
record.slot !== binding.slot))
|
|
440
|
+
throw new Error("Telegram Workspace Thread reset recipient binding authority is unavailable.");
|
|
441
|
+
const bindingIdentity = (value) => ({
|
|
442
|
+
bindingKey: value.bindingKey,
|
|
443
|
+
cwd: value.cwd,
|
|
444
|
+
workspaceKey: value.workspaceKey,
|
|
445
|
+
sessionId: value.sessionId,
|
|
446
|
+
sessionKey: value.sessionKey,
|
|
447
|
+
instanceSlot: value.instanceSlot,
|
|
448
|
+
slot: value.slot,
|
|
449
|
+
target: { ...value.target },
|
|
450
|
+
});
|
|
451
|
+
const capturedBinding = bindingIdentity(binding), owner = record && structuredClone(record.owner);
|
|
452
|
+
const resolveTitle = (value) => resolveTelegramInitialWorkspaceDisplayName({
|
|
453
|
+
bindings: store.listWorkspaceBindings(),
|
|
454
|
+
binding: { ...value, manualThreadName: undefined },
|
|
455
|
+
mode,
|
|
348
456
|
preserveRetainedManualName: false,
|
|
349
457
|
});
|
|
350
|
-
|
|
458
|
+
const automaticTitle = resolveTitle(binding);
|
|
459
|
+
if (!automaticTitle)
|
|
351
460
|
throw new Error("Telegram Workspace automatic title is unavailable.");
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
461
|
+
const assertAuthority = () => {
|
|
462
|
+
recipientAuthority?.();
|
|
463
|
+
if (getEpoch && getEpoch() !== leaderEpoch)
|
|
464
|
+
throw new Error("Telegram Workspace Thread reset lost leader ownership.");
|
|
465
|
+
targetAuthority();
|
|
466
|
+
if (!recipientAuthority)
|
|
467
|
+
return;
|
|
468
|
+
if (getProfile?.() !== profile ||
|
|
469
|
+
getUser() !== user ||
|
|
470
|
+
deps.topicTargetStore !== store ||
|
|
471
|
+
deps.instanceId !== instanceId)
|
|
472
|
+
throw new Error("Telegram Workspace Thread reset lost recipient authority.");
|
|
473
|
+
const currentRecord = Threads.findCurrentTelegramInstanceThreadRecord({
|
|
474
|
+
records: store.list(),
|
|
475
|
+
instanceId,
|
|
476
|
+
});
|
|
477
|
+
const currentBindings = matchingBindings();
|
|
478
|
+
if (currentRecord?.target.chatId !== target.chatId ||
|
|
479
|
+
currentRecord.target.threadId !== target.threadId ||
|
|
480
|
+
currentRecord.slot !== record.slot ||
|
|
481
|
+
currentRecord.status !== record.status ||
|
|
482
|
+
!isDeepStrictEqual(currentRecord.owner, owner) ||
|
|
483
|
+
currentBindings.length !== 1 ||
|
|
484
|
+
!isDeepStrictEqual(bindingIdentity(currentBindings[0]), capturedBinding))
|
|
485
|
+
throw new Error("Telegram Workspace Thread reset target changed or binding authority was lost.");
|
|
486
|
+
if ((getMode?.() ?? "letters") !== mode ||
|
|
487
|
+
resolveTitle(currentBindings[0]) !== automaticTitle)
|
|
488
|
+
throw new Error("Telegram Workspace automatic-title policy changed.");
|
|
489
|
+
};
|
|
490
|
+
assertAuthority();
|
|
491
|
+
const current = () => {
|
|
492
|
+
try {
|
|
493
|
+
assertAuthority();
|
|
494
|
+
return true;
|
|
495
|
+
}
|
|
496
|
+
catch {
|
|
497
|
+
return false;
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
return async () => {
|
|
501
|
+
assertAuthority();
|
|
502
|
+
const body = {
|
|
503
|
+
chat_id: target.chatId,
|
|
504
|
+
message_thread_id: target.threadId,
|
|
505
|
+
name: automaticTitle,
|
|
506
|
+
};
|
|
507
|
+
if (recipientAuthority)
|
|
508
|
+
await callApi("editForumTopic", body, { assertAuthority });
|
|
509
|
+
else
|
|
510
|
+
await callApi("editForumTopic", body);
|
|
511
|
+
assertAuthority();
|
|
512
|
+
if (recipientAuthority &&
|
|
513
|
+
(!isDeepStrictEqual(matchingBindings()[0], binding) ||
|
|
514
|
+
!isDeepStrictEqual(Threads.findCurrentTelegramInstanceThreadRecord({
|
|
515
|
+
records: store.list(),
|
|
516
|
+
instanceId,
|
|
517
|
+
}), record)))
|
|
518
|
+
throw new Error("Telegram Workspace Thread reset metadata changed before publication.");
|
|
519
|
+
const reset = recipientAuthority
|
|
520
|
+
? await publishReset(target, automaticTitle, current)
|
|
521
|
+
: clearManualName(target, automaticTitle);
|
|
522
|
+
if (!reset)
|
|
523
|
+
throw new Error("Telegram Workspace Thread reset changed binding.");
|
|
524
|
+
if (!recipientAuthority)
|
|
525
|
+
await persist();
|
|
526
|
+
assertAuthority();
|
|
527
|
+
if (recipientAuthority &&
|
|
528
|
+
(reset.manualThreadName !== undefined ||
|
|
529
|
+
matchingBindings()[0]?.manualThreadName !== undefined ||
|
|
530
|
+
matchingBindings()[0]?.displayTitle !== automaticTitle ||
|
|
531
|
+
store.getActiveByInstanceId(instanceId)?.manualThreadName !==
|
|
532
|
+
undefined))
|
|
533
|
+
throw new Error("Telegram Workspace Thread reset result metadata changed.");
|
|
534
|
+
return { threadName: automaticTitle };
|
|
535
|
+
};
|
|
369
536
|
};
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
537
|
+
const resetThreadNameAdmitted = async (target, assertTargetCurrent = () => undefined) => prepareThreadNameReset(target, assertTargetCurrent)();
|
|
538
|
+
const renameLeaderThread = async (threadName) => {
|
|
539
|
+
const getEpoch = deps.getCurrentLeaderEpoch, getProfile = deps.getTelegramProfile;
|
|
540
|
+
const getUser = deps.getAllowedUserId, store = deps.topicTargetStore;
|
|
541
|
+
const epoch = getEpoch?.(), profile = getProfile?.(), user = getUser();
|
|
542
|
+
return runWorkspaceOperation({
|
|
543
|
+
operationId: createTelegramWorkspaceAdmissionOperationId(),
|
|
544
|
+
operationKind: "workspace.rename-leader",
|
|
545
|
+
scopes: [{ kind: "profile" }],
|
|
546
|
+
}, () => {
|
|
547
|
+
if (getEpoch?.() !== epoch ||
|
|
548
|
+
getProfile?.() !== profile ||
|
|
549
|
+
getUser() !== user ||
|
|
550
|
+
deps.topicTargetStore !== store)
|
|
551
|
+
throw new Error("Telegram Workspace Thread rename lost leader ownership during admission.");
|
|
552
|
+
return renameLeaderThreadAdmitted(threadName);
|
|
383
553
|
});
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
record
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
return resetThreadNameAdmitted(record.target, () => {
|
|
392
|
-
const current = Threads.findCurrentTelegramInstanceThreadRecord({
|
|
393
|
-
records: deps.topicTargetStore.list(),
|
|
394
|
-
instanceId: deps.instanceId,
|
|
554
|
+
};
|
|
555
|
+
const resetLeaderThreadName = async (expectedTarget, assertRecipientAuthority) => {
|
|
556
|
+
const target = { ...expectedTarget }, store = deps.topicTargetStore, instanceId = deps.instanceId;
|
|
557
|
+
const assertTargetCurrent = () => {
|
|
558
|
+
const record = Threads.findCurrentTelegramInstanceThreadRecord({
|
|
559
|
+
records: store.list(),
|
|
560
|
+
instanceId,
|
|
395
561
|
});
|
|
396
|
-
if (
|
|
397
|
-
|
|
562
|
+
if (!record || typeof record.target.threadId !== "number")
|
|
563
|
+
throw new Error("No Workspace Thread is bound to this Pi instance.");
|
|
564
|
+
if (record.target.chatId !== target.chatId ||
|
|
565
|
+
record.target.threadId !== target.threadId)
|
|
398
566
|
throw new Error("Telegram Workspace Thread reset target changed.");
|
|
399
|
-
|
|
567
|
+
};
|
|
568
|
+
// Selected lifetimes are captured before admission; ordinary reset keeps its post-admission preparation.
|
|
569
|
+
const prepared = assertRecipientAuthority
|
|
570
|
+
? prepareThreadNameReset(target, assertTargetCurrent, assertRecipientAuthority)
|
|
571
|
+
: undefined;
|
|
572
|
+
return runWorkspaceOperation({
|
|
573
|
+
operationId: createTelegramWorkspaceAdmissionOperationId(),
|
|
574
|
+
operationKind: "workspace.reset-leader-name",
|
|
575
|
+
scopes: [{ kind: "profile" }],
|
|
576
|
+
}, () => {
|
|
577
|
+
assertTargetCurrent();
|
|
578
|
+
return prepared
|
|
579
|
+
? prepared()
|
|
580
|
+
: resetThreadNameAdmitted(target, assertTargetCurrent);
|
|
400
581
|
});
|
|
401
|
-
}
|
|
582
|
+
};
|
|
402
583
|
const followerSessionReplacement = createTelegramBusFollowerSessionReplacementAuthority({
|
|
403
584
|
store: deps.topicTargetStore,
|
|
404
585
|
getTelegramProfile: deps.getTelegramProfile,
|
|
@@ -420,10 +601,14 @@ export function createTelegramBusLeaderRuntimeAssembly(deps) {
|
|
|
420
601
|
: { kind: "profile" },
|
|
421
602
|
],
|
|
422
603
|
}, () => provisionLeaderTarget(ctx));
|
|
423
|
-
return runWithWorkspaceCapacity
|
|
604
|
+
return runWithWorkspaceCapacity
|
|
605
|
+
? runWithWorkspaceCapacity(provision)
|
|
606
|
+
: provision();
|
|
424
607
|
},
|
|
425
608
|
getFollowerDisplayTitle(follower) {
|
|
426
|
-
const binding = deps.topicTargetStore
|
|
609
|
+
const binding = deps.topicTargetStore
|
|
610
|
+
.listWorkspaceBindings()
|
|
611
|
+
.find((binding) => binding.target.chatId === follower.target?.chatId &&
|
|
427
612
|
binding.target.threadId === follower.target?.threadId);
|
|
428
613
|
return binding?.displayTitle ?? binding?.threadName;
|
|
429
614
|
},
|
|
@@ -457,7 +642,8 @@ export function createTelegramBusLeaderRuntimeAssembly(deps) {
|
|
|
457
642
|
throw new Error("Telegram Workspace Thread rename lost leader ownership.");
|
|
458
643
|
}
|
|
459
644
|
const current = deps.runtime.followerRegistry.get(follower.instanceId);
|
|
460
|
-
if (current?.registrationGeneration !==
|
|
645
|
+
if (current?.registrationGeneration !==
|
|
646
|
+
follower.registrationGeneration ||
|
|
461
647
|
current?.target?.chatId !== follower.target?.chatId ||
|
|
462
648
|
current?.target?.threadId !== follower.target?.threadId) {
|
|
463
649
|
throw new Error("Telegram follower Workspace Thread target changed.");
|
|
@@ -488,7 +674,8 @@ export function createTelegramBusLeaderRuntimeAssembly(deps) {
|
|
|
488
674
|
operationKind: "workspace.reset-follower-name",
|
|
489
675
|
scopes: [{ kind: "profile" }],
|
|
490
676
|
}, () => {
|
|
491
|
-
if (!follower.target ||
|
|
677
|
+
if (!follower.target ||
|
|
678
|
+
typeof follower.target.threadId !== "number") {
|
|
492
679
|
throw new Error("Telegram follower has no bound Workspace Thread.");
|
|
493
680
|
}
|
|
494
681
|
const target = {
|
|
@@ -497,7 +684,8 @@ export function createTelegramBusLeaderRuntimeAssembly(deps) {
|
|
|
497
684
|
};
|
|
498
685
|
return resetThreadNameAdmitted(target, () => {
|
|
499
686
|
const current = deps.runtime.followerRegistry.get(follower.instanceId);
|
|
500
|
-
if (current?.registrationGeneration !==
|
|
687
|
+
if (current?.registrationGeneration !==
|
|
688
|
+
follower.registrationGeneration ||
|
|
501
689
|
current?.target?.chatId !== target.chatId ||
|
|
502
690
|
current?.target?.threadId !== target.threadId) {
|
|
503
691
|
throw new Error("Telegram follower Workspace Thread target changed.");
|
|
@@ -525,10 +713,12 @@ export function createTelegramBusLeaderRuntimeAssembly(deps) {
|
|
|
525
713
|
async onFollowerConfirmedDeadPreserved(follower, isDetached, operationId) {
|
|
526
714
|
const epoch = deps.getCurrentLeaderEpoch?.();
|
|
527
715
|
const profile = deps.getTelegramProfile?.();
|
|
528
|
-
if (epoch === undefined ||
|
|
716
|
+
if (epoch === undefined ||
|
|
717
|
+
!follower.registrationGeneration ||
|
|
529
718
|
typeof follower.target?.threadId !== "number")
|
|
530
719
|
return false;
|
|
531
|
-
const isCurrent = () => isDetached() &&
|
|
720
|
+
const isCurrent = () => isDetached() &&
|
|
721
|
+
deps.getCurrentLeaderEpoch?.() === epoch &&
|
|
532
722
|
deps.getTelegramProfile?.() === profile;
|
|
533
723
|
if (!isCurrent())
|
|
534
724
|
return false;
|
|
@@ -543,7 +733,9 @@ export function createTelegramBusLeaderRuntimeAssembly(deps) {
|
|
|
543
733
|
if (!isCurrent())
|
|
544
734
|
return false;
|
|
545
735
|
const record = deps.topicTargetStore.getActiveByInstanceId(follower.instanceId);
|
|
546
|
-
if (!record ||
|
|
736
|
+
if (!record ||
|
|
737
|
+
(follower.profileKey &&
|
|
738
|
+
record.profileKey !== follower.profileKey) ||
|
|
547
739
|
record.target.chatId !== follower.target.chatId ||
|
|
548
740
|
record.target.threadId !== follower.target.threadId)
|
|
549
741
|
return true;
|
|
@@ -564,7 +756,8 @@ export function createTelegramBusLeaderRuntimeAssembly(deps) {
|
|
|
564
756
|
}, () => provisionFollowerTarget(registration, options)),
|
|
565
757
|
commitFollowerRegistration: (input, publish) => runWorkspaceOperation({
|
|
566
758
|
operationId: createTelegramWorkspaceAdmissionOperationId(),
|
|
567
|
-
operationKind: "workspace.publish-follower-registration",
|
|
759
|
+
operationKind: "workspace.publish-follower-registration",
|
|
760
|
+
scopes: [{ kind: "profile" }],
|
|
568
761
|
}, async () => {
|
|
569
762
|
if (!input.target) {
|
|
570
763
|
publish();
|
|
@@ -572,10 +765,18 @@ export function createTelegramBusLeaderRuntimeAssembly(deps) {
|
|
|
572
765
|
}
|
|
573
766
|
await deps.topicTargetStore.load();
|
|
574
767
|
const binding = input.registration.cwd && input.registration.sessionId
|
|
575
|
-
? deps.topicTargetStore.getWorkspaceBindingByTarget(input.target, input.registration.sessionId)
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
768
|
+
? deps.topicTargetStore.getWorkspaceBindingByTarget(input.target, input.registration.sessionId)
|
|
769
|
+
: undefined;
|
|
770
|
+
const bindingKey = binding &&
|
|
771
|
+
input.registration.cwd &&
|
|
772
|
+
binding.cwd ===
|
|
773
|
+
WorkspaceIdentity.normalizeTelegramWorkspacePath(input.registration.cwd)
|
|
774
|
+
? binding.bindingKey
|
|
775
|
+
: undefined;
|
|
776
|
+
if (input.target.threadId !== undefined &&
|
|
777
|
+
input.registration.cwd &&
|
|
778
|
+
input.registration.sessionId &&
|
|
779
|
+
!bindingKey)
|
|
579
780
|
throw new Error("Telegram follower Workspace binding changed before registration publication.");
|
|
580
781
|
deps.topicTargetStore.commitWorkspaceRestoreRegistration({ target: input.target, slot: input.slot, bindingKey }, publish);
|
|
581
782
|
}),
|
|
@@ -723,7 +924,8 @@ export function createTelegramBusFollowerTargetProvisioner(deps) {
|
|
|
723
924
|
matchesRequestedTarget;
|
|
724
925
|
return (record.owner?.kind === "manual-follower" &&
|
|
725
926
|
((matchesCurrentIdentity && matchesRequestedTarget) ||
|
|
726
|
-
matchesSessionHandoff ||
|
|
927
|
+
matchesSessionHandoff ||
|
|
928
|
+
matchesWorkspaceRecovery));
|
|
727
929
|
});
|
|
728
930
|
const followerOwner = Threads.getTelegramThreadOwnerFromProfileKey(followerProfileKey);
|
|
729
931
|
const recoverableTarget = !reconnectRecord &&
|
|
@@ -755,7 +957,9 @@ export function createTelegramBusFollowerTargetProvisioner(deps) {
|
|
|
755
957
|
},
|
|
756
958
|
profileKey: followerProfileKey,
|
|
757
959
|
threadName: workspaceBinding?.threadName ?? registration.threadName,
|
|
758
|
-
preferredSlot: workspaceIdentity?.slot ??
|
|
960
|
+
preferredSlot: workspaceIdentity?.slot ??
|
|
961
|
+
workspaceBinding?.slot ??
|
|
962
|
+
registration.slot,
|
|
759
963
|
...(workspaceIdentity
|
|
760
964
|
? {
|
|
761
965
|
workspaceBindingKey: workspaceIdentity.bindingKey,
|
|
@@ -778,7 +982,10 @@ export function createTelegramBusFollowerTargetProvisioner(deps) {
|
|
|
778
982
|
ThreadNaming.isTelegramTopicThreadNameValidForSlot(recoveryHint.threadName, recoveryHint.slot)
|
|
779
983
|
? recoveryHint.threadName
|
|
780
984
|
: undefined;
|
|
781
|
-
const requestedSlot = workspaceIdentity?.slot ??
|
|
985
|
+
const requestedSlot = workspaceIdentity?.slot ??
|
|
986
|
+
workspaceBinding?.slot ??
|
|
987
|
+
registration.slot ??
|
|
988
|
+
recoveryHint?.slot;
|
|
782
989
|
const recoveredSlot = deps.topicTargetStore.allocateSlot(followerProfileKey, requestedSlot, workspaceIdentity?.bindingKey);
|
|
783
990
|
if (!recoveredSlot) {
|
|
784
991
|
throw new TelegramWorkspaceSlotUnavailableError();
|
|
@@ -813,16 +1020,23 @@ export function createTelegramBusFollowerTargetProvisioner(deps) {
|
|
|
813
1020
|
const runRegistration = async () => {
|
|
814
1021
|
if (requestedTarget)
|
|
815
1022
|
deps.topicTargetStore.assertWorkspaceRestoreRegistration({
|
|
816
|
-
target: requestedTarget,
|
|
817
|
-
|
|
1023
|
+
target: requestedTarget,
|
|
1024
|
+
bindingKey: workspaceIdentity?.bindingKey,
|
|
1025
|
+
slot: workspaceIdentity?.slot ??
|
|
1026
|
+
workspaceBinding?.slot ??
|
|
1027
|
+
registration.slot,
|
|
818
1028
|
});
|
|
819
1029
|
const recoveryTarget = reconnectRecord?.target ?? recoverableTarget;
|
|
820
1030
|
if (recoveryTarget) {
|
|
821
1031
|
Threads.assertTelegramPendingTopicRecoveryAllowed(deps.topicTargetStore, recoveryTarget);
|
|
822
1032
|
}
|
|
823
|
-
const pendingTargetRecovery = recoverableTarget &&
|
|
824
|
-
|
|
825
|
-
|
|
1033
|
+
const pendingTargetRecovery = recoverableTarget &&
|
|
1034
|
+
deps.topicTargetStore
|
|
1035
|
+
.listPendingProvisions()
|
|
1036
|
+
.some((entry) => entry.target?.chatId === recoverableTarget.chatId &&
|
|
1037
|
+
entry.target?.threadId === recoverableTarget.threadId &&
|
|
1038
|
+
(entry.instanceId === registration.instanceId ||
|
|
1039
|
+
entry.profileKey === followerProfileKey));
|
|
826
1040
|
let result = reconnectRecord
|
|
827
1041
|
? {
|
|
828
1042
|
target: reconnectRecord.target,
|
|
@@ -833,8 +1047,7 @@ export function createTelegramBusFollowerTargetProvisioner(deps) {
|
|
|
833
1047
|
? await recoverRequestedTarget()
|
|
834
1048
|
: await provisionTarget();
|
|
835
1049
|
const alignResultWithWorkspaceSlot = () => {
|
|
836
|
-
if (!workspaceIdentity ||
|
|
837
|
-
result.record.slot === workspaceIdentity.slot)
|
|
1050
|
+
if (!workspaceIdentity || result.record.slot === workspaceIdentity.slot)
|
|
838
1051
|
return;
|
|
839
1052
|
deps.recordRuntimeEvent("bus", "Telegram follower record slot reconciled to its Workspace claim", {
|
|
840
1053
|
phase: "follower-register-slot-reconcile",
|
|
@@ -851,7 +1064,9 @@ export function createTelegramBusFollowerTargetProvisioner(deps) {
|
|
|
851
1064
|
};
|
|
852
1065
|
alignResultWithWorkspaceSlot();
|
|
853
1066
|
const assertRestoreRegistration = () => deps.topicTargetStore.assertWorkspaceRestoreRegistration({
|
|
854
|
-
target: result.target,
|
|
1067
|
+
target: result.target,
|
|
1068
|
+
slot: result.record.slot,
|
|
1069
|
+
bindingKey: workspaceIdentity?.bindingKey,
|
|
855
1070
|
});
|
|
856
1071
|
assertRestoreRegistration();
|
|
857
1072
|
const crossSessionReuse = !!reconnectRecord &&
|
|
@@ -887,7 +1102,9 @@ export function createTelegramBusFollowerTargetProvisioner(deps) {
|
|
|
887
1102
|
threadName: result.displayTitle ??
|
|
888
1103
|
(workspaceBinding?.target.chatId === result.target.chatId &&
|
|
889
1104
|
workspaceBinding.target.threadId === result.target.threadId
|
|
890
|
-
? workspaceBinding.displayTitle
|
|
1105
|
+
? workspaceBinding.displayTitle
|
|
1106
|
+
: undefined) ??
|
|
1107
|
+
result.record.threadName,
|
|
891
1108
|
slot: result.record.slot,
|
|
892
1109
|
state: "connected",
|
|
893
1110
|
})
|
|
@@ -1011,18 +1228,28 @@ export function createTelegramBusFollowerTargetProvisioner(deps) {
|
|
|
1011
1228
|
: {}),
|
|
1012
1229
|
slot: workspaceIdentity.slot,
|
|
1013
1230
|
...(workspaceIdentity.sessionId
|
|
1014
|
-
? {
|
|
1015
|
-
|
|
1231
|
+
? {
|
|
1232
|
+
journalSources: [
|
|
1233
|
+
{
|
|
1234
|
+
sessionId: workspaceIdentity.sessionId,
|
|
1235
|
+
recipientBindingKey: followerProfileKey,
|
|
1236
|
+
},
|
|
1237
|
+
],
|
|
1238
|
+
journalBindingKeys: [],
|
|
1239
|
+
}
|
|
1016
1240
|
: { journalBindingKeys: [followerProfileKey] }),
|
|
1017
1241
|
journalBindingsComplete: true,
|
|
1018
1242
|
updatedAtMs: getNowMs(),
|
|
1019
1243
|
},
|
|
1020
1244
|
});
|
|
1021
1245
|
if (connectedAnnouncement && workspaceCommit.displayTitle) {
|
|
1022
|
-
connectedAnnouncement =
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1246
|
+
connectedAnnouncement =
|
|
1247
|
+
createTelegramBusInstanceLifecycleAnnouncement({
|
|
1248
|
+
target: result.target,
|
|
1249
|
+
threadName: workspaceCommit.displayTitle,
|
|
1250
|
+
slot: result.record.slot,
|
|
1251
|
+
state: "connected",
|
|
1252
|
+
});
|
|
1026
1253
|
}
|
|
1027
1254
|
deps.topicTargetStore.markWorkspaceBindingActiveByTarget(result.target);
|
|
1028
1255
|
await deps.topicTargetStore.persist();
|
|
@@ -1156,8 +1383,11 @@ function createTelegramBusFollowerCleanupHandler(deps, trigger) {
|
|
|
1156
1383
|
target: { chatId: target.chatId, threadId: target.threadId },
|
|
1157
1384
|
requestedAtMs: (deps.getNowMs ?? Date.now)(),
|
|
1158
1385
|
};
|
|
1159
|
-
const departingRecord = deps.topicTargetStore
|
|
1160
|
-
|
|
1386
|
+
const departingRecord = deps.topicTargetStore
|
|
1387
|
+
.list()
|
|
1388
|
+
.find((record) => record.instanceId === follower.instanceId &&
|
|
1389
|
+
record.target.chatId === target.chatId &&
|
|
1390
|
+
record.target.threadId === target.threadId);
|
|
1161
1391
|
const isCleanupTargetProtected = Threads.createTelegramCleanupTargetProtection(deps.topicTargetStore, departingRecord);
|
|
1162
1392
|
deps.topicTargetStore.upsertPendingCleanup(intent);
|
|
1163
1393
|
await deps.topicTargetStore.persist();
|
|
@@ -1363,19 +1593,39 @@ function parseTelegramBusAttachmentSource(value) {
|
|
|
1363
1593
|
return undefined;
|
|
1364
1594
|
const raw = value, chat = raw.chat;
|
|
1365
1595
|
const text = (field) => typeof field === "string" && field.length > 0 && field.length <= 256;
|
|
1366
|
-
if (!text(raw.kind) ||
|
|
1596
|
+
if (!text(raw.kind) ||
|
|
1597
|
+
!Number.isSafeInteger(raw.messageId) ||
|
|
1367
1598
|
(raw.index !== undefined && !Number.isSafeInteger(raw.index)) ||
|
|
1368
|
-
(raw.userFileName !== undefined && !text(raw.userFileName)) ||
|
|
1369
|
-
(
|
|
1370
|
-
|
|
1599
|
+
(raw.userFileName !== undefined && !text(raw.userFileName)) ||
|
|
1600
|
+
(raw.scope !== undefined && !text(raw.scope)) ||
|
|
1601
|
+
(chat !== undefined &&
|
|
1602
|
+
(!chat ||
|
|
1603
|
+
typeof chat !== "object" ||
|
|
1604
|
+
Array.isArray(chat) ||
|
|
1605
|
+
!Number.isSafeInteger(chat.id) ||
|
|
1606
|
+
(chat.type !== undefined && !text(chat.type)) ||
|
|
1607
|
+
(chat.username !== undefined && !text(chat.username)))))
|
|
1371
1608
|
return undefined;
|
|
1372
|
-
return {
|
|
1609
|
+
return {
|
|
1610
|
+
kind: raw.kind,
|
|
1611
|
+
messageId: raw.messageId,
|
|
1373
1612
|
...(raw.index !== undefined ? { index: raw.index } : {}),
|
|
1374
|
-
...(raw.userFileName !== undefined
|
|
1613
|
+
...(raw.userFileName !== undefined
|
|
1614
|
+
? { userFileName: raw.userFileName }
|
|
1615
|
+
: {}),
|
|
1375
1616
|
...(raw.scope !== undefined ? { scope: raw.scope } : {}),
|
|
1376
|
-
...(chat
|
|
1377
|
-
|
|
1378
|
-
|
|
1617
|
+
...(chat
|
|
1618
|
+
? {
|
|
1619
|
+
chat: {
|
|
1620
|
+
id: chat.id,
|
|
1621
|
+
...(chat.type !== undefined ? { type: chat.type } : {}),
|
|
1622
|
+
...(chat.username !== undefined
|
|
1623
|
+
? { username: chat.username }
|
|
1624
|
+
: {}),
|
|
1625
|
+
},
|
|
1626
|
+
}
|
|
1627
|
+
: {}),
|
|
1628
|
+
};
|
|
1379
1629
|
}
|
|
1380
1630
|
export function createTelegramBusLeaderApiProxy(deps) {
|
|
1381
1631
|
return async (method, args) => {
|
|
@@ -1431,49 +1681,350 @@ function createTelegramBusFollowerMutationRunner() {
|
|
|
1431
1681
|
}
|
|
1432
1682
|
};
|
|
1433
1683
|
}
|
|
1684
|
+
/** One delegated text effect through existing Workspace/API owners; no closure transport, ordinary proxy or replay loop. */
|
|
1685
|
+
export function createTelegramBusSelectedMenuDeliveryHandler(deps) {
|
|
1686
|
+
return async (input, isCallerCurrent) => {
|
|
1687
|
+
let issued = false, method = "sendMessage";
|
|
1688
|
+
try {
|
|
1689
|
+
const decoded = parseTelegramBusEnvelope(JSON.stringify(input));
|
|
1690
|
+
if (decoded?.kind !== "follower.deliverSelectedMenu")
|
|
1691
|
+
throw new Error("Selected-menu wire is unavailable.");
|
|
1692
|
+
const expected = decoded, { recipient, executor, effect } = expected;
|
|
1693
|
+
method = effect.kind === "send-text" ? "sendMessage" : "editMessageText";
|
|
1694
|
+
const workspace = deps.workspace, api = deps.api, registry = deps.followerRegistry, protocol = structuredClone(deps.protocolIdentity);
|
|
1695
|
+
const scope = workspace?.getScopeKey, scopeKey = scope?.call(workspace);
|
|
1696
|
+
const capture = workspace?.captureAuthority, getStore = workspace?.getStore, threads = workspace?.threadStore, resolveJournal = workspace?.getJournalBindingKey, run = workspace?.run, runtime = api?.runtime, call = runtime?.call, authorize = api?.authorize, record = api?.record;
|
|
1697
|
+
const authority = capture?.call(workspace), store = getStore?.call(workspace), follower = registry.get(expected.instanceId);
|
|
1698
|
+
if (!scopeKey?.trim() ||
|
|
1699
|
+
!authority ||
|
|
1700
|
+
!isDeepStrictEqual(authority.executor, executor) ||
|
|
1701
|
+
authority.operatorUserId !== expected.operatorUserId ||
|
|
1702
|
+
!store ||
|
|
1703
|
+
typeof threads?.withWorkspaceLiveRebindSnapshot !== "function" ||
|
|
1704
|
+
typeof resolveJournal !== "function" ||
|
|
1705
|
+
typeof run !== "function" ||
|
|
1706
|
+
typeof call !== "function" ||
|
|
1707
|
+
typeof authorize !== "function" ||
|
|
1708
|
+
typeof record !== "function" ||
|
|
1709
|
+
!follower)
|
|
1710
|
+
throw new Error("Selected-menu proof/API owner is unavailable.");
|
|
1711
|
+
const retained = store
|
|
1712
|
+
.listLiveRebindings()
|
|
1713
|
+
.find((value) => value.request.operationId === expected.operationId);
|
|
1714
|
+
if (!retained ||
|
|
1715
|
+
retained.phase !== "released" ||
|
|
1716
|
+
retained.cleanup ||
|
|
1717
|
+
retained.request.source.updateIds.length !== 1 ||
|
|
1718
|
+
retained.recipient.kind !== "follower" ||
|
|
1719
|
+
!isDeepStrictEqual(retained.executor, executor) ||
|
|
1720
|
+
retained.operatorUserId !== expected.operatorUserId ||
|
|
1721
|
+
retained.recipient.instanceId !== expected.instanceId ||
|
|
1722
|
+
retained.recipient.sessionId !== recipient.sessionId ||
|
|
1723
|
+
retained.recipient.generation !== expected.registrationGeneration ||
|
|
1724
|
+
retained.request.owner.profileKey !== recipient.profileKey ||
|
|
1725
|
+
!isDeepStrictEqual(retained.request.target, recipient.target))
|
|
1726
|
+
throw new Error("Selected-menu operation/recipient is unavailable.");
|
|
1727
|
+
const intent = structuredClone(retained), snapshot = threads.withWorkspaceLiveRebindSnapshot;
|
|
1728
|
+
const registered = structuredClone(follower);
|
|
1729
|
+
const body = {
|
|
1730
|
+
chat_id: recipient.target.chatId,
|
|
1731
|
+
text: effect.text,
|
|
1732
|
+
...(effect.parseMode ? { parse_mode: effect.parseMode } : {}),
|
|
1733
|
+
...(effect.replyMarkup
|
|
1734
|
+
? { reply_markup: structuredClone(effect.replyMarkup) }
|
|
1735
|
+
: {}),
|
|
1736
|
+
...(effect.kind === "send-text"
|
|
1737
|
+
? {
|
|
1738
|
+
message_thread_id: recipient.target.threadId,
|
|
1739
|
+
...(effect.replyToMessageId
|
|
1740
|
+
? {
|
|
1741
|
+
reply_parameters: {
|
|
1742
|
+
message_id: effect.replyToMessageId,
|
|
1743
|
+
allow_sending_without_reply: true,
|
|
1744
|
+
},
|
|
1745
|
+
}
|
|
1746
|
+
: {}),
|
|
1747
|
+
}
|
|
1748
|
+
: { message_id: effect.messageId }),
|
|
1749
|
+
};
|
|
1750
|
+
const assertRecipientCurrent = () => {
|
|
1751
|
+
const activeAuthority = capture.call(workspace), live = registry.get(expected.instanceId);
|
|
1752
|
+
if (!isCallerCurrent() ||
|
|
1753
|
+
deps.workspace !== workspace ||
|
|
1754
|
+
deps.api !== api ||
|
|
1755
|
+
deps.followerRegistry !== registry ||
|
|
1756
|
+
workspace.getScopeKey !== scope ||
|
|
1757
|
+
scope.call(workspace) !== scopeKey ||
|
|
1758
|
+
workspace.captureAuthority !== capture ||
|
|
1759
|
+
workspace.getStore !== getStore ||
|
|
1760
|
+
workspace.threadStore !== threads ||
|
|
1761
|
+
workspace.getJournalBindingKey !== resolveJournal ||
|
|
1762
|
+
workspace.run !== run ||
|
|
1763
|
+
threads.withWorkspaceLiveRebindSnapshot !== snapshot ||
|
|
1764
|
+
api.runtime !== runtime ||
|
|
1765
|
+
runtime.call !== call ||
|
|
1766
|
+
api.authorize !== authorize ||
|
|
1767
|
+
api.record !== record ||
|
|
1768
|
+
!authority.isCurrent() ||
|
|
1769
|
+
!activeAuthority?.isCurrent() ||
|
|
1770
|
+
activeAuthority.operatorUserId !== expected.operatorUserId ||
|
|
1771
|
+
!isDeepStrictEqual(activeAuthority.executor, executor) ||
|
|
1772
|
+
getStore.call(workspace) !== store ||
|
|
1773
|
+
!isDeepStrictEqual(deps.protocolIdentity, protocol) ||
|
|
1774
|
+
!hasTelegramBusCapability(protocol, TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY) ||
|
|
1775
|
+
!live ||
|
|
1776
|
+
!getTelegramBusProtocolCompatibility({
|
|
1777
|
+
local: protocol,
|
|
1778
|
+
remote: live.protocol,
|
|
1779
|
+
}).compatible ||
|
|
1780
|
+
!hasTelegramBusCapability(live.protocol, TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY) ||
|
|
1781
|
+
!isDeepStrictEqual(live.protocol, registered.protocol) ||
|
|
1782
|
+
live.registrationGeneration !== expected.registrationGeneration ||
|
|
1783
|
+
live.sessionId !== recipient.sessionId ||
|
|
1784
|
+
live.sessionGeneration !== recipient.sessionGeneration ||
|
|
1785
|
+
live.pid !== recipient.processId ||
|
|
1786
|
+
live.processBirthId !== recipient.processBirthId ||
|
|
1787
|
+
live.profileKey !== recipient.profileKey ||
|
|
1788
|
+
live.cwd !== registered.cwd ||
|
|
1789
|
+
live.slot !== registered.slot ||
|
|
1790
|
+
live.busSocketPath !== registered.busSocketPath ||
|
|
1791
|
+
live.target?.chatId !== recipient.target.chatId ||
|
|
1792
|
+
live.target.threadId !== recipient.target.threadId ||
|
|
1793
|
+
resolveJournal.call(workspace, live) !== recipient.journalBindingKey)
|
|
1794
|
+
throw new Error("Selected-menu authority changed.");
|
|
1795
|
+
let confirmed = false;
|
|
1796
|
+
snapshot.call(threads, intent, (view) => {
|
|
1797
|
+
const bindings = (view.workspaceBindings ?? []).filter((value) => value.bindingKey === intent.request.binding.bindingKey);
|
|
1798
|
+
const owners = view.threads.filter((value) => value.status === "active" &&
|
|
1799
|
+
(value.slot === intent.request.binding.slot ||
|
|
1800
|
+
isDeepStrictEqual(value.target, recipient.target)));
|
|
1801
|
+
const binding = bindings[0], owner = owners[0];
|
|
1802
|
+
confirmed =
|
|
1803
|
+
bindings.length === 1 &&
|
|
1804
|
+
binding?.sessionId === recipient.sessionId &&
|
|
1805
|
+
binding.cwd === intent.request.binding.cwd &&
|
|
1806
|
+
binding.workspaceKey === intent.request.binding.workspaceKey &&
|
|
1807
|
+
binding.slot === live.slot &&
|
|
1808
|
+
binding.inactiveSinceMs === undefined &&
|
|
1809
|
+
isDeepStrictEqual(binding.target, recipient.target) &&
|
|
1810
|
+
owners.length === 1 &&
|
|
1811
|
+
owner?.instanceId === expected.instanceId &&
|
|
1812
|
+
owner.slot === binding.slot &&
|
|
1813
|
+
owner.profileKey === recipient.profileKey &&
|
|
1814
|
+
isDeepStrictEqual(owner.owner, intent.request.owner.owner) &&
|
|
1815
|
+
isDeepStrictEqual(owner.target, recipient.target) &&
|
|
1816
|
+
!!live.cwd &&
|
|
1817
|
+
WorkspaceIdentity.normalizeTelegramWorkspacePath(live.cwd) ===
|
|
1818
|
+
binding.cwd;
|
|
1819
|
+
});
|
|
1820
|
+
if (!confirmed || !isCallerCurrent() || !authority.isCurrent())
|
|
1821
|
+
throw new Error("Selected-menu canonical recipient is unavailable.");
|
|
1822
|
+
};
|
|
1823
|
+
const assertCurrent = () => {
|
|
1824
|
+
assertRecipientCurrent();
|
|
1825
|
+
const live = registry.get(expected.instanceId);
|
|
1826
|
+
if (!live ||
|
|
1827
|
+
authorize.call(api, {
|
|
1828
|
+
follower: structuredClone(live),
|
|
1829
|
+
method: "call",
|
|
1830
|
+
args: [method, structuredClone(body)],
|
|
1831
|
+
}) !== true)
|
|
1832
|
+
throw new Error("Selected-menu API recipient is unavailable.");
|
|
1833
|
+
assertRecipientCurrent();
|
|
1834
|
+
};
|
|
1835
|
+
assertCurrent();
|
|
1836
|
+
const result = await run({
|
|
1837
|
+
operationId: createTelegramWorkspaceAdmissionOperationId(),
|
|
1838
|
+
operationKind: "workspace.selected-menu-delivery",
|
|
1839
|
+
scopes: [{ kind: "profile" }],
|
|
1840
|
+
}, async () => {
|
|
1841
|
+
assertCurrent();
|
|
1842
|
+
issued = true;
|
|
1843
|
+
const result = await call.call(runtime, method, body, {
|
|
1844
|
+
assertAuthority: assertCurrent,
|
|
1845
|
+
maxAttempts: 1,
|
|
1846
|
+
retryRateLimit: false,
|
|
1847
|
+
retrySafety: "non-idempotent",
|
|
1848
|
+
});
|
|
1849
|
+
assertCurrent();
|
|
1850
|
+
const value = result && typeof result === "object" && !Array.isArray(result)
|
|
1851
|
+
? result
|
|
1852
|
+
: undefined;
|
|
1853
|
+
const messageId = effect.kind === "edit-text" ? effect.messageId : value?.message_id;
|
|
1854
|
+
if (!Number.isSafeInteger(messageId) ||
|
|
1855
|
+
messageId <= 0 ||
|
|
1856
|
+
(effect.kind === "edit-text" &&
|
|
1857
|
+
result !== true &&
|
|
1858
|
+
value?.message_id !== messageId))
|
|
1859
|
+
throw new Error("Selected-menu API result is unconfirmed.");
|
|
1860
|
+
if (effect.kind === "send-text" &&
|
|
1861
|
+
record.call(api, {
|
|
1862
|
+
follower: structuredClone(registered),
|
|
1863
|
+
chatId: recipient.target.chatId,
|
|
1864
|
+
messageId: messageId,
|
|
1865
|
+
target: { ...recipient.target },
|
|
1866
|
+
}, assertCurrent) !== true)
|
|
1867
|
+
throw new Error("Selected-menu ownership publication is unconfirmed.");
|
|
1868
|
+
assertCurrent();
|
|
1869
|
+
return {
|
|
1870
|
+
operationId: expected.operationId,
|
|
1871
|
+
recipient: structuredClone(recipient),
|
|
1872
|
+
registrationGeneration: expected.registrationGeneration,
|
|
1873
|
+
effect: effect.kind,
|
|
1874
|
+
messageId,
|
|
1875
|
+
};
|
|
1876
|
+
});
|
|
1877
|
+
assertCurrent();
|
|
1878
|
+
return {
|
|
1879
|
+
kind: "bus.ack",
|
|
1880
|
+
requestId: expected.requestId,
|
|
1881
|
+
ok: true,
|
|
1882
|
+
result,
|
|
1883
|
+
};
|
|
1884
|
+
}
|
|
1885
|
+
catch (error) {
|
|
1886
|
+
const unknown = issued &&
|
|
1887
|
+
!(error instanceof TelegramApiAuthorityError && !error.requestIssued);
|
|
1888
|
+
return {
|
|
1889
|
+
kind: "bus.ack",
|
|
1890
|
+
requestId: input.requestId,
|
|
1891
|
+
ok: false,
|
|
1892
|
+
message: "Selected-menu delivery is refused or unconfirmed.",
|
|
1893
|
+
...(unknown
|
|
1894
|
+
? { error: { code: "commit-unknown", method } }
|
|
1895
|
+
: {}),
|
|
1896
|
+
};
|
|
1897
|
+
}
|
|
1898
|
+
};
|
|
1899
|
+
}
|
|
1900
|
+
/** The current registration a follower request names, or the negative ack refusing an unknown or stale one. */
|
|
1901
|
+
function resolveRegisteredTelegramBusFollower(registry, requestId, instanceId, registrationGeneration) {
|
|
1902
|
+
const follower = registry.get(instanceId);
|
|
1903
|
+
if (!follower) {
|
|
1904
|
+
return {
|
|
1905
|
+
rejection: rejectTelegramBusRequest(requestId, "Unknown Telegram bus follower instance."),
|
|
1906
|
+
};
|
|
1907
|
+
}
|
|
1908
|
+
if (!follower.registrationGeneration ||
|
|
1909
|
+
registrationGeneration !== follower.registrationGeneration) {
|
|
1910
|
+
return {
|
|
1911
|
+
rejection: rejectTelegramBusRequest(requestId, "Stale Telegram bus follower registration generation."),
|
|
1912
|
+
};
|
|
1913
|
+
}
|
|
1914
|
+
return { follower };
|
|
1915
|
+
}
|
|
1434
1916
|
export function createTelegramBusLeaderEnvelopeHandler(deps) {
|
|
1435
1917
|
const getNowMs = deps.getNowMs ?? Date.now;
|
|
1436
1918
|
const runFollowerMutation = deps.runFollowerMutation ?? createTelegramBusFollowerMutationRunner();
|
|
1437
1919
|
const recipientObservations = new Map();
|
|
1920
|
+
/** Rename and reset share one generation- and target-fenced Workspace Thread name change. */
|
|
1921
|
+
const changeFollowerThreadName = (envelope, action, apply) => runFollowerMutation(deps.followerRegistry.get(envelope.instanceId) ?? {
|
|
1922
|
+
instanceId: envelope.instanceId,
|
|
1923
|
+
}, async () => {
|
|
1924
|
+
const resolved = resolveRegisteredTelegramBusFollower(deps.followerRegistry, envelope.requestId, envelope.instanceId, envelope.registrationGeneration);
|
|
1925
|
+
if ("rejection" in resolved)
|
|
1926
|
+
return resolved.rejection;
|
|
1927
|
+
const follower = resolved.follower;
|
|
1928
|
+
if (follower.target?.chatId !== envelope.target.chatId ||
|
|
1929
|
+
follower.target?.threadId !== envelope.target.threadId)
|
|
1930
|
+
return rejectTelegramBusRequest(envelope.requestId, "Stale Telegram bus follower registration generation.");
|
|
1931
|
+
if (!hasTelegramBusCapability(deps.protocolIdentity, TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME) ||
|
|
1932
|
+
!hasTelegramBusCapability(follower.protocol, TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME) ||
|
|
1933
|
+
!apply)
|
|
1934
|
+
return rejectTelegramBusRequest(envelope.requestId, `Telegram Workspace Thread ${action} is unavailable.`);
|
|
1935
|
+
try {
|
|
1936
|
+
const result = await apply(follower);
|
|
1937
|
+
const current = deps.followerRegistry.get(follower.instanceId);
|
|
1938
|
+
if (!current ||
|
|
1939
|
+
current.registrationGeneration !== follower.registrationGeneration)
|
|
1940
|
+
return rejectTelegramBusRequest(envelope.requestId, "Stale Telegram bus follower registration generation.");
|
|
1941
|
+
deps.followerRegistry.register({
|
|
1942
|
+
...current,
|
|
1943
|
+
threadName: result.threadName,
|
|
1944
|
+
connectedAtMs: current.connectedAtMs,
|
|
1945
|
+
});
|
|
1946
|
+
return {
|
|
1947
|
+
kind: "bus.ack",
|
|
1948
|
+
requestId: envelope.requestId,
|
|
1949
|
+
ok: true,
|
|
1950
|
+
result,
|
|
1951
|
+
};
|
|
1952
|
+
}
|
|
1953
|
+
catch (error) {
|
|
1954
|
+
return rejectTelegramBusRequest(envelope.requestId, error instanceof Error
|
|
1955
|
+
? error.message
|
|
1956
|
+
: `Telegram Workspace Thread ${action} failed.`);
|
|
1957
|
+
}
|
|
1958
|
+
});
|
|
1438
1959
|
const observeRestoreRecipient = (follower) => {
|
|
1439
|
-
if (!deps.authSecret ||
|
|
1960
|
+
if (!deps.authSecret ||
|
|
1961
|
+
!deps.onWorkspaceRestoreRecipientObserved ||
|
|
1962
|
+
!deps.getTelegramProfile ||
|
|
1440
1963
|
!hasTelegramBusCapability(deps.protocolIdentity, TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE) ||
|
|
1441
1964
|
!hasTelegramBusCapability(follower.protocol, TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE))
|
|
1442
1965
|
return;
|
|
1443
1966
|
const instanceId = follower.instanceId;
|
|
1444
1967
|
const report = (error) => {
|
|
1445
1968
|
try {
|
|
1446
|
-
deps.recordRuntimeEvent?.("bus", error, {
|
|
1969
|
+
deps.recordRuntimeEvent?.("bus", error, {
|
|
1970
|
+
phase: "workspace-restore-recipient-observation",
|
|
1971
|
+
instanceId,
|
|
1972
|
+
});
|
|
1973
|
+
}
|
|
1974
|
+
catch {
|
|
1975
|
+
/* Optional observation diagnostics must not break transport liveness. */
|
|
1447
1976
|
}
|
|
1448
|
-
catch { /* Optional observation diagnostics must not break transport liveness. */ }
|
|
1449
1977
|
};
|
|
1450
1978
|
try {
|
|
1451
1979
|
const epoch = deps.getCurrentLeaderEpoch?.(), operator = deps.getAllowedUserId?.();
|
|
1452
1980
|
const runtimeGeneration = deps.getWorkspaceRestoreObservationGeneration?.();
|
|
1453
|
-
if (epoch === undefined ||
|
|
1454
|
-
|
|
1981
|
+
if (epoch === undefined ||
|
|
1982
|
+
operator === undefined ||
|
|
1983
|
+
(deps.getWorkspaceRestoreObservationGeneration &&
|
|
1984
|
+
runtimeGeneration === undefined))
|
|
1455
1985
|
return;
|
|
1456
1986
|
const snapshot = structuredClone(follower);
|
|
1457
|
-
const observed = {
|
|
1458
|
-
|
|
1987
|
+
const observed = {
|
|
1988
|
+
epoch,
|
|
1989
|
+
operator,
|
|
1990
|
+
profile: deps.getTelegramProfile(),
|
|
1991
|
+
runtimeGeneration,
|
|
1992
|
+
follower: {
|
|
1993
|
+
...snapshot,
|
|
1994
|
+
lastHeartbeatMs: undefined,
|
|
1995
|
+
connectedAtMs: undefined,
|
|
1996
|
+
threadName: undefined,
|
|
1997
|
+
},
|
|
1998
|
+
};
|
|
1459
1999
|
if (isDeepStrictEqual(recipientObservations.get(instanceId), observed))
|
|
1460
2000
|
return;
|
|
1461
2001
|
recipientObservations.set(instanceId, observed);
|
|
1462
2002
|
const isCurrent = () => {
|
|
1463
2003
|
const live = deps.followerRegistry.get(instanceId);
|
|
1464
|
-
return recipientObservations.get(instanceId) === observed &&
|
|
1465
|
-
deps.getWorkspaceRestoreObservationGeneration?.() ===
|
|
2004
|
+
return (recipientObservations.get(instanceId) === observed &&
|
|
2005
|
+
deps.getWorkspaceRestoreObservationGeneration?.() ===
|
|
2006
|
+
observed.runtimeGeneration &&
|
|
1466
2007
|
hasTelegramBusCapability(deps.protocolIdentity, TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE) &&
|
|
1467
|
-
deps.getCurrentLeaderEpoch?.() === observed.epoch &&
|
|
1468
|
-
deps.
|
|
2008
|
+
deps.getCurrentLeaderEpoch?.() === observed.epoch &&
|
|
2009
|
+
deps.getAllowedUserId?.() === observed.operator &&
|
|
2010
|
+
deps.getTelegramProfile?.() === observed.profile &&
|
|
2011
|
+
!!live &&
|
|
1469
2012
|
hasTelegramBusCapability(live.protocol, TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE) &&
|
|
1470
|
-
isDeepStrictEqual({
|
|
2013
|
+
isDeepStrictEqual({
|
|
2014
|
+
...live,
|
|
2015
|
+
lastHeartbeatMs: undefined,
|
|
2016
|
+
connectedAtMs: undefined,
|
|
2017
|
+
threadName: undefined,
|
|
2018
|
+
}, observed.follower));
|
|
1471
2019
|
};
|
|
1472
2020
|
// A heartbeat is a hint, not completion proof. Never await the listener before acknowledging it.
|
|
1473
|
-
void Promise.resolve()
|
|
2021
|
+
void Promise.resolve()
|
|
2022
|
+
.then(() => {
|
|
1474
2023
|
if (isCurrent())
|
|
1475
2024
|
return deps.onWorkspaceRestoreRecipientObserved?.(structuredClone(snapshot), isCurrent);
|
|
1476
|
-
})
|
|
2025
|
+
})
|
|
2026
|
+
.catch(report)
|
|
2027
|
+
.finally(() => {
|
|
1477
2028
|
if (recipientObservations.get(instanceId) === observed)
|
|
1478
2029
|
recipientObservations.delete(instanceId);
|
|
1479
2030
|
});
|
|
@@ -1483,24 +2034,10 @@ export function createTelegramBusLeaderEnvelopeHandler(deps) {
|
|
|
1483
2034
|
}
|
|
1484
2035
|
};
|
|
1485
2036
|
const handleAgentRequest = async (envelope) => {
|
|
1486
|
-
const
|
|
1487
|
-
if (
|
|
1488
|
-
return
|
|
1489
|
-
|
|
1490
|
-
requestId: envelope.requestId,
|
|
1491
|
-
ok: false,
|
|
1492
|
-
message: "Unknown Telegram bus follower instance.",
|
|
1493
|
-
};
|
|
1494
|
-
}
|
|
1495
|
-
if (!follower.registrationGeneration ||
|
|
1496
|
-
envelope.registrationGeneration !== follower.registrationGeneration) {
|
|
1497
|
-
return {
|
|
1498
|
-
kind: "bus.ack",
|
|
1499
|
-
requestId: envelope.requestId,
|
|
1500
|
-
ok: false,
|
|
1501
|
-
message: "Stale Telegram bus follower registration generation.",
|
|
1502
|
-
};
|
|
1503
|
-
}
|
|
2037
|
+
const followerResolution = resolveRegisteredTelegramBusFollower(deps.followerRegistry, envelope.requestId, envelope.instanceId, envelope.registrationGeneration);
|
|
2038
|
+
if ("rejection" in followerResolution)
|
|
2039
|
+
return followerResolution.rejection;
|
|
2040
|
+
const follower = followerResolution.follower;
|
|
1504
2041
|
deps.followerRegistry.heartbeat(envelope.instanceId, getNowMs());
|
|
1505
2042
|
if (envelope.kind === "follower.resolveAgentTarget") {
|
|
1506
2043
|
const target = await deps.resolveAgentTarget?.(follower, envelope.selector);
|
|
@@ -1511,71 +2048,32 @@ export function createTelegramBusLeaderEnvelopeHandler(deps) {
|
|
|
1511
2048
|
ok: true,
|
|
1512
2049
|
result: target,
|
|
1513
2050
|
}
|
|
1514
|
-
:
|
|
1515
|
-
kind: "bus.ack",
|
|
1516
|
-
requestId: envelope.requestId,
|
|
1517
|
-
ok: false,
|
|
1518
|
-
message: "Telegram agent target is unavailable or ambiguous.",
|
|
1519
|
-
};
|
|
2051
|
+
: rejectTelegramBusRequest(envelope.requestId, "Telegram agent target is unavailable or ambiguous.");
|
|
1520
2052
|
}
|
|
1521
2053
|
if (!deps.routeAgentMessage) {
|
|
1522
|
-
return
|
|
1523
|
-
kind: "bus.ack",
|
|
1524
|
-
requestId: envelope.requestId,
|
|
1525
|
-
ok: false,
|
|
1526
|
-
message: "Telegram agent message routing is unavailable.",
|
|
1527
|
-
};
|
|
2054
|
+
return rejectTelegramBusRequest(envelope.requestId, "Telegram agent message routing is unavailable.");
|
|
1528
2055
|
}
|
|
1529
2056
|
await deps.routeAgentMessage(follower, envelope.message);
|
|
1530
2057
|
return { kind: "bus.ack", requestId: envelope.requestId, ok: true };
|
|
1531
2058
|
};
|
|
1532
2059
|
const routeQueueHandoff = async (envelope) => {
|
|
1533
|
-
const
|
|
1534
|
-
if (
|
|
1535
|
-
return
|
|
1536
|
-
|
|
1537
|
-
requestId: envelope.requestId,
|
|
1538
|
-
ok: false,
|
|
1539
|
-
message: "Unknown Telegram bus follower instance.",
|
|
1540
|
-
};
|
|
1541
|
-
}
|
|
1542
|
-
if (!donor.registrationGeneration ||
|
|
1543
|
-
envelope.registrationGeneration !== donor.registrationGeneration) {
|
|
1544
|
-
return {
|
|
1545
|
-
kind: "bus.ack",
|
|
1546
|
-
requestId: envelope.requestId,
|
|
1547
|
-
ok: false,
|
|
1548
|
-
message: "Stale Telegram bus follower registration generation.",
|
|
1549
|
-
};
|
|
1550
|
-
}
|
|
2060
|
+
const donorResolution = resolveRegisteredTelegramBusFollower(deps.followerRegistry, envelope.requestId, envelope.instanceId, envelope.registrationGeneration);
|
|
2061
|
+
if ("rejection" in donorResolution)
|
|
2062
|
+
return donorResolution.rejection;
|
|
2063
|
+
const donor = donorResolution.follower;
|
|
1551
2064
|
const recipient = deps.followerRegistry.get(envelope.recipientInstanceId);
|
|
1552
2065
|
if (!hasTelegramBusCapability(deps.protocolIdentity, TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF) ||
|
|
1553
2066
|
!hasTelegramBusCapability(donor.protocol, TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF) ||
|
|
1554
2067
|
!hasTelegramBusCapability(recipient?.protocol, TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF)) {
|
|
1555
|
-
return
|
|
1556
|
-
kind: "bus.ack",
|
|
1557
|
-
requestId: envelope.requestId,
|
|
1558
|
-
ok: false,
|
|
1559
|
-
message: "Telegram queue handoff capability was not negotiated.",
|
|
1560
|
-
};
|
|
2068
|
+
return rejectTelegramBusRequest(envelope.requestId, "Telegram queue handoff capability was not negotiated.");
|
|
1561
2069
|
}
|
|
1562
2070
|
if (!recipient?.registrationGeneration ||
|
|
1563
2071
|
envelope.recipientRegistrationGeneration !==
|
|
1564
2072
|
recipient.registrationGeneration) {
|
|
1565
|
-
return
|
|
1566
|
-
kind: "bus.ack",
|
|
1567
|
-
requestId: envelope.requestId,
|
|
1568
|
-
ok: false,
|
|
1569
|
-
message: "Stale Telegram queue handoff recipient registration generation.",
|
|
1570
|
-
};
|
|
2073
|
+
return rejectTelegramBusRequest(envelope.requestId, "Stale Telegram queue handoff recipient registration generation.");
|
|
1571
2074
|
}
|
|
1572
2075
|
if (donor.instanceId === recipient.instanceId) {
|
|
1573
|
-
return
|
|
1574
|
-
kind: "bus.ack",
|
|
1575
|
-
requestId: envelope.requestId,
|
|
1576
|
-
ok: false,
|
|
1577
|
-
message: "Telegram queue handoff recipient must be another runtime.",
|
|
1578
|
-
};
|
|
2076
|
+
return rejectTelegramBusRequest(envelope.requestId, "Telegram queue handoff recipient must be another runtime.");
|
|
1579
2077
|
}
|
|
1580
2078
|
if (deps.routeQueueHandoff) {
|
|
1581
2079
|
const result = await deps.routeQueueHandoff(donor, envelope);
|
|
@@ -1632,25 +2130,10 @@ export function createTelegramBusLeaderEnvelopeHandler(deps) {
|
|
|
1632
2130
|
};
|
|
1633
2131
|
};
|
|
1634
2132
|
const forwardToFollower = async (envelope) => {
|
|
1635
|
-
const
|
|
1636
|
-
if (
|
|
1637
|
-
return
|
|
1638
|
-
|
|
1639
|
-
requestId: envelope.requestId,
|
|
1640
|
-
ok: false,
|
|
1641
|
-
message: "Unknown Telegram bus follower instance.",
|
|
1642
|
-
};
|
|
1643
|
-
}
|
|
1644
|
-
if (!follower.registrationGeneration ||
|
|
1645
|
-
envelope.recipientRegistrationGeneration !==
|
|
1646
|
-
follower.registrationGeneration) {
|
|
1647
|
-
return {
|
|
1648
|
-
kind: "bus.ack",
|
|
1649
|
-
requestId: envelope.requestId,
|
|
1650
|
-
ok: false,
|
|
1651
|
-
message: "Stale Telegram bus follower registration generation.",
|
|
1652
|
-
};
|
|
1653
|
-
}
|
|
2133
|
+
const followerResolution = resolveRegisteredTelegramBusFollower(deps.followerRegistry, envelope.requestId, envelope.recipientInstanceId, envelope.recipientRegistrationGeneration);
|
|
2134
|
+
if ("rejection" in followerResolution)
|
|
2135
|
+
return followerResolution.rejection;
|
|
2136
|
+
const follower = followerResolution.follower;
|
|
1654
2137
|
const followerSocketPath = follower.busSocketPath ??
|
|
1655
2138
|
getTelegramBusFollowerSocketPath(envelope.recipientInstanceId);
|
|
1656
2139
|
deps.followerRegistry.heartbeat(follower.instanceId, getNowMs());
|
|
@@ -1695,12 +2178,7 @@ export function createTelegramBusLeaderEnvelopeHandler(deps) {
|
|
|
1695
2178
|
return async (envelope) => {
|
|
1696
2179
|
const trafficClass = getTelegramBusEnvelopeTrafficClass(envelope);
|
|
1697
2180
|
if (trafficClass === "response") {
|
|
1698
|
-
return
|
|
1699
|
-
kind: "bus.ack",
|
|
1700
|
-
requestId: envelope.requestId,
|
|
1701
|
-
ok: false,
|
|
1702
|
-
message: "Telegram bus response envelope cannot be used as a request.",
|
|
1703
|
-
};
|
|
2181
|
+
return rejectTelegramBusRequest(envelope.requestId, "Telegram bus response envelope cannot be used as a request.");
|
|
1704
2182
|
}
|
|
1705
2183
|
if (!isTelegramBusEnvelopeAuthorized(envelope, deps.authSecret)) {
|
|
1706
2184
|
return createUnauthorizedBusAck(envelope.requestId);
|
|
@@ -1712,12 +2190,14 @@ export function createTelegramBusLeaderEnvelopeHandler(deps) {
|
|
|
1712
2190
|
local: deps.protocolIdentity,
|
|
1713
2191
|
remote: envelope.registration.protocol,
|
|
1714
2192
|
});
|
|
1715
|
-
const displayCompatible = () => (deps.getThreadDisplayMode?.() ?? "names") === "names" ||
|
|
1716
|
-
hasTelegramBusCapability(
|
|
2193
|
+
const displayCompatible = () => (deps.getThreadDisplayMode?.() ?? "names") === "names" ||
|
|
2194
|
+
(hasTelegramBusCapability(deps.protocolIdentity, TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE) &&
|
|
2195
|
+
hasTelegramBusCapability(envelope.registration.protocol, TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE));
|
|
1717
2196
|
const restoringWorkspace = envelope.kind === "follower.restoreWorkspace";
|
|
1718
2197
|
const supportsWorkspaceAutoConnect = hasTelegramBusCapability(deps.protocolIdentity, TELEGRAM_BUS_CAPABILITY_WORKSPACE_FOLLOWER_AUTO_CONNECT) &&
|
|
1719
2198
|
hasTelegramBusCapability(envelope.registration.protocol, TELEGRAM_BUS_CAPABILITY_WORKSPACE_FOLLOWER_AUTO_CONNECT);
|
|
1720
|
-
if (!compatibility.compatible ||
|
|
2199
|
+
if (!compatibility.compatible ||
|
|
2200
|
+
!displayCompatible() ||
|
|
1721
2201
|
(envelope.registration.cwd !== undefined &&
|
|
1722
2202
|
envelope.registration.sessionId === undefined) ||
|
|
1723
2203
|
(restoringWorkspace && !supportsWorkspaceAutoConnect)) {
|
|
@@ -1736,7 +2216,9 @@ export function createTelegramBusLeaderEnvelopeHandler(deps) {
|
|
|
1736
2216
|
}
|
|
1737
2217
|
if (!envelope.registration.registrationGeneration) {
|
|
1738
2218
|
return {
|
|
1739
|
-
kind: "bus.ack",
|
|
2219
|
+
kind: "bus.ack",
|
|
2220
|
+
requestId: envelope.requestId,
|
|
2221
|
+
ok: false,
|
|
1740
2222
|
protocol: deps.protocolIdentity,
|
|
1741
2223
|
message: "Telegram follower registration requires an exact generation.",
|
|
1742
2224
|
};
|
|
@@ -1746,13 +2228,17 @@ export function createTelegramBusLeaderEnvelopeHandler(deps) {
|
|
|
1746
2228
|
const leaderEpoch = deps.getCurrentLeaderEpoch?.();
|
|
1747
2229
|
const telegramProfile = deps.getTelegramProfile?.();
|
|
1748
2230
|
const operatorUserId = deps.getAllowedUserId?.();
|
|
1749
|
-
const registrationCurrent = () => (!deps.getCurrentLeaderEpoch ||
|
|
1750
|
-
|
|
1751
|
-
(!deps.
|
|
2231
|
+
const registrationCurrent = () => (!deps.getCurrentLeaderEpoch ||
|
|
2232
|
+
deps.getCurrentLeaderEpoch() === leaderEpoch) &&
|
|
2233
|
+
(!deps.getTelegramProfile ||
|
|
2234
|
+
deps.getTelegramProfile() === telegramProfile) &&
|
|
2235
|
+
(!deps.getAllowedUserId ||
|
|
2236
|
+
deps.getAllowedUserId() === operatorUserId);
|
|
1752
2237
|
if (deps.getCurrentLeaderEpoch && leaderEpoch === undefined) {
|
|
1753
2238
|
throw new Error("Telegram follower registration requires leader ownership.");
|
|
1754
2239
|
}
|
|
1755
|
-
if (hasTelegramBusCapability(deps.protocolIdentity, TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE) &&
|
|
2240
|
+
if (hasTelegramBusCapability(deps.protocolIdentity, TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE) &&
|
|
2241
|
+
!deps.commitFollowerRegistration)
|
|
1756
2242
|
throw new Error("Workspace Restore registration publication protection is unavailable.");
|
|
1757
2243
|
const target = await deps.provisionFollowerTarget?.(envelope.registration, { existingWorkspaceBindingOnly: restoringWorkspace });
|
|
1758
2244
|
if (restoringWorkspace && !target) {
|
|
@@ -1777,29 +2263,38 @@ export function createTelegramBusLeaderEnvelopeHandler(deps) {
|
|
|
1777
2263
|
(!registeredSlot || !/^[A-Z]$/u.test(registeredSlot))) {
|
|
1778
2264
|
throw new Error("Telegram Thread slot authority is unavailable.");
|
|
1779
2265
|
}
|
|
2266
|
+
const assertRegistrationPublishable = () => {
|
|
2267
|
+
if (!registrationCurrent())
|
|
2268
|
+
throw new Error("Telegram follower registration lost leader ownership.");
|
|
2269
|
+
if (!displayCompatible())
|
|
2270
|
+
throw new Error("Thread display mode changed; update or restart this follower before connecting.");
|
|
2271
|
+
};
|
|
1780
2272
|
let publicationOpen = true;
|
|
1781
2273
|
let published;
|
|
1782
2274
|
const publish = () => {
|
|
1783
2275
|
if (!publicationOpen)
|
|
1784
2276
|
throw new Error("Telegram follower registration publication is no longer available.");
|
|
1785
2277
|
publicationOpen = false;
|
|
1786
|
-
|
|
1787
|
-
throw new Error("Telegram follower registration lost leader ownership.");
|
|
1788
|
-
if (!displayCompatible())
|
|
1789
|
-
throw new Error("Thread display mode changed; update or restart this follower before connecting.");
|
|
2278
|
+
assertRegistrationPublishable();
|
|
1790
2279
|
published = deps.followerRegistry.register({
|
|
1791
2280
|
...envelope.registration,
|
|
1792
2281
|
connectedAtMs: getNowMs(),
|
|
1793
2282
|
target: registeredTarget,
|
|
1794
2283
|
...(registeredSlot ? { slot: registeredSlot } : {}),
|
|
1795
2284
|
...((target?.threadName ?? envelope.registration.threadName)
|
|
1796
|
-
? {
|
|
2285
|
+
? {
|
|
2286
|
+
threadName: target?.threadName ??
|
|
2287
|
+
envelope.registration.threadName,
|
|
2288
|
+
}
|
|
2289
|
+
: {}),
|
|
1797
2290
|
});
|
|
1798
2291
|
};
|
|
1799
2292
|
try {
|
|
1800
2293
|
if (deps.commitFollowerRegistration)
|
|
1801
2294
|
await deps.commitFollowerRegistration({
|
|
1802
|
-
registration: envelope.registration,
|
|
2295
|
+
registration: envelope.registration,
|
|
2296
|
+
target: registeredTarget,
|
|
2297
|
+
slot: registeredSlot,
|
|
1803
2298
|
}, publish);
|
|
1804
2299
|
else
|
|
1805
2300
|
publish();
|
|
@@ -1809,10 +2304,7 @@ export function createTelegramBusLeaderEnvelopeHandler(deps) {
|
|
|
1809
2304
|
}
|
|
1810
2305
|
if (!published)
|
|
1811
2306
|
throw new Error("Telegram follower registration publication was not confirmed.");
|
|
1812
|
-
|
|
1813
|
-
throw new Error("Telegram follower registration lost leader ownership.");
|
|
1814
|
-
if (!displayCompatible())
|
|
1815
|
-
throw new Error("Thread display mode changed; update or restart this follower before connecting.");
|
|
2307
|
+
assertRegistrationPublishable();
|
|
1816
2308
|
const follower = deps.followerRegistry.get(envelope.registration.instanceId);
|
|
1817
2309
|
if (!follower)
|
|
1818
2310
|
throw new Error("Telegram follower registration changed before acknowledgement.");
|
|
@@ -1827,16 +2319,19 @@ export function createTelegramBusLeaderEnvelopeHandler(deps) {
|
|
|
1827
2319
|
requestId: envelope.requestId,
|
|
1828
2320
|
ok: true,
|
|
1829
2321
|
protocol: deps.protocolIdentity,
|
|
1830
|
-
...(registeredTarget
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
2322
|
+
...(registeredTarget
|
|
2323
|
+
? {
|
|
2324
|
+
result: {
|
|
2325
|
+
...registeredTarget,
|
|
2326
|
+
...(displayTitle !== undefined ? { displayTitle } : {}),
|
|
2327
|
+
},
|
|
2328
|
+
}
|
|
2329
|
+
: {}),
|
|
1836
2330
|
};
|
|
1837
2331
|
}
|
|
1838
2332
|
catch (error) {
|
|
1839
|
-
if (!restoringWorkspace &&
|
|
2333
|
+
if (!restoringWorkspace &&
|
|
2334
|
+
deps.runWithWorkspaceCapacity &&
|
|
1840
2335
|
error instanceof TelegramWorkspaceSlotUnavailableError)
|
|
1841
2336
|
throw error;
|
|
1842
2337
|
return {
|
|
@@ -1850,14 +2345,17 @@ export function createTelegramBusLeaderEnvelopeHandler(deps) {
|
|
|
1850
2345
|
};
|
|
1851
2346
|
}
|
|
1852
2347
|
});
|
|
1853
|
-
const register = () => deps.runWorkspaceAdmission
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
2348
|
+
const register = () => deps.runWorkspaceAdmission
|
|
2349
|
+
? deps.runWorkspaceAdmission({
|
|
2350
|
+
operationId: `follower-registration:${envelope.requestId}`,
|
|
2351
|
+
operationKind: "workspace.register-follower",
|
|
2352
|
+
scopes: [{ kind: "profile" }],
|
|
2353
|
+
}, registrationOperation)
|
|
2354
|
+
: registrationOperation();
|
|
1858
2355
|
try {
|
|
1859
2356
|
return await (!restoringWorkspace && deps.runWithWorkspaceCapacity
|
|
1860
|
-
? deps.runWithWorkspaceCapacity(register)
|
|
2357
|
+
? deps.runWithWorkspaceCapacity(register)
|
|
2358
|
+
: register());
|
|
1861
2359
|
}
|
|
1862
2360
|
catch (error) {
|
|
1863
2361
|
return {
|
|
@@ -1875,155 +2373,49 @@ export function createTelegramBusLeaderEnvelopeHandler(deps) {
|
|
|
1875
2373
|
return routeQueueHandoff(envelope);
|
|
1876
2374
|
case "follower.setThreadDisplayMode": {
|
|
1877
2375
|
const epoch = deps.getCurrentLeaderEpoch?.();
|
|
1878
|
-
const current = () => epoch !== undefined &&
|
|
1879
|
-
deps.
|
|
2376
|
+
const current = () => epoch !== undefined &&
|
|
2377
|
+
deps.getCurrentLeaderEpoch?.() === epoch &&
|
|
2378
|
+
deps.followerRegistry.get(envelope.instanceId)
|
|
2379
|
+
?.registrationGeneration === envelope.registrationGeneration;
|
|
1880
2380
|
const follower = deps.followerRegistry.get(envelope.instanceId);
|
|
1881
|
-
const requiredCapability = envelope.mode === "directory-snake" ||
|
|
2381
|
+
const requiredCapability = envelope.mode === "directory-snake" ||
|
|
2382
|
+
envelope.mode === "directory-title"
|
|
1882
2383
|
? TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT
|
|
1883
2384
|
: TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE;
|
|
1884
|
-
if (!current() ||
|
|
2385
|
+
if (!current() ||
|
|
2386
|
+
!follower?.registrationGeneration ||
|
|
2387
|
+
!deps.applyThreadDisplayMode ||
|
|
1885
2388
|
!hasTelegramBusCapability(deps.protocolIdentity, requiredCapability) ||
|
|
1886
2389
|
!hasTelegramBusCapability(follower.protocol, requiredCapability)) {
|
|
1887
|
-
return
|
|
1888
|
-
message: "Thread display settings require current registration and compatible leader authority." };
|
|
2390
|
+
return rejectTelegramBusRequest(envelope.requestId, "Thread display settings require current registration and compatible leader authority.");
|
|
1889
2391
|
}
|
|
1890
2392
|
try {
|
|
1891
2393
|
await deps.applyThreadDisplayMode(envelope.mode, current);
|
|
1892
2394
|
if (!current())
|
|
1893
2395
|
throw new Error("Thread display setting completed for a stale registration.");
|
|
1894
|
-
return {
|
|
2396
|
+
return {
|
|
2397
|
+
kind: "bus.ack",
|
|
2398
|
+
requestId: envelope.requestId,
|
|
2399
|
+
ok: true,
|
|
2400
|
+
result: { mode: envelope.mode },
|
|
2401
|
+
};
|
|
1895
2402
|
}
|
|
1896
2403
|
catch (error) {
|
|
1897
|
-
return {
|
|
1898
|
-
|
|
2404
|
+
return {
|
|
2405
|
+
kind: "bus.ack",
|
|
2406
|
+
requestId: envelope.requestId,
|
|
2407
|
+
ok: false,
|
|
2408
|
+
message: error instanceof Error
|
|
2409
|
+
? error.message
|
|
2410
|
+
: "Thread display setting was not fully applied.",
|
|
2411
|
+
};
|
|
1899
2412
|
}
|
|
1900
2413
|
}
|
|
1901
|
-
case "follower.renameThread":
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
return {
|
|
1907
|
-
kind: "bus.ack",
|
|
1908
|
-
requestId: envelope.requestId,
|
|
1909
|
-
ok: false,
|
|
1910
|
-
message: "Unknown Telegram bus follower instance.",
|
|
1911
|
-
};
|
|
1912
|
-
}
|
|
1913
|
-
if (!follower.registrationGeneration ||
|
|
1914
|
-
envelope.registrationGeneration !== follower.registrationGeneration ||
|
|
1915
|
-
follower.target?.chatId !== envelope.target.chatId ||
|
|
1916
|
-
follower.target?.threadId !== envelope.target.threadId) {
|
|
1917
|
-
return {
|
|
1918
|
-
kind: "bus.ack",
|
|
1919
|
-
requestId: envelope.requestId,
|
|
1920
|
-
ok: false,
|
|
1921
|
-
message: "Stale Telegram bus follower registration generation.",
|
|
1922
|
-
};
|
|
1923
|
-
}
|
|
1924
|
-
if (!hasTelegramBusCapability(deps.protocolIdentity, TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME) ||
|
|
1925
|
-
!hasTelegramBusCapability(follower.protocol, TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME) ||
|
|
1926
|
-
!deps.renameFollowerThread) {
|
|
1927
|
-
return {
|
|
1928
|
-
kind: "bus.ack",
|
|
1929
|
-
requestId: envelope.requestId,
|
|
1930
|
-
ok: false,
|
|
1931
|
-
message: "Telegram Workspace Thread rename is unavailable.",
|
|
1932
|
-
};
|
|
1933
|
-
}
|
|
1934
|
-
try {
|
|
1935
|
-
const result = await deps.renameFollowerThread(follower, envelope.threadName);
|
|
1936
|
-
const current = deps.followerRegistry.get(follower.instanceId);
|
|
1937
|
-
if (current?.registrationGeneration !== follower.registrationGeneration) {
|
|
1938
|
-
return {
|
|
1939
|
-
kind: "bus.ack",
|
|
1940
|
-
requestId: envelope.requestId,
|
|
1941
|
-
ok: false,
|
|
1942
|
-
message: "Stale Telegram bus follower registration generation.",
|
|
1943
|
-
};
|
|
1944
|
-
}
|
|
1945
|
-
deps.followerRegistry.register({
|
|
1946
|
-
...current,
|
|
1947
|
-
threadName: result.threadName,
|
|
1948
|
-
connectedAtMs: current.connectedAtMs,
|
|
1949
|
-
});
|
|
1950
|
-
return {
|
|
1951
|
-
kind: "bus.ack",
|
|
1952
|
-
requestId: envelope.requestId,
|
|
1953
|
-
ok: true,
|
|
1954
|
-
result,
|
|
1955
|
-
};
|
|
1956
|
-
}
|
|
1957
|
-
catch (error) {
|
|
1958
|
-
return {
|
|
1959
|
-
kind: "bus.ack",
|
|
1960
|
-
requestId: envelope.requestId,
|
|
1961
|
-
ok: false,
|
|
1962
|
-
message: error instanceof Error
|
|
1963
|
-
? error.message
|
|
1964
|
-
: "Telegram Workspace Thread rename failed.",
|
|
1965
|
-
};
|
|
1966
|
-
}
|
|
1967
|
-
});
|
|
1968
|
-
}
|
|
1969
|
-
case "follower.resetThreadName": {
|
|
1970
|
-
const registeredFollower = deps.followerRegistry.get(envelope.instanceId);
|
|
1971
|
-
return runFollowerMutation(registeredFollower ?? { instanceId: envelope.instanceId }, async () => {
|
|
1972
|
-
const follower = deps.followerRegistry.get(envelope.instanceId);
|
|
1973
|
-
if (!follower || !follower.registrationGeneration ||
|
|
1974
|
-
envelope.registrationGeneration !== follower.registrationGeneration ||
|
|
1975
|
-
follower.target?.chatId !== envelope.target.chatId ||
|
|
1976
|
-
follower.target?.threadId !== envelope.target.threadId) {
|
|
1977
|
-
return {
|
|
1978
|
-
kind: "bus.ack",
|
|
1979
|
-
requestId: envelope.requestId,
|
|
1980
|
-
ok: false,
|
|
1981
|
-
message: "Stale or unknown Telegram bus follower registration.",
|
|
1982
|
-
};
|
|
1983
|
-
}
|
|
1984
|
-
if (!hasTelegramBusCapability(deps.protocolIdentity, TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME) || !hasTelegramBusCapability(follower.protocol, TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME) || !deps.resetFollowerThreadName) {
|
|
1985
|
-
return {
|
|
1986
|
-
kind: "bus.ack",
|
|
1987
|
-
requestId: envelope.requestId,
|
|
1988
|
-
ok: false,
|
|
1989
|
-
message: "Telegram Workspace Thread reset is unavailable.",
|
|
1990
|
-
};
|
|
1991
|
-
}
|
|
1992
|
-
try {
|
|
1993
|
-
const result = await deps.resetFollowerThreadName(follower);
|
|
1994
|
-
const current = deps.followerRegistry.get(follower.instanceId);
|
|
1995
|
-
if (current?.registrationGeneration !==
|
|
1996
|
-
follower.registrationGeneration) {
|
|
1997
|
-
return {
|
|
1998
|
-
kind: "bus.ack",
|
|
1999
|
-
requestId: envelope.requestId,
|
|
2000
|
-
ok: false,
|
|
2001
|
-
message: "Stale Telegram bus follower registration generation.",
|
|
2002
|
-
};
|
|
2003
|
-
}
|
|
2004
|
-
deps.followerRegistry.register({
|
|
2005
|
-
...current,
|
|
2006
|
-
threadName: result.threadName,
|
|
2007
|
-
connectedAtMs: current.connectedAtMs,
|
|
2008
|
-
});
|
|
2009
|
-
return {
|
|
2010
|
-
kind: "bus.ack",
|
|
2011
|
-
requestId: envelope.requestId,
|
|
2012
|
-
ok: true,
|
|
2013
|
-
result,
|
|
2014
|
-
};
|
|
2015
|
-
}
|
|
2016
|
-
catch (error) {
|
|
2017
|
-
return {
|
|
2018
|
-
kind: "bus.ack",
|
|
2019
|
-
requestId: envelope.requestId,
|
|
2020
|
-
ok: false,
|
|
2021
|
-
message: error instanceof Error
|
|
2022
|
-
? error.message : "Telegram Workspace Thread reset failed.",
|
|
2023
|
-
};
|
|
2024
|
-
}
|
|
2025
|
-
});
|
|
2026
|
-
}
|
|
2414
|
+
case "follower.renameThread":
|
|
2415
|
+
return changeFollowerThreadName(envelope, "rename", deps.renameFollowerThread &&
|
|
2416
|
+
((follower) => deps.renameFollowerThread(follower, envelope.threadName)));
|
|
2417
|
+
case "follower.resetThreadName":
|
|
2418
|
+
return changeFollowerThreadName(envelope, "reset", deps.resetFollowerThreadName);
|
|
2027
2419
|
case "follower.publishSessionReplacement":
|
|
2028
2420
|
case "follower.settleSessionReplacement": {
|
|
2029
2421
|
const registeredFollower = deps.followerRegistry.get(envelope.instanceId);
|
|
@@ -2036,19 +2428,16 @@ export function createTelegramBusLeaderEnvelopeHandler(deps) {
|
|
|
2036
2428
|
const follower = deps.followerRegistry.get(envelope.instanceId);
|
|
2037
2429
|
const isCurrent = () => epoch !== undefined &&
|
|
2038
2430
|
deps.getCurrentLeaderEpoch?.() === epoch &&
|
|
2039
|
-
deps.followerRegistry.get(envelope.instanceId)
|
|
2040
|
-
envelope.registrationGeneration;
|
|
2431
|
+
deps.followerRegistry.get(envelope.instanceId)
|
|
2432
|
+
?.registrationGeneration === envelope.registrationGeneration;
|
|
2041
2433
|
if (!follower?.registrationGeneration ||
|
|
2042
|
-
follower.registrationGeneration !==
|
|
2043
|
-
|
|
2434
|
+
follower.registrationGeneration !==
|
|
2435
|
+
envelope.registrationGeneration ||
|
|
2436
|
+
!isCurrent() ||
|
|
2437
|
+
!operation ||
|
|
2044
2438
|
!hasTelegramBusCapability(deps.protocolIdentity, TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT) ||
|
|
2045
2439
|
!hasTelegramBusCapability(follower.protocol, TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT)) {
|
|
2046
|
-
return
|
|
2047
|
-
kind: "bus.ack",
|
|
2048
|
-
requestId: envelope.requestId,
|
|
2049
|
-
ok: false,
|
|
2050
|
-
message: "Telegram session replacement requires current follower registration and compatible leader authority.",
|
|
2051
|
-
};
|
|
2440
|
+
return rejectTelegramBusRequest(envelope.requestId, "Telegram session replacement requires current follower registration and compatible leader authority.");
|
|
2052
2441
|
}
|
|
2053
2442
|
try {
|
|
2054
2443
|
const committed = await operation(follower, envelope.intent, isCurrent);
|
|
@@ -2086,32 +2475,19 @@ export function createTelegramBusLeaderEnvelopeHandler(deps) {
|
|
|
2086
2475
|
return runFollowerMutation(registeredFollower ?? { instanceId: envelope.instanceId }, async () => {
|
|
2087
2476
|
const follower = deps.followerRegistry.get(envelope.instanceId);
|
|
2088
2477
|
if (!follower) {
|
|
2089
|
-
return
|
|
2090
|
-
kind: "bus.ack",
|
|
2091
|
-
requestId: envelope.requestId,
|
|
2092
|
-
ok: false,
|
|
2093
|
-
message: "Unknown Telegram bus follower instance.",
|
|
2094
|
-
};
|
|
2478
|
+
return rejectTelegramBusRequest(envelope.requestId, "Unknown Telegram bus follower instance.");
|
|
2095
2479
|
}
|
|
2096
2480
|
if (!follower.registrationGeneration ||
|
|
2097
2481
|
!envelope.registrationGeneration ||
|
|
2098
|
-
envelope.registrationGeneration !==
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
requestId: envelope.requestId,
|
|
2102
|
-
ok: false,
|
|
2103
|
-
message: "Stale Telegram bus follower registration generation.",
|
|
2104
|
-
};
|
|
2482
|
+
envelope.registrationGeneration !==
|
|
2483
|
+
follower.registrationGeneration) {
|
|
2484
|
+
return rejectTelegramBusRequest(envelope.requestId, "Stale Telegram bus follower registration generation.");
|
|
2105
2485
|
}
|
|
2106
2486
|
await deps.onFollowerDisconnected?.(follower);
|
|
2107
2487
|
const current = deps.followerRegistry.get(follower.instanceId);
|
|
2108
|
-
if (current?.registrationGeneration !==
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
requestId: envelope.requestId,
|
|
2112
|
-
ok: false,
|
|
2113
|
-
message: "Stale Telegram bus follower registration generation.",
|
|
2114
|
-
};
|
|
2488
|
+
if (current?.registrationGeneration !==
|
|
2489
|
+
follower.registrationGeneration) {
|
|
2490
|
+
return rejectTelegramBusRequest(envelope.requestId, "Stale Telegram bus follower registration generation.");
|
|
2115
2491
|
}
|
|
2116
2492
|
deps.followerRegistry.remove(follower.instanceId);
|
|
2117
2493
|
return {
|
|
@@ -2122,28 +2498,15 @@ export function createTelegramBusLeaderEnvelopeHandler(deps) {
|
|
|
2122
2498
|
});
|
|
2123
2499
|
}
|
|
2124
2500
|
case "follower.heartbeat": {
|
|
2125
|
-
const current = deps.followerRegistry.
|
|
2126
|
-
if (
|
|
2127
|
-
return
|
|
2128
|
-
kind: "bus.ack",
|
|
2129
|
-
requestId: envelope.requestId,
|
|
2130
|
-
ok: false,
|
|
2131
|
-
message: "Unknown Telegram bus follower instance.",
|
|
2132
|
-
};
|
|
2133
|
-
}
|
|
2134
|
-
if (!current.registrationGeneration ||
|
|
2135
|
-
envelope.registrationGeneration !== current.registrationGeneration) {
|
|
2136
|
-
return {
|
|
2137
|
-
kind: "bus.ack",
|
|
2138
|
-
requestId: envelope.requestId,
|
|
2139
|
-
ok: false,
|
|
2140
|
-
message: "Stale Telegram bus follower registration generation.",
|
|
2141
|
-
};
|
|
2142
|
-
}
|
|
2501
|
+
const current = resolveRegisteredTelegramBusFollower(deps.followerRegistry, envelope.requestId, envelope.instanceId, envelope.registrationGeneration);
|
|
2502
|
+
if ("rejection" in current)
|
|
2503
|
+
return current.rejection;
|
|
2143
2504
|
const follower = deps.followerRegistry.heartbeat(envelope.instanceId, getNowMs());
|
|
2144
2505
|
if (follower)
|
|
2145
2506
|
observeRestoreRecipient(follower);
|
|
2146
|
-
const displayTitle = follower
|
|
2507
|
+
const displayTitle = follower
|
|
2508
|
+
? deps.getFollowerDisplayTitle?.(follower)
|
|
2509
|
+
: undefined;
|
|
2147
2510
|
return follower
|
|
2148
2511
|
? {
|
|
2149
2512
|
kind: "bus.ack",
|
|
@@ -2160,12 +2523,7 @@ export function createTelegramBusLeaderEnvelopeHandler(deps) {
|
|
|
2160
2523
|
.sort(),
|
|
2161
2524
|
},
|
|
2162
2525
|
}
|
|
2163
|
-
:
|
|
2164
|
-
kind: "bus.ack",
|
|
2165
|
-
requestId: envelope.requestId,
|
|
2166
|
-
ok: false,
|
|
2167
|
-
message: "Unknown Telegram bus follower instance.",
|
|
2168
|
-
};
|
|
2526
|
+
: rejectTelegramBusRequest(envelope.requestId, "Unknown Telegram bus follower instance.");
|
|
2169
2527
|
}
|
|
2170
2528
|
case "follower.resolveAgentTarget":
|
|
2171
2529
|
case "follower.routeAgentMessage":
|
|
@@ -2175,15 +2533,21 @@ export function createTelegramBusLeaderEnvelopeHandler(deps) {
|
|
|
2175
2533
|
case "leader.forwardMessage":
|
|
2176
2534
|
case "leader.forwardEditedMessage":
|
|
2177
2535
|
return forwardToFollower(envelope);
|
|
2536
|
+
case "follower.deliverSelectedMenu": {
|
|
2537
|
+
const deliver = deps.selectedMenuDelivery, secret = deps.authSecret, protocol = structuredClone(deps.protocolIdentity);
|
|
2538
|
+
const current = () => !!secret &&
|
|
2539
|
+
deps.authSecret === secret &&
|
|
2540
|
+
deps.selectedMenuDelivery === deliver &&
|
|
2541
|
+
isDeepStrictEqual(deps.protocolIdentity, protocol) &&
|
|
2542
|
+
hasTelegramBusCapability(protocol, TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY);
|
|
2543
|
+
if (!deliver || !current())
|
|
2544
|
+
return rejectTelegramBusRequest(envelope.requestId, "Selected-menu delivery is unavailable.");
|
|
2545
|
+
return deliver(structuredClone(envelope), current);
|
|
2546
|
+
}
|
|
2178
2547
|
case "follower.callApi":
|
|
2179
2548
|
return handleFollowerApiCall(envelope, { ...deps, getNowMs });
|
|
2180
2549
|
default:
|
|
2181
|
-
return
|
|
2182
|
-
kind: "bus.ack",
|
|
2183
|
-
requestId: envelope.requestId,
|
|
2184
|
-
ok: false,
|
|
2185
|
-
message: "Telegram bus envelope is not handled by this leader.",
|
|
2186
|
-
};
|
|
2550
|
+
return rejectTelegramBusRequest(envelope.requestId, "Telegram bus envelope is not handled by this leader.");
|
|
2187
2551
|
}
|
|
2188
2552
|
};
|
|
2189
2553
|
}
|
|
@@ -2192,14 +2556,6 @@ function asRecord(value) {
|
|
|
2192
2556
|
? value
|
|
2193
2557
|
: undefined;
|
|
2194
2558
|
}
|
|
2195
|
-
function asInteger(value) {
|
|
2196
|
-
if (typeof value === "number" && Number.isInteger(value))
|
|
2197
|
-
return value;
|
|
2198
|
-
if (typeof value !== "string" || value.trim() === "")
|
|
2199
|
-
return undefined;
|
|
2200
|
-
const parsed = Number(value);
|
|
2201
|
-
return Number.isInteger(parsed) ? parsed : undefined;
|
|
2202
|
-
}
|
|
2203
2559
|
function getFollowerApiMethodAndBody(envelope) {
|
|
2204
2560
|
if (envelope.method === "call" || envelope.method === "callMultipart") {
|
|
2205
2561
|
return {
|
|
@@ -2242,24 +2598,10 @@ function recordFollowerApiMessageOwnership(input) {
|
|
|
2242
2598
|
}
|
|
2243
2599
|
}
|
|
2244
2600
|
async function handleFollowerApiCall(envelope, deps) {
|
|
2245
|
-
const
|
|
2246
|
-
if (
|
|
2247
|
-
return
|
|
2248
|
-
|
|
2249
|
-
requestId: envelope.requestId,
|
|
2250
|
-
ok: false,
|
|
2251
|
-
message: "Unknown Telegram bus follower instance.",
|
|
2252
|
-
};
|
|
2253
|
-
}
|
|
2254
|
-
if (!follower.registrationGeneration ||
|
|
2255
|
-
envelope.registrationGeneration !== follower.registrationGeneration) {
|
|
2256
|
-
return {
|
|
2257
|
-
kind: "bus.ack",
|
|
2258
|
-
requestId: envelope.requestId,
|
|
2259
|
-
ok: false,
|
|
2260
|
-
message: "Stale Telegram bus follower registration generation.",
|
|
2261
|
-
};
|
|
2262
|
-
}
|
|
2601
|
+
const followerResolution = resolveRegisteredTelegramBusFollower(deps.followerRegistry, envelope.requestId, envelope.instanceId, envelope.registrationGeneration);
|
|
2602
|
+
if ("rejection" in followerResolution)
|
|
2603
|
+
return followerResolution.rejection;
|
|
2604
|
+
const follower = followerResolution.follower;
|
|
2263
2605
|
deps.followerRegistry.heartbeat(envelope.instanceId, deps.getNowMs());
|
|
2264
2606
|
if (deps.authorizeFollowerApiCall &&
|
|
2265
2607
|
!deps.authorizeFollowerApiCall({
|
|
@@ -2267,20 +2609,10 @@ async function handleFollowerApiCall(envelope, deps) {
|
|
|
2267
2609
|
method: envelope.method,
|
|
2268
2610
|
args: envelope.args,
|
|
2269
2611
|
})) {
|
|
2270
|
-
return
|
|
2271
|
-
kind: "bus.ack",
|
|
2272
|
-
requestId: envelope.requestId,
|
|
2273
|
-
ok: false,
|
|
2274
|
-
message: "Telegram bus API call is not allowed for this follower.",
|
|
2275
|
-
};
|
|
2612
|
+
return rejectTelegramBusRequest(envelope.requestId, "Telegram bus API call is not allowed for this follower.");
|
|
2276
2613
|
}
|
|
2277
2614
|
if (!deps.callApi) {
|
|
2278
|
-
return
|
|
2279
|
-
kind: "bus.ack",
|
|
2280
|
-
requestId: envelope.requestId,
|
|
2281
|
-
ok: false,
|
|
2282
|
-
message: "Telegram bus leader does not expose API calling.",
|
|
2283
|
-
};
|
|
2615
|
+
return rejectTelegramBusRequest(envelope.requestId, "Telegram bus leader does not expose API calling.");
|
|
2284
2616
|
}
|
|
2285
2617
|
try {
|
|
2286
2618
|
const result = await deps.callApi(envelope.method, envelope.args);
|
|
@@ -2327,48 +2659,6 @@ async function handleFollowerApiCall(envelope, deps) {
|
|
|
2327
2659
|
};
|
|
2328
2660
|
}
|
|
2329
2661
|
}
|
|
2330
|
-
export function createTelegramBusLeaderActivationScheduler(deps) {
|
|
2331
|
-
let pending = false;
|
|
2332
|
-
return (ctx) => {
|
|
2333
|
-
if (deps.isBusPollingStarted() || pending)
|
|
2334
|
-
return;
|
|
2335
|
-
if (!deps.isBusEnabled())
|
|
2336
|
-
return;
|
|
2337
|
-
if (!deps.ownsPolling(ctx))
|
|
2338
|
-
return;
|
|
2339
|
-
pending = true;
|
|
2340
|
-
const timer = setTimeout(() => {
|
|
2341
|
-
void (async () => {
|
|
2342
|
-
try {
|
|
2343
|
-
if (deps.isBusPollingStarted())
|
|
2344
|
-
return;
|
|
2345
|
-
if (!deps.isBusEnabled())
|
|
2346
|
-
return;
|
|
2347
|
-
if (!deps.ownsPolling(ctx))
|
|
2348
|
-
return;
|
|
2349
|
-
await deps.stopClassicPolling();
|
|
2350
|
-
try {
|
|
2351
|
-
await deps.startBusLeaderPolling(ctx);
|
|
2352
|
-
deps.setBusPollingStarted(true);
|
|
2353
|
-
deps.updateStatus(ctx);
|
|
2354
|
-
deps.recordRuntimeEvent?.("bus", "Telegram bus leader mode activated", { phase: "leader-hot-switch" });
|
|
2355
|
-
}
|
|
2356
|
-
catch (error) {
|
|
2357
|
-
deps.recordRuntimeEvent?.("bus", error, {
|
|
2358
|
-
phase: "leader-hot-switch",
|
|
2359
|
-
});
|
|
2360
|
-
deps.setBusPollingStarted(false);
|
|
2361
|
-
await deps.startClassicPolling(ctx);
|
|
2362
|
-
}
|
|
2363
|
-
}
|
|
2364
|
-
finally {
|
|
2365
|
-
pending = false;
|
|
2366
|
-
}
|
|
2367
|
-
})();
|
|
2368
|
-
}, 0);
|
|
2369
|
-
timer.unref?.();
|
|
2370
|
-
};
|
|
2371
|
-
}
|
|
2372
2662
|
export function createTelegramBusLeaderRuntime(deps) {
|
|
2373
2663
|
const getNowMs = deps.getNowMs ?? Date.now;
|
|
2374
2664
|
const followerPruneIntervalMs = deps.followerPruneIntervalMs ?? 1000;
|
|
@@ -2408,7 +2698,8 @@ export function createTelegramBusLeaderRuntime(deps) {
|
|
|
2408
2698
|
const profile = deps.getTelegramProfile?.();
|
|
2409
2699
|
const isCurrent = () => pruneGeneration === expectedGeneration &&
|
|
2410
2700
|
deps.getTelegramProfile?.() === profile &&
|
|
2411
|
-
(!deps.getCurrentLeaderEpoch ||
|
|
2701
|
+
(!deps.getCurrentLeaderEpoch ||
|
|
2702
|
+
(epoch !== undefined && deps.getCurrentLeaderEpoch() === epoch));
|
|
2412
2703
|
try {
|
|
2413
2704
|
await localServer.ensureEndpoint();
|
|
2414
2705
|
}
|
|
@@ -2424,20 +2715,27 @@ export function createTelegramBusLeaderRuntime(deps) {
|
|
|
2424
2715
|
if (!isCurrent())
|
|
2425
2716
|
return;
|
|
2426
2717
|
const attempts = [...pendingPreservations.values()].map((observation) => ({
|
|
2427
|
-
follower: observation.follower,
|
|
2718
|
+
follower: observation.follower,
|
|
2719
|
+
observation,
|
|
2720
|
+
fresh: false,
|
|
2428
2721
|
}));
|
|
2429
2722
|
const removed = deps.followerRegistry.pruneStale(getNowMs(), followerStaleAfterMs);
|
|
2430
2723
|
for (const follower of removed) {
|
|
2431
2724
|
forgetPreservation(pendingPreservations.get(follower.instanceId));
|
|
2432
2725
|
let observation;
|
|
2433
|
-
if (deps.onFollowerConfirmedDeadPreserved &&
|
|
2434
|
-
|
|
2435
|
-
|
|
2726
|
+
if (deps.onFollowerConfirmedDeadPreserved &&
|
|
2727
|
+
epoch !== undefined &&
|
|
2728
|
+
follower.registrationGeneration &&
|
|
2729
|
+
Number.isSafeInteger(follower.pid) &&
|
|
2730
|
+
follower.pid > 0 &&
|
|
2731
|
+
Number.isSafeInteger(follower.target?.threadId) &&
|
|
2732
|
+
follower.target.threadId > 0) {
|
|
2436
2733
|
if (pendingPreservations.size < TELEGRAM_WORKSPACE_SLOTS.length) {
|
|
2437
2734
|
const registration = deps.followerRegistry.observeUnregistered(follower);
|
|
2438
2735
|
let current = true;
|
|
2439
2736
|
observation = {
|
|
2440
|
-
follower,
|
|
2737
|
+
follower,
|
|
2738
|
+
registration,
|
|
2441
2739
|
// Reuse exact admission authority after a lost acquisition acknowledgement.
|
|
2442
2740
|
operationId: createTelegramWorkspaceAdmissionOperationId(),
|
|
2443
2741
|
isCurrent() {
|
|
@@ -2449,7 +2747,8 @@ export function createTelegramBusLeaderRuntime(deps) {
|
|
|
2449
2747
|
}
|
|
2450
2748
|
else {
|
|
2451
2749
|
recordPruneEvent("Telegram follower preservation observation capacity exhausted; retaining binding", {
|
|
2452
|
-
phase: "follower-preservation-capacity",
|
|
2750
|
+
phase: "follower-preservation-capacity",
|
|
2751
|
+
instanceId: follower.instanceId,
|
|
2453
2752
|
});
|
|
2454
2753
|
}
|
|
2455
2754
|
}
|
|
@@ -2463,23 +2762,31 @@ export function createTelegramBusLeaderRuntime(deps) {
|
|
|
2463
2762
|
continue;
|
|
2464
2763
|
}
|
|
2465
2764
|
let processConfirmedDead = false;
|
|
2466
|
-
if (follower.pid !== undefined &&
|
|
2467
|
-
follower.pid
|
|
2765
|
+
if (follower.pid !== undefined &&
|
|
2766
|
+
Number.isSafeInteger(follower.pid) &&
|
|
2767
|
+
follower.pid > 0 &&
|
|
2768
|
+
deps.isFollowerProcessAlive) {
|
|
2468
2769
|
try {
|
|
2469
|
-
processConfirmedDead =
|
|
2770
|
+
processConfirmedDead =
|
|
2771
|
+
deps.isFollowerProcessAlive(follower.pid) === false;
|
|
2470
2772
|
}
|
|
2471
2773
|
catch (error) {
|
|
2472
2774
|
if (fresh)
|
|
2473
2775
|
recordPruneEvent(error, {
|
|
2474
|
-
phase: "follower-process-liveness",
|
|
2776
|
+
phase: "follower-process-liveness",
|
|
2777
|
+
instanceId: follower.instanceId,
|
|
2778
|
+
pid: follower.pid,
|
|
2475
2779
|
});
|
|
2476
2780
|
}
|
|
2477
2781
|
}
|
|
2478
2782
|
if (!processConfirmedDead) {
|
|
2479
2783
|
if (fresh)
|
|
2480
2784
|
recordPruneEvent("Telegram bus follower heartbeat stale; preserving thread binding", {
|
|
2481
|
-
phase: "follower-pruned",
|
|
2482
|
-
|
|
2785
|
+
phase: "follower-pruned",
|
|
2786
|
+
instanceId: follower.instanceId,
|
|
2787
|
+
processLiveness: follower.pid === undefined || !deps.isFollowerProcessAlive
|
|
2788
|
+
? "unknown"
|
|
2789
|
+
: "alive-or-unknown",
|
|
2483
2790
|
});
|
|
2484
2791
|
continue;
|
|
2485
2792
|
}
|
|
@@ -2489,23 +2796,32 @@ export function createTelegramBusLeaderRuntime(deps) {
|
|
|
2489
2796
|
}
|
|
2490
2797
|
catch (error) {
|
|
2491
2798
|
recordPruneEvent(error, {
|
|
2492
|
-
phase: "follower-confirmed-dead-cleanup-policy",
|
|
2799
|
+
phase: "follower-confirmed-dead-cleanup-policy",
|
|
2800
|
+
instanceId: follower.instanceId,
|
|
2801
|
+
pid: follower.pid,
|
|
2493
2802
|
});
|
|
2494
2803
|
}
|
|
2495
2804
|
if (!isCurrent()) {
|
|
2496
2805
|
forgetPreservation(observation);
|
|
2497
2806
|
return;
|
|
2498
2807
|
}
|
|
2499
|
-
const recordPreserved = () => recordPruneEvent("Telegram bus follower process confirmed dead; preserving thread binding", {
|
|
2500
|
-
|
|
2808
|
+
const recordPreserved = () => recordPruneEvent("Telegram bus follower process confirmed dead; preserving thread binding", {
|
|
2809
|
+
phase: "follower-confirmed-dead-preserved",
|
|
2810
|
+
instanceId: follower.instanceId,
|
|
2811
|
+
pid: follower.pid,
|
|
2812
|
+
cleanupEnabled,
|
|
2813
|
+
});
|
|
2501
2814
|
// Deferred observations authorize only non-destructive publication, never deletion retries.
|
|
2502
2815
|
if (cleanupEnabled === true && !fresh) {
|
|
2503
2816
|
forgetPreservation(observation);
|
|
2504
2817
|
recordPreserved();
|
|
2505
2818
|
continue;
|
|
2506
2819
|
}
|
|
2507
|
-
const onConfirmedDead = cleanupEnabled === true
|
|
2508
|
-
|
|
2820
|
+
const onConfirmedDead = cleanupEnabled === true
|
|
2821
|
+
? deps.onFollowerConfirmedDead
|
|
2822
|
+
: cleanupEnabled === false && observation
|
|
2823
|
+
? deps.onFollowerConfirmedDeadPreserved
|
|
2824
|
+
: undefined;
|
|
2509
2825
|
if (!onConfirmedDead) {
|
|
2510
2826
|
if (cleanupEnabled === true)
|
|
2511
2827
|
forgetPreservation(observation);
|
|
@@ -2513,15 +2829,21 @@ export function createTelegramBusLeaderRuntime(deps) {
|
|
|
2513
2829
|
recordPreserved();
|
|
2514
2830
|
continue;
|
|
2515
2831
|
}
|
|
2516
|
-
const findReplacement = () => deps.followerRegistry
|
|
2517
|
-
(
|
|
2518
|
-
(
|
|
2832
|
+
const findReplacement = () => deps.followerRegistry
|
|
2833
|
+
.list()
|
|
2834
|
+
.find((candidate) => candidate.instanceId === follower.instanceId ||
|
|
2835
|
+
(!!follower.profileKey &&
|
|
2836
|
+
candidate.profileKey === follower.profileKey) ||
|
|
2837
|
+
(!!follower.target &&
|
|
2838
|
+
candidate.target?.chatId === follower.target.chatId &&
|
|
2519
2839
|
candidate.target?.threadId === follower.target.threadId));
|
|
2520
2840
|
let settled = false;
|
|
2521
2841
|
try {
|
|
2522
2842
|
await runFollowerMutation(follower, async () => {
|
|
2523
|
-
const isDetached = () => isCurrent() &&
|
|
2524
|
-
!
|
|
2843
|
+
const isDetached = () => isCurrent() &&
|
|
2844
|
+
(!observation || observation.isCurrent()) &&
|
|
2845
|
+
!findReplacement() &&
|
|
2846
|
+
deps.isFollowerProcessAlive?.(follower.pid) === false;
|
|
2525
2847
|
if (!isDetached())
|
|
2526
2848
|
return;
|
|
2527
2849
|
if (cleanupEnabled === true)
|
|
@@ -2534,13 +2856,19 @@ export function createTelegramBusLeaderRuntime(deps) {
|
|
|
2534
2856
|
}
|
|
2535
2857
|
catch (error) {
|
|
2536
2858
|
recordPruneEvent(error, {
|
|
2537
|
-
phase: cleanupEnabled
|
|
2538
|
-
|
|
2539
|
-
|
|
2859
|
+
phase: cleanupEnabled
|
|
2860
|
+
? "follower-confirmed-dead-cleanup"
|
|
2861
|
+
: "follower-confirmed-dead-preserve",
|
|
2862
|
+
instanceId: follower.instanceId,
|
|
2863
|
+
pid: follower.pid,
|
|
2864
|
+
chatId: follower.target?.chatId,
|
|
2865
|
+
threadId: follower.target?.threadId,
|
|
2540
2866
|
});
|
|
2541
2867
|
}
|
|
2542
2868
|
finally {
|
|
2543
|
-
if (settled ||
|
|
2869
|
+
if (settled ||
|
|
2870
|
+
cleanupEnabled === true ||
|
|
2871
|
+
(observation && !observation.isCurrent())) {
|
|
2544
2872
|
forgetPreservation(observation);
|
|
2545
2873
|
}
|
|
2546
2874
|
}
|
|
@@ -2575,7 +2903,20 @@ export function createTelegramBusLeaderRuntime(deps) {
|
|
|
2575
2903
|
}, followerPruneIntervalMs);
|
|
2576
2904
|
pruneInterval.unref?.();
|
|
2577
2905
|
};
|
|
2906
|
+
const selectedMenuDelivery = deps.selectedMenuDelivery, selectedMenuProtocol = deps.protocolIdentity, selectedMenuRegistry = deps.followerRegistry, selectedMenuSecret = deps.authSecret;
|
|
2578
2907
|
const handleEnvelope = createTelegramBusLeaderEnvelopeHandler({
|
|
2908
|
+
selectedMenuDelivery: selectedMenuDelivery
|
|
2909
|
+
? (input, isCallerCurrent) => {
|
|
2910
|
+
const generation = pruneGeneration;
|
|
2911
|
+
return selectedMenuDelivery.call(deps, input, () => !!pruneInterval &&
|
|
2912
|
+
pruneGeneration === generation &&
|
|
2913
|
+
deps.selectedMenuDelivery === selectedMenuDelivery &&
|
|
2914
|
+
deps.protocolIdentity === selectedMenuProtocol &&
|
|
2915
|
+
deps.followerRegistry === selectedMenuRegistry &&
|
|
2916
|
+
deps.authSecret === selectedMenuSecret &&
|
|
2917
|
+
isCallerCurrent());
|
|
2918
|
+
}
|
|
2919
|
+
: undefined,
|
|
2579
2920
|
followerRegistry: deps.followerRegistry,
|
|
2580
2921
|
authSecret: deps.authSecret,
|
|
2581
2922
|
protocolIdentity: deps.protocolIdentity,
|
|
@@ -2586,11 +2927,13 @@ export function createTelegramBusLeaderRuntime(deps) {
|
|
|
2586
2927
|
resolveAgentTarget: deps.resolveAgentTarget,
|
|
2587
2928
|
routeAgentMessage: deps.routeAgentMessage,
|
|
2588
2929
|
routeQueueHandoff: deps.routeQueueHandoff,
|
|
2589
|
-
provisionFollowerTarget: deps.provisionFollowerTarget
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2930
|
+
provisionFollowerTarget: deps.provisionFollowerTarget
|
|
2931
|
+
? (registration, options) => {
|
|
2932
|
+
// Same-instance ownership can change in the store before registry publication.
|
|
2933
|
+
forgetPreservation(pendingPreservations.get(registration.instanceId));
|
|
2934
|
+
return deps.provisionFollowerTarget(registration, options);
|
|
2935
|
+
}
|
|
2936
|
+
: undefined,
|
|
2594
2937
|
commitFollowerRegistration: deps.commitFollowerRegistration,
|
|
2595
2938
|
onFollowerDisconnected: deps.onFollowerDisconnected,
|
|
2596
2939
|
renameFollowerThread: deps.renameFollowerThread,
|
|
@@ -2615,22 +2958,11 @@ export function createTelegramBusLeaderRuntime(deps) {
|
|
|
2615
2958
|
const recipient = deps.followerRegistry.get(input.recipientInstanceId);
|
|
2616
2959
|
if (!hasTelegramBusCapability(deps.protocolIdentity, TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF) ||
|
|
2617
2960
|
!hasTelegramBusCapability(recipient?.protocol, TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF)) {
|
|
2618
|
-
return
|
|
2619
|
-
kind: "bus.ack",
|
|
2620
|
-
requestId: input.requestId,
|
|
2621
|
-
ok: false,
|
|
2622
|
-
message: "Telegram queue handoff capability was not negotiated.",
|
|
2623
|
-
};
|
|
2961
|
+
return rejectTelegramBusRequest(input.requestId, "Telegram queue handoff capability was not negotiated.");
|
|
2624
2962
|
}
|
|
2625
2963
|
if (!recipient?.registrationGeneration ||
|
|
2626
|
-
input.recipientRegistrationGeneration !==
|
|
2627
|
-
|
|
2628
|
-
return {
|
|
2629
|
-
kind: "bus.ack",
|
|
2630
|
-
requestId: input.requestId,
|
|
2631
|
-
ok: false,
|
|
2632
|
-
message: "Stale Telegram queue handoff recipient registration generation.",
|
|
2633
|
-
};
|
|
2964
|
+
input.recipientRegistrationGeneration !== recipient.registrationGeneration) {
|
|
2965
|
+
return rejectTelegramBusRequest(input.requestId, "Stale Telegram queue handoff recipient registration generation.");
|
|
2634
2966
|
}
|
|
2635
2967
|
const recipientSocketPath = recipient.busSocketPath ??
|
|
2636
2968
|
getTelegramBusFollowerSocketPath(recipient.instanceId);
|