@llblab/pi-telegram 0.52.2 → 0.53.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +9 -7
- package/BACKLOG.md +1 -18
- package/CHANGELOG.md +15 -0
- package/README.md +7 -11
- package/api/commands.ts +3 -0
- package/dist/api/commands.d.ts +1 -1
- package/dist/lib/activity-verbosity.d.ts +1 -5
- package/dist/lib/activity-verbosity.js +15 -42
- package/dist/lib/activity.d.ts +9 -0
- package/dist/lib/activity.js +39 -10
- package/dist/lib/agent-messages.js +7 -3
- package/dist/lib/bindings.d.ts +43 -5
- package/dist/lib/bindings.js +163 -20
- package/dist/lib/bus-api.d.ts +12 -0
- package/dist/lib/bus-api.js +182 -79
- package/dist/lib/bus-follower.d.ts +111 -31
- package/dist/lib/bus-follower.js +1324 -359
- package/dist/lib/bus-leader.d.ts +41 -91
- package/dist/lib/bus-leader.js +969 -637
- package/dist/lib/bus-transport.js +16 -9
- package/dist/lib/bus.d.ts +215 -18
- package/dist/lib/bus.js +1048 -327
- package/dist/lib/channel-posts.d.ts +5 -0
- package/dist/lib/channel-posts.js +434 -261
- package/dist/lib/command-templates.d.ts +26 -5
- package/dist/lib/command-templates.js +92 -176
- package/dist/lib/commands.d.ts +168 -82
- package/dist/lib/commands.js +1409 -291
- package/dist/lib/config.d.ts +1 -8
- package/dist/lib/config.js +34 -76
- package/dist/lib/delivery.d.ts +2 -2
- package/dist/lib/delivery.js +16 -16
- package/dist/lib/extension.js +639 -171
- package/dist/lib/generative-app-worker.mjs +19 -7
- package/dist/lib/generative-apps.d.ts +0 -5
- package/dist/lib/generative-apps.js +61 -49
- package/dist/lib/inbound.d.ts +5 -0
- package/dist/lib/inbound.js +35 -59
- package/dist/lib/journal.d.ts +33 -3
- package/dist/lib/journal.js +2063 -782
- package/dist/lib/lifecycle.d.ts +2 -6
- package/dist/lib/lifecycle.js +75 -58
- package/dist/lib/locks.d.ts +29 -3
- package/dist/lib/locks.js +289 -108
- package/dist/lib/logging.js +50 -14
- package/dist/lib/media.d.ts +22 -1
- package/dist/lib/media.js +46 -28
- package/dist/lib/menu-model.d.ts +19 -5
- package/dist/lib/menu-model.js +53 -11
- package/dist/lib/menu-queue.d.ts +5 -4
- package/dist/lib/menu-queue.js +45 -63
- package/dist/lib/menu-settings.d.ts +5 -4
- package/dist/lib/menu-settings.js +114 -78
- package/dist/lib/menu-status.d.ts +1 -1
- package/dist/lib/menu-status.js +8 -25
- package/dist/lib/menu-thinking.d.ts +6 -1
- package/dist/lib/menu-thinking.js +14 -21
- package/dist/lib/menu.d.ts +8 -61
- package/dist/lib/menu.js +92 -164
- package/dist/lib/model.d.ts +1 -1
- package/dist/lib/model.js +2 -16
- package/dist/lib/outbound-attachments.d.ts +6 -27
- package/dist/lib/outbound-attachments.js +45 -38
- package/dist/lib/outbound-buttons.js +22 -16
- package/dist/lib/outbound-markup.d.ts +0 -2
- package/dist/lib/outbound-markup.js +51 -64
- package/dist/lib/outbound-voice.js +6 -3
- package/dist/lib/outbound.d.ts +6 -6
- package/dist/lib/outbound.js +65 -51
- package/dist/lib/ownership.js +6 -2
- package/dist/lib/paths.d.ts +5 -0
- package/dist/lib/paths.js +37 -21
- package/dist/lib/pi.d.ts +2 -0
- package/dist/lib/pi.js +8 -1
- package/dist/lib/polling.d.ts +21 -37
- package/dist/lib/polling.js +59 -50
- package/dist/lib/preview.js +39 -19
- package/dist/lib/process-identity.d.ts +24 -0
- package/dist/lib/process-identity.js +133 -0
- package/dist/lib/prompts.js +2 -7
- package/dist/lib/queue.d.ts +14 -16
- package/dist/lib/queue.js +252 -127
- package/dist/lib/recovery.d.ts +2 -83
- package/dist/lib/recovery.js +20 -274
- package/dist/lib/replies.d.ts +16 -24
- package/dist/lib/replies.js +99 -116
- package/dist/lib/routing.d.ts +120 -7
- package/dist/lib/routing.js +4667 -1152
- package/dist/lib/runtime.d.ts +1 -11
- package/dist/lib/sections.d.ts +4 -29
- package/dist/lib/sections.js +7 -20
- package/dist/lib/skills.js +4 -3
- package/dist/lib/status.js +86 -57
- package/dist/lib/sync.d.ts +1 -3
- package/dist/lib/sync.js +67 -43
- package/dist/lib/target.d.ts +4 -0
- package/dist/lib/target.js +18 -0
- package/dist/lib/telegram-api.d.ts +18 -13
- package/dist/lib/telegram-api.js +148 -53
- package/dist/lib/text-groups.js +4 -39
- package/dist/lib/thread-cleanup-manager.d.ts +6 -27
- package/dist/lib/thread-cleanup-manager.js +369 -186
- package/dist/lib/thread-display.d.ts +1 -2
- package/dist/lib/thread-display.js +58 -38
- package/dist/lib/thread-naming.d.ts +28 -8
- package/dist/lib/thread-naming.js +175 -66
- package/dist/lib/thread-reconciler.d.ts +42 -16
- package/dist/lib/thread-reconciler.js +90 -50
- package/dist/lib/threads.d.ts +82 -20
- package/dist/lib/threads.js +2247 -855
- package/dist/lib/time-injection.js +1 -2
- package/dist/lib/turns.js +1 -19
- package/dist/lib/updates.d.ts +169 -80
- package/dist/lib/updates.js +2418 -641
- package/dist/lib/voice.d.ts +7 -22
- package/dist/lib/voice.js +7 -22
- package/dist/lib/wire.js +1 -1
- package/dist/lib/workspace-admission.d.ts +3 -1
- package/dist/lib/workspace-admission.js +425 -211
- package/dist/lib/workspace-identity.js +8 -13
- package/dist/lib/workspace-retirement.d.ts +1 -1
- package/dist/lib/workspace-retirement.js +328 -140
- package/dist/lib/workspace-slots.js +8 -4
- package/dist/package.json +1 -1
- package/docs/architecture.md +31 -29
- package/docs/inbound.md +1 -1
- package/docs/multi-instance-bus.md +15 -3
- package/docs/outbound.md +1 -1
- package/docs/public-api.md +29 -1
- package/docs/ui-style.md +25 -8
- package/lib/activity-verbosity.ts +59 -82
- package/lib/activity.ts +146 -102
- package/lib/agent-messages.ts +12 -5
- package/lib/bindings.ts +389 -82
- package/lib/bus-api.ts +263 -102
- package/lib/bus-follower.ts +2440 -601
- package/lib/bus-leader.ts +2209 -1290
- package/lib/bus-transport.ts +46 -12
- package/lib/bus.ts +1673 -444
- package/lib/channel-posts.ts +848 -327
- package/lib/command-templates.ts +152 -265
- package/lib/commands.ts +2398 -555
- package/lib/config.ts +177 -170
- package/lib/delivery.ts +41 -29
- package/lib/extension.ts +1206 -433
- package/lib/generative-app-worker.mjs +19 -7
- package/lib/generative-apps.ts +324 -211
- package/lib/inbound.ts +95 -97
- package/lib/journal.ts +5311 -1763
- package/lib/keyboard.ts +1 -3
- package/lib/lifecycle.ts +128 -78
- package/lib/locks.ts +677 -203
- package/lib/logging.ts +147 -58
- package/lib/media.ts +82 -43
- package/lib/menu-model.ts +85 -18
- package/lib/menu-queue.ts +88 -129
- package/lib/menu-settings.ts +260 -155
- package/lib/menu-status.ts +20 -60
- package/lib/menu-thinking.ts +41 -56
- package/lib/menu.ts +112 -285
- package/lib/model.ts +8 -22
- package/lib/outbound-attachments.ts +156 -125
- package/lib/outbound-buttons.ts +76 -49
- package/lib/outbound-markup.ts +96 -94
- package/lib/outbound-voice.ts +33 -27
- package/lib/outbound.ts +128 -125
- package/lib/ownership.ts +19 -10
- package/lib/paths.ts +148 -49
- package/lib/pi.ts +35 -15
- package/lib/polling.ts +144 -147
- package/lib/preview.ts +93 -40
- package/lib/process-identity.ts +164 -0
- package/lib/prompts.ts +4 -11
- package/lib/queue.ts +516 -301
- package/lib/recovery.ts +50 -454
- package/lib/rendering.ts +1 -3
- package/lib/replies.ts +248 -209
- package/lib/routing.ts +8509 -2186
- package/lib/runtime.ts +1 -12
- package/lib/sections.ts +24 -89
- package/lib/setup.ts +1 -3
- package/lib/skills.ts +9 -4
- package/lib/status.ts +215 -110
- package/lib/sync.ts +448 -361
- package/lib/target.ts +22 -3
- package/lib/telegram-api.ts +302 -127
- package/lib/text-groups.ts +5 -39
- package/lib/thread-cleanup-manager.ts +833 -331
- package/lib/thread-display.ts +213 -97
- package/lib/thread-naming.ts +232 -87
- package/lib/thread-reconciler.ts +155 -72
- package/lib/threads.ts +5318 -1845
- package/lib/time-injection.ts +1 -4
- package/lib/turns.ts +15 -28
- package/lib/updates.ts +5184 -1612
- package/lib/voice.ts +7 -35
- package/lib/wire.ts +1 -1
- package/lib/workspace-admission.ts +996 -416
- package/lib/workspace-identity.ts +14 -17
- package/lib/workspace-retirement.ts +878 -376
- package/lib/workspace-slots.ts +22 -13
- package/package.json +1 -1
- package/scripts/check-downgrade.mjs +0 -472
- package/scripts/measure-workspace.mjs +0 -108
package/dist/lib/bus-follower.js
CHANGED
|
@@ -5,17 +5,20 @@
|
|
|
5
5
|
* heartbeat, forwarded-update receiving, and follower-routed API calls.
|
|
6
6
|
* It must not spawn Pi processes or create hidden Telegram-originated instances.
|
|
7
7
|
*/
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
8
9
|
import { basename } from "node:path";
|
|
9
10
|
import { isDeepStrictEqual } from "node:util";
|
|
11
|
+
import { getTelegramBusTransportRetryPolicy, TELEGRAM_BUS_REGISTRATION_RETRY, } from "./bus-transport.js";
|
|
12
|
+
import { createTelegramBusFollowerDeliveryIdentity, createTelegramBusForeignOwnedUpdateForwarder, createTelegramBusLocalServer, createTelegramBusRequestIdFactory, createUnauthorizedBusAck, rejectTelegramBusRequest, getTelegramBusProtocolCompatibility, getTelegramBusSocketPath, hasTelegramBusCapability, isTelegramBusEnvelopeAuthorized, parseTelegramBusEnvelope, resolveTelegramBusSocketPath, sendTelegramBusLocalEnvelope, TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT, TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_APPLY, TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_COMMAND_SET, TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SAVE, TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SETTLE, TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY, TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT, TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE, TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE, TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME, TELEGRAM_BUS_HELD_COMMAND_CAPABILITIES, TELEGRAM_BUS_LIVE_REBIND_CAPABILITIES, TelegramBusLocalAuthorityError, } from "./bus.js";
|
|
13
|
+
import { parseTelegramUpdateJournalQueueOwner, } from "./journal.js";
|
|
14
|
+
import { TELEGRAM_BUS_LEADER_STALE_HEARTBEAT_MS, } from "./locks.js";
|
|
15
|
+
import { isPiStaleContextError } from "./pi.js";
|
|
16
|
+
import { parseTelegramTarget as parseTarget, } from "./target.js";
|
|
17
|
+
import { isTelegramApiMethodRetrySafe, TelegramApiAuthorityError, TelegramApiCommitUnknownError, TelegramApiStaleTargetError, } from "./telegram-api.js";
|
|
10
18
|
import * as Threads from "./threads.js";
|
|
11
|
-
import * as WorkspaceIdentity from "./workspace-identity.js";
|
|
12
19
|
import { isWireRecord as isRecord } from "./wire.js";
|
|
13
|
-
import { parseTelegramUpdateJournalQueueOwner } from "./journal.js";
|
|
14
|
-
import { TELEGRAM_BUS_LEADER_STALE_HEARTBEAT_MS, } from "./locks.js";
|
|
15
|
-
import { isTelegramApiMethodRetrySafe, TelegramApiCommitUnknownError, TelegramApiStaleTargetError, } from "./telegram-api.js";
|
|
16
|
-
import { createTelegramBusFollowerDeliveryIdentity, createTelegramBusForeignOwnedUpdateForwarder, createTelegramBusLocalServer, createTelegramBusRequestIdFactory, createUnauthorizedBusAck, getTelegramBusProtocolCompatibility, getTelegramBusSocketPath, hasTelegramBusCapability, isTelegramBusEnvelopeAuthorized, resolveTelegramBusSocketPath, sendTelegramBusLocalEnvelope, TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME, TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE, TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT, TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT, TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE, } from "./bus.js";
|
|
17
|
-
import { getTelegramBusTransportRetryPolicy, TELEGRAM_BUS_REGISTRATION_RETRY, } from "./bus-transport.js";
|
|
18
20
|
import { createTelegramWorkspaceAdmissionOperationId, runWithTelegramWorkspaceAdmissionsAsync, } from "./workspace-admission.js";
|
|
21
|
+
import * as WorkspaceIdentity from "./workspace-identity.js";
|
|
19
22
|
const TELEGRAM_BUS_FOLLOWER_PROMOTION_GRACE_MS = 2_500;
|
|
20
23
|
const TELEGRAM_FOLLOWER_SESSION_HANDOFF_TTL_MS = 30_000;
|
|
21
24
|
const TELEGRAM_BUS_FOLLOWER_CLIENT_TIMEOUT_MS = 30_000;
|
|
@@ -86,17 +89,16 @@ export function createTelegramBusFollowerPromotionHandler(input) {
|
|
|
86
89
|
return (ctx, binding, election) => runTelegramBusFollowerWorkspaceMutation(input, "workspace.promote-follower", async () => {
|
|
87
90
|
let promotedRecord;
|
|
88
91
|
const promoted = await input.startLeader(ctx, election, async () => {
|
|
89
|
-
promotedRecord =
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
});
|
|
92
|
+
promotedRecord = await Threads.promoteTelegramFollowerBindingToLeader({
|
|
93
|
+
store: input.topicTargetStore,
|
|
94
|
+
instanceId: input.instanceId,
|
|
95
|
+
cwd: ctx.cwd,
|
|
96
|
+
sessionId: input.getSessionId?.(ctx),
|
|
97
|
+
telegramProfile: input.getActiveProfileName(),
|
|
98
|
+
target: binding.target,
|
|
99
|
+
slot: binding.slot,
|
|
100
|
+
threadName: binding.threadName,
|
|
101
|
+
});
|
|
100
102
|
if (!promotedRecord && typeof binding.target?.threadId === "number") {
|
|
101
103
|
throw new Error("Telegram follower promotion slot authority is unavailable.");
|
|
102
104
|
}
|
|
@@ -148,18 +150,23 @@ export function createTelegramBusFollowerRuntimeAssembly(ports) {
|
|
|
148
150
|
const isReadyContext = (ctx) => Boolean(readyContext &&
|
|
149
151
|
(ctx === undefined || readyContext.ctx === ctx) &&
|
|
150
152
|
ports.registrationState.getGeneration() === readyContext.generation &&
|
|
151
|
-
ports.registration.getSessionGeneration?.() ===
|
|
152
|
-
|
|
153
|
+
ports.registration.getSessionGeneration?.() ===
|
|
154
|
+
readyContext.sessionGeneration &&
|
|
155
|
+
ports.registration.getSessionId?.(readyContext.ctx) ===
|
|
156
|
+
readyContext.sessionId &&
|
|
153
157
|
ports.registration.isContextActive?.(readyContext.ctx) !== false);
|
|
154
158
|
const prepareContext = async (ctx) => {
|
|
155
159
|
const generation = ports.registrationState.getGeneration();
|
|
156
160
|
const sessionGeneration = ports.registration.getSessionGeneration?.();
|
|
157
161
|
const sessionId = ports.registration.getSessionId?.(ctx);
|
|
158
162
|
readyContext = undefined;
|
|
159
|
-
if (!generation ||
|
|
163
|
+
if (!generation ||
|
|
164
|
+
(sessionId !== undefined &&
|
|
165
|
+
ports.registrationState.getSessionId(sessionId) !== sessionId))
|
|
160
166
|
throw new Error("Telegram follower session change requires acknowledged leader registration.");
|
|
161
167
|
await ports.registration.onRegistered?.(ctx);
|
|
162
|
-
if (generation &&
|
|
168
|
+
if (generation &&
|
|
169
|
+
ports.registrationState.getGeneration() === generation &&
|
|
163
170
|
ports.registration.getSessionGeneration?.() === sessionGeneration &&
|
|
164
171
|
ports.registration.getSessionId?.(ctx) === sessionId &&
|
|
165
172
|
ports.registration.isContextActive?.(ctx) !== false)
|
|
@@ -208,7 +215,11 @@ export function createTelegramBusFollowerRuntimeAssembly(ports) {
|
|
|
208
215
|
await prepareContext(ctx);
|
|
209
216
|
},
|
|
210
217
|
};
|
|
211
|
-
return {
|
|
218
|
+
return {
|
|
219
|
+
receiver,
|
|
220
|
+
registration,
|
|
221
|
+
getReadySessionId: () => isReadyContext() ? readyContext?.sessionId : undefined,
|
|
222
|
+
};
|
|
212
223
|
}
|
|
213
224
|
/** Binds Restore to real Pi lifetime and the current authenticated transport owner, not request fields. */
|
|
214
225
|
export function createTelegramBusFollowerRestoreContextGetter(deps) {
|
|
@@ -218,42 +229,105 @@ export function createTelegramBusFollowerRestoreContextGetter(deps) {
|
|
|
218
229
|
const leader = deps.getLeaderState();
|
|
219
230
|
const secret = deps.getAuthenticatedSecret();
|
|
220
231
|
const leaderProtocol = deps.getLeaderProtocol();
|
|
221
|
-
if (!leaderProtocol ||
|
|
232
|
+
if (!leaderProtocol ||
|
|
233
|
+
!hasTelegramBusCapability(leaderProtocol, deps.capability ?? TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE))
|
|
222
234
|
return undefined;
|
|
223
|
-
if (leader.kind !== "active-elsewhere" ||
|
|
224
|
-
!
|
|
235
|
+
if (leader.kind !== "active-elsewhere" ||
|
|
236
|
+
!secret ||
|
|
237
|
+
leader.lock.busSecret !== secret ||
|
|
238
|
+
!leader.lock.instanceId ||
|
|
239
|
+
leader.lock.leaderEpoch === undefined)
|
|
225
240
|
return undefined;
|
|
226
241
|
const profileBindingKey = deps.getProfileBindingKey();
|
|
227
242
|
const operatorUserId = deps.getOperatorUserId();
|
|
228
243
|
const sessionId = deps.getSessionId(ctx);
|
|
229
244
|
const cwd = deps.getCwd(ctx);
|
|
230
245
|
const generation = deps.getGeneration();
|
|
231
|
-
if (!profileBindingKey ||
|
|
232
|
-
!
|
|
233
|
-
!
|
|
246
|
+
if (!profileBindingKey ||
|
|
247
|
+
!sessionId ||
|
|
248
|
+
!cwd ||
|
|
249
|
+
typeof operatorUserId !== "number" ||
|
|
250
|
+
!Number.isSafeInteger(operatorUserId) ||
|
|
251
|
+
operatorUserId <= 0 ||
|
|
252
|
+
!Number.isSafeInteger(generation) ||
|
|
253
|
+
generation < 0 ||
|
|
254
|
+
!deps.isContextCurrent(ctx))
|
|
234
255
|
return undefined;
|
|
235
|
-
return {
|
|
236
|
-
|
|
256
|
+
return {
|
|
257
|
+
executor: {
|
|
258
|
+
instanceId: leader.lock.instanceId,
|
|
259
|
+
leaderEpoch: String(leader.lock.leaderEpoch),
|
|
260
|
+
},
|
|
261
|
+
profileBindingKey,
|
|
262
|
+
operatorUserId,
|
|
263
|
+
sessionId,
|
|
264
|
+
cwd,
|
|
265
|
+
generation,
|
|
266
|
+
leaderProtocol: structuredClone(leaderProtocol),
|
|
267
|
+
};
|
|
237
268
|
};
|
|
238
269
|
}
|
|
239
|
-
/**
|
|
270
|
+
/** Shared canonical/local target owner and scoped live-carrier hooks; caller authenticates transport, no canonical writes or cleanup. */
|
|
240
271
|
export function createTelegramBusFollowerWorkspaceRestoreHandler(deps) {
|
|
241
|
-
|
|
272
|
+
const handler = async (input, ctx) => {
|
|
273
|
+
const scope = input.liveRebind, ports = scope && {
|
|
274
|
+
isCurrent: scope.isCurrent,
|
|
275
|
+
release: scope.release,
|
|
276
|
+
observeWork: scope.observeWork,
|
|
277
|
+
};
|
|
278
|
+
const suppliedTarget = scope?.expectedTarget, expectedTarget = suppliedTarget && { ...suppliedTarget };
|
|
279
|
+
const sourceUpdateIds = scope && [...scope.sourceUpdateIds].sort((a, b) => a - b);
|
|
280
|
+
const liveCurrent = scope && ports.isCurrent.bind(scope), release = scope && ports.release?.bind(scope), workObserver = scope && ports.observeWork?.bind(scope);
|
|
242
281
|
const captured = deps.getContextAuthority(ctx);
|
|
243
282
|
if (!captured)
|
|
244
283
|
throw new Error("Stale Telegram follower Restore context.");
|
|
245
284
|
const authority = structuredClone(captured);
|
|
246
|
-
const request = structuredClone(
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
285
|
+
const request = structuredClone({
|
|
286
|
+
operationId: input.operationId,
|
|
287
|
+
registrationGeneration: input.registrationGeneration,
|
|
288
|
+
mode: input.mode,
|
|
289
|
+
});
|
|
290
|
+
const readIntent = () => liveCurrent
|
|
291
|
+
? deps.readLiveRebindIntent?.(request.operationId, authority.profileBindingKey)
|
|
292
|
+
: deps.readRestoreIntent(request.operationId, authority.profileBindingKey);
|
|
293
|
+
let sampleWork;
|
|
294
|
+
if (suppliedTarget !== undefined && !expectedTarget)
|
|
295
|
+
throw new Error("Invalid prepared live-rebind target.");
|
|
296
|
+
if (expectedTarget &&
|
|
297
|
+
(!Number.isSafeInteger(expectedTarget.chatId) ||
|
|
298
|
+
expectedTarget.chatId !== authority.operatorUserId ||
|
|
299
|
+
!Number.isSafeInteger(expectedTarget.threadId) ||
|
|
300
|
+
expectedTarget.threadId <= 0))
|
|
301
|
+
throw new Error("Invalid prepared live-rebind target.");
|
|
302
|
+
if ((release || workObserver) &&
|
|
303
|
+
!hasTelegramBusCapability(authority.leaderProtocol, TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SETTLE))
|
|
304
|
+
throw new Error("Live-rebind settlement capability is unavailable.");
|
|
305
|
+
if (liveCurrent &&
|
|
306
|
+
!hasTelegramBusCapability(authority.leaderProtocol, TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_APPLY))
|
|
307
|
+
throw new Error("Live-rebind apply capability is unavailable.");
|
|
308
|
+
if (!authority.profileBindingKey ||
|
|
309
|
+
!authority.sessionId ||
|
|
310
|
+
!authority.cwd ||
|
|
311
|
+
!authority.executor?.instanceId ||
|
|
312
|
+
!authority.executor.leaderEpoch ||
|
|
313
|
+
!(typeof authority.generation === "string"
|
|
314
|
+
? authority.generation.length > 0
|
|
315
|
+
: Number.isSafeInteger(authority.generation) &&
|
|
316
|
+
authority.generation >= 0) ||
|
|
317
|
+
!Number.isSafeInteger(authority.operatorUserId) ||
|
|
318
|
+
authority.operatorUserId <= 0 ||
|
|
319
|
+
!request.operationId ||
|
|
320
|
+
!request.registrationGeneration ||
|
|
321
|
+
!["apply", "inspect"].includes(request.mode))
|
|
253
322
|
throw new Error("Invalid Telegram follower Restore authority.");
|
|
254
323
|
const assertCurrent = () => {
|
|
255
324
|
if (!isDeepStrictEqual(deps.getContextAuthority(ctx), authority) ||
|
|
256
|
-
|
|
325
|
+
liveCurrent?.() === false ||
|
|
326
|
+
(scope &&
|
|
327
|
+
(input.liveRebind !== scope ||
|
|
328
|
+
Object.entries(ports).some(([key, value]) => Reflect.get(scope, key) !== value))) ||
|
|
329
|
+
deps.registrationState.getGeneration() !==
|
|
330
|
+
request.registrationGeneration)
|
|
257
331
|
throw new Error("Stale Telegram follower Restore authority.");
|
|
258
332
|
};
|
|
259
333
|
assertCurrent();
|
|
@@ -261,35 +335,61 @@ export function createTelegramBusFollowerWorkspaceRestoreHandler(deps) {
|
|
|
261
335
|
throw new Error("Telegram Workspace admission authority is unavailable.");
|
|
262
336
|
const result = await runTelegramBusFollowerWorkspaceMutation(deps, "workspace.restore-follower-target", async () => {
|
|
263
337
|
assertCurrent();
|
|
264
|
-
const intent =
|
|
338
|
+
const intent = readIntent();
|
|
265
339
|
assertCurrent();
|
|
266
|
-
if (!intent ||
|
|
267
|
-
|
|
340
|
+
if (!intent ||
|
|
341
|
+
intent.request.operationId !== request.operationId ||
|
|
342
|
+
(liveCurrent
|
|
343
|
+
? !("kind" in intent) ||
|
|
344
|
+
intent.kind !== "live-rebind" ||
|
|
345
|
+
!isDeepStrictEqual([...intent.request.source.updateIds].sort((a, b) => a - b), sourceUpdateIds) ||
|
|
346
|
+
(request.mode === "apply"
|
|
347
|
+
? intent.phase !== "rebound"
|
|
348
|
+
: !["rebound", "released", "finished"].includes(intent.phase))
|
|
349
|
+
: "kind" in intent ||
|
|
350
|
+
(intent.phase !== "recipient-issued" &&
|
|
351
|
+
intent.phase !== "ready")) ||
|
|
352
|
+
(expectedTarget &&
|
|
353
|
+
!isDeepStrictEqual(intent.request.target, expectedTarget)) ||
|
|
268
354
|
!isDeepStrictEqual(intent.executor, authority.executor) ||
|
|
269
|
-
intent.operatorUserId !== authority.operatorUserId ||
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
355
|
+
intent.operatorUserId !== authority.operatorUserId ||
|
|
356
|
+
!intent.recipient ||
|
|
357
|
+
((request.mode === "apply" || liveCurrent) &&
|
|
358
|
+
(intent.recipient.kind !== "follower" ||
|
|
359
|
+
intent.recipient.instanceId !== deps.instanceId ||
|
|
360
|
+
intent.recipient.generation !==
|
|
361
|
+
request.registrationGeneration)) ||
|
|
362
|
+
intent.recipient.sessionId !== authority.sessionId ||
|
|
363
|
+
intent.request.binding.sessionId !== authority.sessionId ||
|
|
364
|
+
intent.request.binding.cwd !==
|
|
365
|
+
WorkspaceIdentity.normalizeTelegramWorkspacePath(authority.cwd))
|
|
274
366
|
throw new Error("Telegram follower Restore intent does not match this recipient.");
|
|
275
367
|
const expected = structuredClone(intent);
|
|
276
368
|
await deps.topicTargetStore.load();
|
|
277
369
|
assertCurrent();
|
|
278
370
|
const observe = (canonical) => {
|
|
279
|
-
if (!isDeepStrictEqual(
|
|
371
|
+
if (!isDeepStrictEqual(readIntent(), expected))
|
|
280
372
|
throw new Error("Telegram follower Restore intent changed.");
|
|
281
373
|
assertCurrent();
|
|
282
374
|
const { binding, target } = expected.request;
|
|
283
|
-
const bindings = (canonical.workspaceBindings ?? []).filter(value => value.bindingKey === binding.bindingKey);
|
|
375
|
+
const bindings = (canonical.workspaceBindings ?? []).filter((value) => value.bindingKey === binding.bindingKey);
|
|
284
376
|
const snapshot = canonical.threads;
|
|
285
|
-
const records = snapshot.filter(value => value.instanceId === deps.instanceId);
|
|
286
|
-
const owners = snapshot.filter(value => value.status === "active" &&
|
|
287
|
-
(value.slot === binding.slot ||
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
377
|
+
const records = snapshot.filter((value) => value.instanceId === deps.instanceId);
|
|
378
|
+
const owners = snapshot.filter((value) => value.status === "active" &&
|
|
379
|
+
(value.slot === binding.slot ||
|
|
380
|
+
isDeepStrictEqual(value.target, target)));
|
|
381
|
+
if (bindings.length !== 1 ||
|
|
382
|
+
records.length !== 1 ||
|
|
383
|
+
owners.length !== 1 ||
|
|
384
|
+
bindings[0]?.cwd !== binding.cwd ||
|
|
385
|
+
bindings[0]?.sessionId !== binding.sessionId ||
|
|
386
|
+
bindings[0]?.slot !== binding.slot ||
|
|
387
|
+
bindings[0]?.inactiveSinceMs !== undefined ||
|
|
388
|
+
!isDeepStrictEqual(bindings[0]?.target, target) ||
|
|
389
|
+
records[0]?.status !== "active" ||
|
|
291
390
|
records[0]?.owner?.kind !== "manual-follower" ||
|
|
292
|
-
records[0]?.slot !== binding.slot ||
|
|
391
|
+
records[0]?.slot !== binding.slot ||
|
|
392
|
+
!isDeepStrictEqual(records[0]?.target, target) ||
|
|
293
393
|
deps.registrationState.getSlot() !== binding.slot)
|
|
294
394
|
throw new Error("Telegram follower Restore canonical binding is not committed.");
|
|
295
395
|
const localTarget = deps.registrationState.getTarget();
|
|
@@ -298,33 +398,223 @@ export function createTelegramBusFollowerWorkspaceRestoreHandler(deps) {
|
|
|
298
398
|
throw new Error("Telegram follower Restore local target changed.");
|
|
299
399
|
assertCurrent();
|
|
300
400
|
if (!alreadyReady && request.mode === "apply") {
|
|
301
|
-
if (expected.phase !== "recipient-issued")
|
|
401
|
+
if (expected.phase !== (liveCurrent ? "rebound" : "recipient-issued"))
|
|
302
402
|
throw new Error("Telegram follower Restore readiness regressed.");
|
|
303
|
-
deps.registrationState.setRegistered(true, target, {
|
|
304
|
-
|
|
305
|
-
|
|
403
|
+
deps.registrationState.setRegistered(true, target, {
|
|
404
|
+
slot: binding.slot,
|
|
405
|
+
threadName: records[0]?.threadName,
|
|
406
|
+
generation: request.registrationGeneration,
|
|
407
|
+
leaderProtocol: deps.registrationState.getLeaderProtocol(),
|
|
408
|
+
});
|
|
306
409
|
}
|
|
307
410
|
assertCurrent();
|
|
308
411
|
const observedTarget = deps.registrationState.getTarget();
|
|
309
|
-
if (!observedTarget ||
|
|
412
|
+
if (!observedTarget ||
|
|
413
|
+
(!isDeepStrictEqual(observedTarget, target) &&
|
|
414
|
+
!isDeepStrictEqual(observedTarget, binding.target)))
|
|
310
415
|
throw new Error("Telegram follower Restore local target changed.");
|
|
311
416
|
const ready = isDeepStrictEqual(observedTarget, target);
|
|
312
417
|
assertCurrent();
|
|
313
|
-
return {
|
|
314
|
-
|
|
315
|
-
|
|
418
|
+
return {
|
|
419
|
+
operationId: request.operationId,
|
|
420
|
+
recipient: {
|
|
421
|
+
kind: "follower",
|
|
422
|
+
instanceId: deps.instanceId,
|
|
423
|
+
sessionId: authority.sessionId,
|
|
424
|
+
generation: request.registrationGeneration,
|
|
425
|
+
},
|
|
426
|
+
target: {
|
|
427
|
+
chatId: observedTarget.chatId,
|
|
428
|
+
threadId: observedTarget.threadId,
|
|
429
|
+
},
|
|
430
|
+
slot: binding.slot,
|
|
431
|
+
ready,
|
|
432
|
+
};
|
|
316
433
|
};
|
|
317
434
|
let result;
|
|
318
|
-
deps.topicTargetStore.withWorkspaceRestoreSnapshot(expected, snapshot => {
|
|
435
|
+
deps.topicTargetStore.withWorkspaceRestoreSnapshot(expected, (snapshot) => {
|
|
436
|
+
result = observe(snapshot);
|
|
437
|
+
});
|
|
319
438
|
if (!result)
|
|
320
439
|
throw new Error("Telegram follower Restore observation was not completed.");
|
|
440
|
+
if (release || workObserver) {
|
|
441
|
+
if (!("kind" in expected) ||
|
|
442
|
+
expected.kind !== "live-rebind" ||
|
|
443
|
+
expected.phase !== "released" ||
|
|
444
|
+
!result.ready ||
|
|
445
|
+
request.mode !== "inspect")
|
|
446
|
+
throw new Error("Live-rebind dispatch has no canonical released/local-applied grant.");
|
|
447
|
+
// Journal reads stay outside the canonical transaction; each barrier check reobserves only Workspace authority.
|
|
448
|
+
if (workObserver)
|
|
449
|
+
sampleWork = () => {
|
|
450
|
+
deps.topicTargetStore.withWorkspaceRestoreSnapshot(expected, (snapshot) => {
|
|
451
|
+
if (!observe(snapshot).ready)
|
|
452
|
+
throw new Error("Live-rebind work observation lost local target application.");
|
|
453
|
+
workObserver(expected.request.binding.target);
|
|
454
|
+
assertCurrent();
|
|
455
|
+
});
|
|
456
|
+
};
|
|
457
|
+
release?.(() => {
|
|
458
|
+
assertCurrent();
|
|
459
|
+
let ready = false;
|
|
460
|
+
deps.topicTargetStore.withWorkspaceRestoreSnapshot(expected, (snapshot) => {
|
|
461
|
+
ready = observe(snapshot).ready;
|
|
462
|
+
});
|
|
463
|
+
assertCurrent();
|
|
464
|
+
return ready;
|
|
465
|
+
});
|
|
466
|
+
}
|
|
321
467
|
return result;
|
|
322
468
|
});
|
|
323
469
|
assertCurrent();
|
|
324
|
-
if (!isDeepStrictEqual(deps.registrationState.getTarget(), result.target) ||
|
|
470
|
+
if (!isDeepStrictEqual(deps.registrationState.getTarget(), result.target) ||
|
|
471
|
+
deps.registrationState.getSlot() !== result.slot)
|
|
325
472
|
throw new Error("Telegram follower Restore local registration changed after admission release.");
|
|
473
|
+
// Work can change while admission releases; sample only after that await under a fresh canonical snapshot.
|
|
474
|
+
sampleWork?.();
|
|
475
|
+
assertCurrent();
|
|
326
476
|
return result;
|
|
327
477
|
};
|
|
478
|
+
return Object.assign(handler, {
|
|
479
|
+
/** Capture availability only; asserting current recipient requires canonical released/local-applied ownership. */
|
|
480
|
+
prepareLiveCommandRecipient(input, ctx) {
|
|
481
|
+
const expected = structuredClone(input), getAuthority = deps.getContextAuthority, read = deps.readLiveRebindIntent, store = deps.topicTargetStore;
|
|
482
|
+
const snapshot = store.withWorkspaceLiveRebindSnapshot, state = deps.registrationState;
|
|
483
|
+
const statePorts = {
|
|
484
|
+
getTarget: state.getTarget,
|
|
485
|
+
getSlot: state.getSlot,
|
|
486
|
+
getGeneration: state.getGeneration,
|
|
487
|
+
getLeaderProtocol: state.getLeaderProtocol,
|
|
488
|
+
};
|
|
489
|
+
const instanceId = deps.instanceId, authority = structuredClone(getAuthority.call(deps, ctx)), oldTarget = structuredClone(state.getTarget()), slot = state.getSlot();
|
|
490
|
+
if (!authority ||
|
|
491
|
+
!read ||
|
|
492
|
+
!snapshot ||
|
|
493
|
+
!instanceId ||
|
|
494
|
+
!slot ||
|
|
495
|
+
!expected.operationId?.trim() ||
|
|
496
|
+
!expected.registrationGeneration?.trim() ||
|
|
497
|
+
expected.sessionId !== authority.sessionId ||
|
|
498
|
+
expected.sourceUpdateIds.length !== 1 ||
|
|
499
|
+
!Number.isSafeInteger(expected.sourceUpdateIds[0]) ||
|
|
500
|
+
expected.sourceUpdateIds[0] < 0 ||
|
|
501
|
+
typeof authority.generation !== "number" ||
|
|
502
|
+
!Number.isSafeInteger(authority.generation) ||
|
|
503
|
+
authority.generation < 0 ||
|
|
504
|
+
!authority.profileBindingKey?.trim() ||
|
|
505
|
+
!authority.cwd?.trim() ||
|
|
506
|
+
!authority.executor?.instanceId?.trim() ||
|
|
507
|
+
!authority.executor.leaderEpoch?.trim() ||
|
|
508
|
+
!Number.isSafeInteger(authority.operatorUserId) ||
|
|
509
|
+
authority.operatorUserId <= 0 ||
|
|
510
|
+
expected.target.chatId !== authority.operatorUserId ||
|
|
511
|
+
!Number.isSafeInteger(expected.target.threadId) ||
|
|
512
|
+
expected.target.threadId <= 0 ||
|
|
513
|
+
!oldTarget ||
|
|
514
|
+
oldTarget.chatId !== expected.target.chatId ||
|
|
515
|
+
!Number.isSafeInteger(oldTarget.threadId) ||
|
|
516
|
+
oldTarget.threadId <= 0)
|
|
517
|
+
return undefined;
|
|
518
|
+
const portsCurrent = () => deps.instanceId === instanceId &&
|
|
519
|
+
deps.getContextAuthority === getAuthority &&
|
|
520
|
+
deps.readLiveRebindIntent === read &&
|
|
521
|
+
deps.topicTargetStore === store &&
|
|
522
|
+
store.withWorkspaceLiveRebindSnapshot === snapshot &&
|
|
523
|
+
deps.registrationState === state &&
|
|
524
|
+
Object.entries(statePorts).every(([key, value]) => Reflect.get(state, key) === value);
|
|
525
|
+
const isCurrent = () => {
|
|
526
|
+
try {
|
|
527
|
+
if (!portsCurrent() ||
|
|
528
|
+
!isDeepStrictEqual(getAuthority.call(deps, ctx), authority) ||
|
|
529
|
+
state.getGeneration() !== expected.registrationGeneration ||
|
|
530
|
+
state.getSlot() !== slot ||
|
|
531
|
+
!isDeepStrictEqual(state.getLeaderProtocol(), authority.leaderProtocol) ||
|
|
532
|
+
![
|
|
533
|
+
...TELEGRAM_BUS_LIVE_REBIND_CAPABILITIES,
|
|
534
|
+
...TELEGRAM_BUS_HELD_COMMAND_CAPABILITIES,
|
|
535
|
+
].every((cap) => hasTelegramBusCapability(authority.leaderProtocol, cap)) ||
|
|
536
|
+
![oldTarget, expected.target].some((target) => isDeepStrictEqual(state.getTarget(), target)))
|
|
537
|
+
return false;
|
|
538
|
+
return (portsCurrent() &&
|
|
539
|
+
isDeepStrictEqual(getAuthority.call(deps, ctx), authority) &&
|
|
540
|
+
state.getGeneration() === expected.registrationGeneration &&
|
|
541
|
+
state.getSlot() === slot &&
|
|
542
|
+
isDeepStrictEqual(state.getLeaderProtocol(), authority.leaderProtocol) &&
|
|
543
|
+
[oldTarget, expected.target].some((target) => isDeepStrictEqual(state.getTarget(), target)) &&
|
|
544
|
+
portsCurrent());
|
|
545
|
+
}
|
|
546
|
+
catch {
|
|
547
|
+
return false;
|
|
548
|
+
}
|
|
549
|
+
};
|
|
550
|
+
if (!isCurrent())
|
|
551
|
+
return undefined;
|
|
552
|
+
return {
|
|
553
|
+
isCurrent,
|
|
554
|
+
assertRecipientCurrent() {
|
|
555
|
+
if (!isCurrent())
|
|
556
|
+
throw new Error("Selected command recipient authority changed.");
|
|
557
|
+
const value = read.call(deps, expected.operationId, authority.profileBindingKey);
|
|
558
|
+
if (!value ||
|
|
559
|
+
value.kind !== "live-rebind" ||
|
|
560
|
+
value.phase !== "released" ||
|
|
561
|
+
value.cleanup ||
|
|
562
|
+
value.request.operationId !== expected.operationId ||
|
|
563
|
+
value.recipient.kind !== "follower" ||
|
|
564
|
+
value.recipient.instanceId !== instanceId ||
|
|
565
|
+
value.recipient.sessionId !== expected.sessionId ||
|
|
566
|
+
value.recipient.generation !== expected.registrationGeneration ||
|
|
567
|
+
!isDeepStrictEqual(value.executor, authority.executor) ||
|
|
568
|
+
value.operatorUserId !== authority.operatorUserId ||
|
|
569
|
+
!isDeepStrictEqual(value.request.source.updateIds, expected.sourceUpdateIds) ||
|
|
570
|
+
!isDeepStrictEqual(value.request.target, expected.target) ||
|
|
571
|
+
!isDeepStrictEqual(value.request.binding.target, oldTarget) ||
|
|
572
|
+
value.request.owner.instanceId !== instanceId ||
|
|
573
|
+
value.request.owner.owner?.kind !== "manual-follower" ||
|
|
574
|
+
value.request.owner.slot !== slot ||
|
|
575
|
+
!isDeepStrictEqual(value.request.owner.target, oldTarget) ||
|
|
576
|
+
value.request.binding.cwd !==
|
|
577
|
+
WorkspaceIdentity.normalizeTelegramWorkspacePath(authority.cwd) ||
|
|
578
|
+
value.request.binding.sessionId !== expected.sessionId ||
|
|
579
|
+
value.request.binding.slot !== slot ||
|
|
580
|
+
!isCurrent())
|
|
581
|
+
throw new Error("Selected command recipient has no exact released intent.");
|
|
582
|
+
const intent = structuredClone(value);
|
|
583
|
+
let confirmed = false;
|
|
584
|
+
snapshot.call(store, intent, (canonical) => {
|
|
585
|
+
if (!isCurrent() ||
|
|
586
|
+
!isDeepStrictEqual(read.call(deps, expected.operationId, authority.profileBindingKey), intent))
|
|
587
|
+
return;
|
|
588
|
+
const bindings = (canonical.workspaceBindings ?? []).filter((row) => row.bindingKey === intent.request.binding.bindingKey);
|
|
589
|
+
const owners = canonical.threads.filter((row) => row.status === "active" &&
|
|
590
|
+
(row.slot === slot ||
|
|
591
|
+
isDeepStrictEqual(row.target, expected.target)));
|
|
592
|
+
const row = bindings[0], owner = owners[0];
|
|
593
|
+
confirmed =
|
|
594
|
+
bindings.length === 1 &&
|
|
595
|
+
row?.sessionId === expected.sessionId &&
|
|
596
|
+
row.cwd === intent.request.binding.cwd &&
|
|
597
|
+
row.slot === slot &&
|
|
598
|
+
row.workspaceKey === intent.request.binding.workspaceKey &&
|
|
599
|
+
row.inactiveSinceMs === undefined &&
|
|
600
|
+
isDeepStrictEqual(row.target, expected.target) &&
|
|
601
|
+
owners.length === 1 &&
|
|
602
|
+
owner?.instanceId === instanceId &&
|
|
603
|
+
owner.owner?.kind === "manual-follower" &&
|
|
604
|
+
owner.slot === slot &&
|
|
605
|
+
owner.profileKey === intent.request.owner.profileKey &&
|
|
606
|
+
isDeepStrictEqual(owner.owner, intent.request.owner.owner) &&
|
|
607
|
+
isDeepStrictEqual(owner.target, expected.target) &&
|
|
608
|
+
isDeepStrictEqual(state.getTarget(), expected.target) &&
|
|
609
|
+
isCurrent() &&
|
|
610
|
+
isDeepStrictEqual(read.call(deps, expected.operationId, authority.profileBindingKey), intent);
|
|
611
|
+
});
|
|
612
|
+
if (!confirmed || !isCurrent())
|
|
613
|
+
throw new Error("Selected command canonical/local recipient changed.");
|
|
614
|
+
},
|
|
615
|
+
};
|
|
616
|
+
},
|
|
617
|
+
});
|
|
328
618
|
}
|
|
329
619
|
export function createTelegramBusFollowerClientRuntime(deps) {
|
|
330
620
|
const createRequestId = createTelegramBusRequestIdFactory(deps.instanceId);
|
|
@@ -370,22 +660,14 @@ export function createTelegramBusFollowerQueueHandoffClient(deps) {
|
|
|
370
660
|
return async (input) => {
|
|
371
661
|
const registration = await resolveTelegramBusFollowerRegistration(deps, timeoutMs);
|
|
372
662
|
const socketPath = resolveTelegramBusSocketPath(deps.socketPath);
|
|
373
|
-
const response = await
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
kind: "follower.offerQueueHandoff",
|
|
382
|
-
requestId: deps.createRequestId(),
|
|
383
|
-
auth: deps.getAuthSecret?.(),
|
|
384
|
-
instanceId: deps.instanceId,
|
|
385
|
-
registrationGeneration: registration.generation,
|
|
386
|
-
...input,
|
|
387
|
-
sentAtMs: getNowMs(),
|
|
388
|
-
},
|
|
663
|
+
const response = await sendTelegramBusOperation(socketPath, registration.remainingTimeoutMs, {
|
|
664
|
+
kind: "follower.offerQueueHandoff",
|
|
665
|
+
requestId: deps.createRequestId(),
|
|
666
|
+
auth: deps.getAuthSecret?.(),
|
|
667
|
+
instanceId: deps.instanceId,
|
|
668
|
+
registrationGeneration: registration.generation,
|
|
669
|
+
...input,
|
|
670
|
+
sentAtMs: getNowMs(),
|
|
389
671
|
});
|
|
390
672
|
const queueOwner = response?.kind === "bus.ack" && isRecord(response.result)
|
|
391
673
|
? parseTelegramUpdateJournalQueueOwner(response.result.queueOwner)
|
|
@@ -402,7 +684,8 @@ export function createTelegramBusFollowerQueueHandoffClient(deps) {
|
|
|
402
684
|
input.payload.admissionReceipts[0]?.receiptId &&
|
|
403
685
|
response.result.sourceUpdateIds.length ===
|
|
404
686
|
input.payload.admissionReceipts[0].sourceUpdateIds.length &&
|
|
405
|
-
response.result.sourceUpdateIds.every((updateId, index) => updateId ===
|
|
687
|
+
response.result.sourceUpdateIds.every((updateId, index) => updateId ===
|
|
688
|
+
input.payload.admissionReceipts[0].sourceUpdateIds[index]) &&
|
|
406
689
|
queueOwner) {
|
|
407
690
|
return {
|
|
408
691
|
status: "staged",
|
|
@@ -412,7 +695,7 @@ export function createTelegramBusFollowerQueueHandoffClient(deps) {
|
|
|
412
695
|
};
|
|
413
696
|
}
|
|
414
697
|
throw new Error(response?.kind === "bus.ack"
|
|
415
|
-
? response.message ?? "Telegram queue handoff was rejected."
|
|
698
|
+
? (response.message ?? "Telegram queue handoff was rejected.")
|
|
416
699
|
: "Telegram queue handoff did not return an acknowledgement.");
|
|
417
700
|
};
|
|
418
701
|
}
|
|
@@ -421,19 +704,11 @@ function createTelegramBusAgentMessageClient(deps) {
|
|
|
421
704
|
const timeoutMs = deps.timeoutMs ?? TELEGRAM_BUS_FOLLOWER_CLIENT_TIMEOUT_MS;
|
|
422
705
|
const request = async (envelope, requestTimeoutMs = timeoutMs) => {
|
|
423
706
|
const socketPath = resolveTelegramBusSocketPath(deps.socketPath);
|
|
424
|
-
const response = await
|
|
425
|
-
socketPath,
|
|
426
|
-
timeoutMs: requestTimeoutMs,
|
|
427
|
-
retry: getTelegramBusTransportRetryPolicy({
|
|
428
|
-
endpoint: socketPath,
|
|
429
|
-
operation: "operation",
|
|
430
|
-
}),
|
|
431
|
-
envelope,
|
|
432
|
-
});
|
|
707
|
+
const response = await sendTelegramBusOperation(socketPath, requestTimeoutMs, envelope);
|
|
433
708
|
if (response?.kind === "bus.ack" && response.ok)
|
|
434
709
|
return response.result;
|
|
435
710
|
throw new Error(response?.kind === "bus.ack"
|
|
436
|
-
? response.message ?? "Telegram bus agent message failed."
|
|
711
|
+
? (response.message ?? "Telegram bus agent message failed.")
|
|
437
712
|
: "Telegram bus agent message did not return an acknowledgement.");
|
|
438
713
|
};
|
|
439
714
|
const registrationFields = async () => {
|
|
@@ -479,6 +754,151 @@ function createTelegramBusAgentMessageClient(deps) {
|
|
|
479
754
|
},
|
|
480
755
|
};
|
|
481
756
|
}
|
|
757
|
+
/** Private selected text-effect caller; not a guarded ordinary API adapter or follower command admission grant. */
|
|
758
|
+
export function createTelegramBusFollowerSelectedMenuCaller(deps) {
|
|
759
|
+
return async (input) => {
|
|
760
|
+
let dispatched = false, method = "sendMessage";
|
|
761
|
+
try {
|
|
762
|
+
const client = { ...deps.client }, recipient = deps.recipient, ctx = input.ctx, assertAuthority = input.assertAuthority;
|
|
763
|
+
const getAuthority = recipient.getContextAuthority, getJournal = recipient.getJournalBindingKey, getProcess = recipient.getProcessIdentity;
|
|
764
|
+
const state = recipient.registrationState, statePorts = {
|
|
765
|
+
getTarget: state.getTarget,
|
|
766
|
+
getSlot: state.getSlot,
|
|
767
|
+
getGeneration: state.getGeneration,
|
|
768
|
+
getLeaderProtocol: state.getLeaderProtocol,
|
|
769
|
+
};
|
|
770
|
+
const authority = structuredClone(getAuthority.call(recipient, ctx));
|
|
771
|
+
const journalBindingKey = getJournal.call(recipient, ctx), process = structuredClone(getProcess.call(recipient)), target = structuredClone(state.getTarget()), slot = state.getSlot();
|
|
772
|
+
const protocol = structuredClone(deps.protocolIdentity), leaderProtocol = structuredClone(state.getLeaderProtocol()), auth = client.getAuthSecret?.();
|
|
773
|
+
const endpoint = resolveTelegramBusSocketPath(client.socketPath), timeoutMs = client.timeoutMs ?? TELEGRAM_BUS_FOLLOWER_CLIENT_TIMEOUT_MS;
|
|
774
|
+
if (!authority ||
|
|
775
|
+
typeof authority.generation !== "number" ||
|
|
776
|
+
typeof assertAuthority !== "function" ||
|
|
777
|
+
!slot ||
|
|
778
|
+
!auth ||
|
|
779
|
+
!Number.isFinite(timeoutMs) ||
|
|
780
|
+
timeoutMs <= 0)
|
|
781
|
+
throw new Error("Selected-menu local owner is unavailable.");
|
|
782
|
+
const decoded = parseTelegramBusEnvelope(JSON.stringify({
|
|
783
|
+
kind: "follower.deliverSelectedMenu",
|
|
784
|
+
requestId: client.createRequestId(),
|
|
785
|
+
auth,
|
|
786
|
+
instanceId: client.instanceId,
|
|
787
|
+
operationId: input.operationId,
|
|
788
|
+
registrationGeneration: input.registrationGeneration,
|
|
789
|
+
recipient: {
|
|
790
|
+
sessionId: authority.sessionId,
|
|
791
|
+
sessionGeneration: authority.generation,
|
|
792
|
+
...process,
|
|
793
|
+
profileKey: authority.profileBindingKey,
|
|
794
|
+
journalBindingKey,
|
|
795
|
+
target,
|
|
796
|
+
},
|
|
797
|
+
executor: authority.executor,
|
|
798
|
+
operatorUserId: authority.operatorUserId,
|
|
799
|
+
effect: structuredClone(input.effect),
|
|
800
|
+
sentAtMs: (client.getNowMs ?? Date.now)(),
|
|
801
|
+
}));
|
|
802
|
+
if (decoded?.kind !== "follower.deliverSelectedMenu")
|
|
803
|
+
throw new Error("Selected-menu local description is invalid.");
|
|
804
|
+
const expected = decoded;
|
|
805
|
+
method =
|
|
806
|
+
expected.effect.kind === "send-text"
|
|
807
|
+
? "sendMessage"
|
|
808
|
+
: "editMessageText";
|
|
809
|
+
const assertCurrent = (allowPending = false) => {
|
|
810
|
+
assertAuthority();
|
|
811
|
+
const local = getAuthority.call(recipient, ctx), currentGeneration = client.getRegistrationGeneration();
|
|
812
|
+
if (!isDeepStrictEqual(deps.client, client) ||
|
|
813
|
+
deps.recipient !== recipient ||
|
|
814
|
+
recipient.getContextAuthority !== getAuthority ||
|
|
815
|
+
recipient.getJournalBindingKey !== getJournal ||
|
|
816
|
+
recipient.getProcessIdentity !== getProcess ||
|
|
817
|
+
recipient.registrationState !== state ||
|
|
818
|
+
!Object.entries(statePorts).every(([key, value]) => state[key] === value) ||
|
|
819
|
+
!isDeepStrictEqual(local, authority) ||
|
|
820
|
+
getJournal.call(recipient, ctx) !== journalBindingKey ||
|
|
821
|
+
!isDeepStrictEqual(getProcess.call(recipient), process) ||
|
|
822
|
+
!isDeepStrictEqual(state.getTarget(), target) ||
|
|
823
|
+
state.getSlot() !== slot ||
|
|
824
|
+
state.getGeneration() !== expected.registrationGeneration ||
|
|
825
|
+
(!allowPending &&
|
|
826
|
+
currentGeneration !== expected.registrationGeneration) ||
|
|
827
|
+
(currentGeneration !== undefined &&
|
|
828
|
+
currentGeneration !== expected.registrationGeneration) ||
|
|
829
|
+
client.getAuthSecret?.() !== auth ||
|
|
830
|
+
resolveTelegramBusSocketPath(client.socketPath) !== endpoint ||
|
|
831
|
+
!isDeepStrictEqual(deps.protocolIdentity, protocol) ||
|
|
832
|
+
!isDeepStrictEqual(state.getLeaderProtocol(), leaderProtocol) ||
|
|
833
|
+
!getTelegramBusProtocolCompatibility({
|
|
834
|
+
local: protocol,
|
|
835
|
+
remote: leaderProtocol,
|
|
836
|
+
}).compatible ||
|
|
837
|
+
!hasTelegramBusCapability(protocol, TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY) ||
|
|
838
|
+
!hasTelegramBusCapability(leaderProtocol, TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY))
|
|
839
|
+
throw new Error("Selected-menu local authority changed.");
|
|
840
|
+
if (!isDeepStrictEqual(getAuthority.call(recipient, ctx), authority))
|
|
841
|
+
throw new Error("Selected-menu context changed after local observations.");
|
|
842
|
+
assertAuthority();
|
|
843
|
+
};
|
|
844
|
+
assertCurrent(true);
|
|
845
|
+
const registration = await resolveTelegramBusFollowerRegistration(client, timeoutMs);
|
|
846
|
+
if (registration.generation !== expected.registrationGeneration)
|
|
847
|
+
throw new Error("Selected-menu registration changed.");
|
|
848
|
+
assertCurrent();
|
|
849
|
+
dispatched = true;
|
|
850
|
+
const response = await sendTelegramBusLocalEnvelope({
|
|
851
|
+
socketPath: endpoint,
|
|
852
|
+
envelope: expected,
|
|
853
|
+
timeoutMs: registration.remainingTimeoutMs,
|
|
854
|
+
retry: { attempts: 1, delayMs: 0 },
|
|
855
|
+
assertAuthority: () => assertCurrent(),
|
|
856
|
+
});
|
|
857
|
+
assertCurrent();
|
|
858
|
+
if (response?.kind !== "bus.ack" ||
|
|
859
|
+
response.requestId !== expected.requestId ||
|
|
860
|
+
!response.ok ||
|
|
861
|
+
response.error !== undefined ||
|
|
862
|
+
(response.protocol !== undefined &&
|
|
863
|
+
!isDeepStrictEqual(response.protocol, leaderProtocol)) ||
|
|
864
|
+
!isRecord(response.result))
|
|
865
|
+
throw new Error("Selected-menu acknowledgement is unconfirmed.");
|
|
866
|
+
const result = response.result;
|
|
867
|
+
if (!isDeepStrictEqual(Object.keys(result).sort(), [
|
|
868
|
+
"effect",
|
|
869
|
+
"messageId",
|
|
870
|
+
"operationId",
|
|
871
|
+
"recipient",
|
|
872
|
+
"registrationGeneration",
|
|
873
|
+
]) ||
|
|
874
|
+
result.operationId !== expected.operationId ||
|
|
875
|
+
result.registrationGeneration !== expected.registrationGeneration ||
|
|
876
|
+
result.effect !== expected.effect.kind ||
|
|
877
|
+
!isDeepStrictEqual(result.recipient, expected.recipient) ||
|
|
878
|
+
typeof result.messageId !== "number" ||
|
|
879
|
+
!Number.isSafeInteger(result.messageId) ||
|
|
880
|
+
result.messageId <= 0 ||
|
|
881
|
+
(expected.effect.kind === "edit-text" &&
|
|
882
|
+
result.messageId !== expected.effect.messageId))
|
|
883
|
+
throw new Error("Selected-menu recipient/result is unconfirmed.");
|
|
884
|
+
assertCurrent();
|
|
885
|
+
return {
|
|
886
|
+
operationId: expected.operationId,
|
|
887
|
+
recipient: structuredClone(expected.recipient),
|
|
888
|
+
registrationGeneration: expected.registrationGeneration,
|
|
889
|
+
effect: expected.effect.kind,
|
|
890
|
+
messageId: result.messageId,
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
catch (error) {
|
|
894
|
+
if (!dispatched ||
|
|
895
|
+
(error instanceof TelegramBusLocalAuthorityError &&
|
|
896
|
+
!error.requestIssued))
|
|
897
|
+
throw new TelegramApiAuthorityError(false);
|
|
898
|
+
throw new TelegramApiCommitUnknownError(method, error);
|
|
899
|
+
}
|
|
900
|
+
};
|
|
901
|
+
}
|
|
482
902
|
export function createTelegramBusFollowerApiCaller(deps) {
|
|
483
903
|
const getNowMs = deps.getNowMs ?? Date.now;
|
|
484
904
|
const timeoutMs = deps.timeoutMs ?? TELEGRAM_BUS_FOLLOWER_CLIENT_TIMEOUT_MS;
|
|
@@ -487,23 +907,15 @@ export function createTelegramBusFollowerApiCaller(deps) {
|
|
|
487
907
|
const socketPath = resolveTelegramBusSocketPath(deps.socketPath);
|
|
488
908
|
let response;
|
|
489
909
|
try {
|
|
490
|
-
response = await
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
requestId: deps.createRequestId(),
|
|
500
|
-
auth: deps.getAuthSecret?.(),
|
|
501
|
-
instanceId: deps.instanceId,
|
|
502
|
-
registrationGeneration: registration.generation,
|
|
503
|
-
method,
|
|
504
|
-
args,
|
|
505
|
-
sentAtMs: getNowMs(),
|
|
506
|
-
},
|
|
910
|
+
response = await sendTelegramBusOperation(socketPath, registration.remainingTimeoutMs, {
|
|
911
|
+
kind: "follower.callApi",
|
|
912
|
+
requestId: deps.createRequestId(),
|
|
913
|
+
auth: deps.getAuthSecret?.(),
|
|
914
|
+
instanceId: deps.instanceId,
|
|
915
|
+
registrationGeneration: registration.generation,
|
|
916
|
+
method,
|
|
917
|
+
args,
|
|
918
|
+
sentAtMs: getNowMs(),
|
|
507
919
|
});
|
|
508
920
|
}
|
|
509
921
|
catch (error) {
|
|
@@ -550,11 +962,6 @@ async function resolveTelegramBusFollowerRegistration(deps, timeoutMs) {
|
|
|
550
962
|
}
|
|
551
963
|
throw new Error("Telegram bus follower is not registered.");
|
|
552
964
|
}
|
|
553
|
-
function isTelegramStaleContextError(error) {
|
|
554
|
-
return (error instanceof Error &&
|
|
555
|
-
(error.message.includes("stale after session") ||
|
|
556
|
-
error.message.includes("stale ctx")));
|
|
557
|
-
}
|
|
558
965
|
export function createTelegramBusFollowerSessionReplacementSuspender(deps) {
|
|
559
966
|
const getNowMs = deps.getNowMs ?? Date.now;
|
|
560
967
|
const getPid = deps.getPid ?? (() => process.pid);
|
|
@@ -661,7 +1068,9 @@ export function createTelegramBusFollowerSessionRefreshHook(deps) {
|
|
|
661
1068
|
await deps.registrationRuntime.setContext(ctx);
|
|
662
1069
|
}
|
|
663
1070
|
catch (error) {
|
|
664
|
-
deps.recordRuntimeEvent("bus", error, {
|
|
1071
|
+
deps.recordRuntimeEvent("bus", error, {
|
|
1072
|
+
phase: "follower-session-refresh",
|
|
1073
|
+
});
|
|
665
1074
|
return;
|
|
666
1075
|
}
|
|
667
1076
|
if (deps.isSessionActive && !deps.isSessionActive(ctx))
|
|
@@ -710,14 +1119,20 @@ export function createTelegramBusFollowerRegistrationState(options = {}) {
|
|
|
710
1119
|
getThreadName: () => threadName,
|
|
711
1120
|
getDisplayTitle: () => displayTitle,
|
|
712
1121
|
setDisplayTitle(title, expectedGeneration) {
|
|
713
|
-
if (!registered ||
|
|
714
|
-
!
|
|
1122
|
+
if (!registered ||
|
|
1123
|
+
!generation ||
|
|
1124
|
+
expectedGeneration !== generation ||
|
|
1125
|
+
!title.trim() ||
|
|
1126
|
+
title.length > 128 ||
|
|
1127
|
+
displayTitle === title)
|
|
715
1128
|
return false;
|
|
716
1129
|
displayTitle = title;
|
|
717
1130
|
return true;
|
|
718
1131
|
},
|
|
719
1132
|
getGeneration: () => generation,
|
|
720
|
-
getSessionId: currentSessionId => registered && generation && currentSessionId === sessionId
|
|
1133
|
+
getSessionId: (currentSessionId) => registered && generation && currentSessionId === sessionId
|
|
1134
|
+
? sessionId
|
|
1135
|
+
: undefined,
|
|
721
1136
|
beginRecovery: () => {
|
|
722
1137
|
if (activeRecoveryEpoch !== undefined)
|
|
723
1138
|
return activeRecoveryEpoch;
|
|
@@ -757,21 +1172,31 @@ export function createTelegramBusFollowerRegistrationState(options = {}) {
|
|
|
757
1172
|
eligibleElectionSlots = Array.from(new Set(slots.filter((slot) => /^[A-Z]$/.test(slot)))).sort();
|
|
758
1173
|
},
|
|
759
1174
|
setRegistered: (next, nextTarget, metadata) => {
|
|
760
|
-
const retainedDisplayTitle = next &&
|
|
1175
|
+
const retainedDisplayTitle = next &&
|
|
1176
|
+
registered &&
|
|
761
1177
|
generation === metadata?.generation &&
|
|
762
|
-
target?.chatId === nextTarget?.chatId &&
|
|
763
|
-
|
|
764
|
-
|
|
1178
|
+
target?.chatId === nextTarget?.chatId &&
|
|
1179
|
+
target?.threadId === nextTarget?.threadId
|
|
1180
|
+
? displayTitle
|
|
1181
|
+
: undefined;
|
|
1182
|
+
const retainedSessionId = next && registered && generation === metadata?.generation
|
|
1183
|
+
? sessionId
|
|
1184
|
+
: undefined;
|
|
765
1185
|
const availabilityChanged = registered !== next;
|
|
766
1186
|
registered = next;
|
|
767
1187
|
target = next ? (nextTarget ? { ...nextTarget } : undefined) : undefined;
|
|
768
1188
|
slot = next ? metadata?.slot : undefined;
|
|
769
1189
|
threadName = next ? metadata?.threadName : undefined;
|
|
770
|
-
displayTitle =
|
|
771
|
-
|
|
772
|
-
|
|
1190
|
+
displayTitle =
|
|
1191
|
+
next &&
|
|
1192
|
+
nextTarget &&
|
|
1193
|
+
metadata?.generation &&
|
|
1194
|
+
metadata.displayTitle?.trim() &&
|
|
1195
|
+
metadata.displayTitle.length <= 128
|
|
1196
|
+
? metadata.displayTitle
|
|
1197
|
+
: retainedDisplayTitle;
|
|
773
1198
|
generation = next ? metadata?.generation : undefined;
|
|
774
|
-
sessionId = next ? metadata?.sessionId ?? retainedSessionId : undefined;
|
|
1199
|
+
sessionId = next ? (metadata?.sessionId ?? retainedSessionId) : undefined;
|
|
775
1200
|
leaderProtocol =
|
|
776
1201
|
next && metadata?.leaderProtocol
|
|
777
1202
|
? {
|
|
@@ -803,7 +1228,7 @@ export function createTelegramBusFollowerHeartbeatRecoveryHandler(deps) {
|
|
|
803
1228
|
deps.updateStatus(ctx);
|
|
804
1229
|
}
|
|
805
1230
|
catch (error) {
|
|
806
|
-
if (!
|
|
1231
|
+
if (!isPiStaleContextError(error))
|
|
807
1232
|
throw error;
|
|
808
1233
|
deps.recordRuntimeEvent("bus", error, {
|
|
809
1234
|
phase: "follower-stale-context-status",
|
|
@@ -950,7 +1375,7 @@ export function createTelegramBusFollowerHeartbeatRecoveryHandler(deps) {
|
|
|
950
1375
|
catch (promotionError) {
|
|
951
1376
|
deps.setLifecyclePhase(undefined);
|
|
952
1377
|
safeUpdateStatus(ctx);
|
|
953
|
-
if (
|
|
1378
|
+
if (isPiStaleContextError(promotionError)) {
|
|
954
1379
|
deps.recordRuntimeEvent("bus", promotionError, {
|
|
955
1380
|
phase: "follower-heartbeat-stale-context",
|
|
956
1381
|
});
|
|
@@ -990,6 +1415,11 @@ export function createTelegramBusFollowerRegistrationRuntime(deps) {
|
|
|
990
1415
|
let lastKnownTarget;
|
|
991
1416
|
let lastKnownSlot;
|
|
992
1417
|
let lastKnownThreadName;
|
|
1418
|
+
// A leader result is accepted only for the same endpoint and registration generation that issued the request.
|
|
1419
|
+
const isLeaderSessionCurrent = (socketPath, generation) => activeLeaderSocketPath === socketPath &&
|
|
1420
|
+
activeRegistrationGeneration === generation &&
|
|
1421
|
+
(!deps.registrationState ||
|
|
1422
|
+
deps.registrationState.getGeneration() === generation);
|
|
993
1423
|
const stopHeartbeat = () => {
|
|
994
1424
|
if (!heartbeatInterval)
|
|
995
1425
|
return;
|
|
@@ -1031,21 +1461,13 @@ export function createTelegramBusFollowerRegistrationRuntime(deps) {
|
|
|
1031
1461
|
activeRegistrationGeneration === registrationGeneration &&
|
|
1032
1462
|
activeContext === heartbeatContext;
|
|
1033
1463
|
try {
|
|
1034
|
-
const response = await
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
envelope: {
|
|
1042
|
-
kind: "follower.heartbeat",
|
|
1043
|
-
requestId: deps.createRequestId(),
|
|
1044
|
-
auth: activeAuthSecret,
|
|
1045
|
-
instanceId: deps.instanceId,
|
|
1046
|
-
registrationGeneration,
|
|
1047
|
-
sentAtMs: getNowMs(),
|
|
1048
|
-
},
|
|
1464
|
+
const response = await sendTelegramBusOperation(leaderSocketPath, heartbeatTimeoutMs, {
|
|
1465
|
+
kind: "follower.heartbeat",
|
|
1466
|
+
requestId: deps.createRequestId(),
|
|
1467
|
+
auth: activeAuthSecret,
|
|
1468
|
+
instanceId: deps.instanceId,
|
|
1469
|
+
registrationGeneration,
|
|
1470
|
+
sentAtMs: getNowMs(),
|
|
1049
1471
|
});
|
|
1050
1472
|
if (!isCurrentHeartbeat())
|
|
1051
1473
|
return;
|
|
@@ -1133,7 +1555,8 @@ export function createTelegramBusFollowerRegistrationRuntime(deps) {
|
|
|
1133
1555
|
const attempt = { ctx, sessionGeneration, sessionId };
|
|
1134
1556
|
registrationAttempt = attempt;
|
|
1135
1557
|
const isCurrentRequest = () => registrationAttempt === attempt &&
|
|
1136
|
-
deps.getSessionGeneration?.() === sessionGeneration &&
|
|
1558
|
+
deps.getSessionGeneration?.() === sessionGeneration &&
|
|
1559
|
+
deps.getSessionId?.(ctx) === sessionId &&
|
|
1137
1560
|
deps.isContextActive?.(ctx) !== false;
|
|
1138
1561
|
const abandonOwnedRequest = async () => {
|
|
1139
1562
|
if (registrationAttempt !== attempt)
|
|
@@ -1148,6 +1571,17 @@ export function createTelegramBusFollowerRegistrationRuntime(deps) {
|
|
|
1148
1571
|
deps.setActiveAuthSecret?.(undefined);
|
|
1149
1572
|
await deps.stopReceiving?.();
|
|
1150
1573
|
};
|
|
1574
|
+
// Failed registration releases its leader session but keeps the attempt owner for caller-visible errors.
|
|
1575
|
+
const failRegistration = async (clearGeneration = false) => {
|
|
1576
|
+
stopHeartbeat();
|
|
1577
|
+
activeLeaderSocketPath = undefined;
|
|
1578
|
+
activeAuthSecret = undefined;
|
|
1579
|
+
if (clearGeneration)
|
|
1580
|
+
activeRegistrationGeneration = undefined;
|
|
1581
|
+
deps.registrationState?.setRegistered(false);
|
|
1582
|
+
deps.setActiveAuthSecret?.(undefined);
|
|
1583
|
+
await deps.stopReceiving?.();
|
|
1584
|
+
};
|
|
1151
1585
|
const pendingHandoff = options
|
|
1152
1586
|
? undefined
|
|
1153
1587
|
: getTelegramFollowerSessionHandoff();
|
|
@@ -1232,12 +1666,7 @@ export function createTelegramBusFollowerRegistrationRuntime(deps) {
|
|
|
1232
1666
|
await abandonOwnedRequest();
|
|
1233
1667
|
return false;
|
|
1234
1668
|
}
|
|
1235
|
-
|
|
1236
|
-
activeLeaderSocketPath = undefined;
|
|
1237
|
-
activeAuthSecret = undefined;
|
|
1238
|
-
deps.registrationState?.setRegistered(false);
|
|
1239
|
-
deps.setActiveAuthSecret?.(undefined);
|
|
1240
|
-
await deps.stopReceiving?.();
|
|
1669
|
+
await failRegistration();
|
|
1241
1670
|
throw error;
|
|
1242
1671
|
}
|
|
1243
1672
|
if (!isCurrentRequest()) {
|
|
@@ -1249,23 +1678,15 @@ export function createTelegramBusFollowerRegistrationRuntime(deps) {
|
|
|
1249
1678
|
remote: response?.kind === "bus.ack" ? response.protocol : undefined,
|
|
1250
1679
|
});
|
|
1251
1680
|
// An identity-less error ACK is a rejection/transport failure, not evidence of an incompatible leader.
|
|
1252
|
-
const unidentifiedRejection = response?.kind === "bus.ack" &&
|
|
1681
|
+
const unidentifiedRejection = response?.kind === "bus.ack" &&
|
|
1682
|
+
!response.ok &&
|
|
1683
|
+
response.protocol === undefined;
|
|
1253
1684
|
if (!compatibility.compatible && !unidentifiedRejection) {
|
|
1254
|
-
|
|
1255
|
-
activeLeaderSocketPath = undefined;
|
|
1256
|
-
activeAuthSecret = undefined;
|
|
1257
|
-
deps.registrationState?.setRegistered(false);
|
|
1258
|
-
deps.setActiveAuthSecret?.(undefined);
|
|
1259
|
-
await deps.stopReceiving?.();
|
|
1685
|
+
await failRegistration();
|
|
1260
1686
|
throw new Error(`Incompatible Telegram bus leader protocol: ${compatibility.reason}.`);
|
|
1261
1687
|
}
|
|
1262
1688
|
if (response?.kind === "bus.ack" && !response.ok) {
|
|
1263
|
-
|
|
1264
|
-
activeLeaderSocketPath = undefined;
|
|
1265
|
-
activeAuthSecret = undefined;
|
|
1266
|
-
deps.registrationState?.setRegistered(false);
|
|
1267
|
-
deps.setActiveAuthSecret?.(undefined);
|
|
1268
|
-
await deps.stopReceiving?.();
|
|
1689
|
+
await failRegistration();
|
|
1269
1690
|
if (registrationOptions?.restoreWorkspace &&
|
|
1270
1691
|
response.error?.code === "workspace-binding-unavailable") {
|
|
1271
1692
|
deps.recordRuntimeEvent?.("bus", "Telegram follower auto-connect refused by leader: Workspace binding unavailable.", {
|
|
@@ -1283,9 +1704,7 @@ export function createTelegramBusFollowerRegistrationRuntime(deps) {
|
|
|
1283
1704
|
...registrationResult,
|
|
1284
1705
|
generation: registrationGeneration,
|
|
1285
1706
|
sessionId,
|
|
1286
|
-
...(response.protocol
|
|
1287
|
-
? { leaderProtocol: response.protocol }
|
|
1288
|
-
: {}),
|
|
1707
|
+
...(response.protocol ? { leaderProtocol: response.protocol } : {}),
|
|
1289
1708
|
});
|
|
1290
1709
|
lastKnownTarget = registrationResult.target;
|
|
1291
1710
|
lastKnownSlot = registrationResult.slot;
|
|
@@ -1301,13 +1720,7 @@ export function createTelegramBusFollowerRegistrationRuntime(deps) {
|
|
|
1301
1720
|
await abandonOwnedRequest();
|
|
1302
1721
|
return false;
|
|
1303
1722
|
}
|
|
1304
|
-
|
|
1305
|
-
activeLeaderSocketPath = undefined;
|
|
1306
|
-
activeAuthSecret = undefined;
|
|
1307
|
-
activeRegistrationGeneration = undefined;
|
|
1308
|
-
deps.registrationState?.setRegistered(false);
|
|
1309
|
-
deps.setActiveAuthSecret?.(undefined);
|
|
1310
|
-
await deps.stopReceiving?.();
|
|
1723
|
+
await failRegistration(true);
|
|
1311
1724
|
throw error;
|
|
1312
1725
|
}
|
|
1313
1726
|
if (!isCurrentRequest()) {
|
|
@@ -1327,18 +1740,15 @@ export function createTelegramBusFollowerRegistrationRuntime(deps) {
|
|
|
1327
1740
|
}
|
|
1328
1741
|
return true;
|
|
1329
1742
|
}
|
|
1330
|
-
|
|
1331
|
-
activeLeaderSocketPath = undefined;
|
|
1332
|
-
activeAuthSecret = undefined;
|
|
1333
|
-
deps.registrationState?.setRegistered(false);
|
|
1334
|
-
deps.setActiveAuthSecret?.(undefined);
|
|
1335
|
-
await deps.stopReceiving?.();
|
|
1743
|
+
await failRegistration();
|
|
1336
1744
|
return false;
|
|
1337
1745
|
},
|
|
1338
1746
|
setContext(ctx) {
|
|
1339
|
-
if (registrationAttempt &&
|
|
1340
|
-
registrationAttempt.
|
|
1341
|
-
|
|
1747
|
+
if (registrationAttempt &&
|
|
1748
|
+
(registrationAttempt.ctx !== ctx ||
|
|
1749
|
+
registrationAttempt.sessionGeneration !==
|
|
1750
|
+
deps.getSessionGeneration?.() ||
|
|
1751
|
+
registrationAttempt.sessionId !== deps.getSessionId?.(ctx)))
|
|
1342
1752
|
registrationAttempt = undefined;
|
|
1343
1753
|
activeContext = ctx;
|
|
1344
1754
|
},
|
|
@@ -1346,7 +1756,9 @@ export function createTelegramBusFollowerRegistrationRuntime(deps) {
|
|
|
1346
1756
|
const socketPath = activeLeaderSocketPath;
|
|
1347
1757
|
const generation = activeRegistrationGeneration;
|
|
1348
1758
|
const auth = activeAuthSecret;
|
|
1349
|
-
if (!socketPath ||
|
|
1759
|
+
if (!socketPath ||
|
|
1760
|
+
!generation ||
|
|
1761
|
+
!deps.registrationState?.isRegistered()) {
|
|
1350
1762
|
throw new Error("Telegram follower is not registered with the leader.");
|
|
1351
1763
|
}
|
|
1352
1764
|
const requiredCapability = mode === "directory-snake" || mode === "directory-title"
|
|
@@ -1358,20 +1770,31 @@ export function createTelegramBusFollowerRegistrationRuntime(deps) {
|
|
|
1358
1770
|
}
|
|
1359
1771
|
const requestId = deps.createRequestId();
|
|
1360
1772
|
const response = await sendTelegramBusLocalEnvelope({
|
|
1361
|
-
socketPath,
|
|
1773
|
+
socketPath,
|
|
1774
|
+
timeoutMs: registrationTimeoutMs,
|
|
1362
1775
|
envelope: {
|
|
1363
|
-
kind: "follower.setThreadDisplayMode",
|
|
1364
|
-
|
|
1776
|
+
kind: "follower.setThreadDisplayMode",
|
|
1777
|
+
requestId,
|
|
1778
|
+
auth,
|
|
1779
|
+
instanceId: deps.instanceId,
|
|
1780
|
+
registrationGeneration: generation,
|
|
1781
|
+
mode,
|
|
1365
1782
|
},
|
|
1366
1783
|
});
|
|
1367
|
-
if (activeLeaderSocketPath !== socketPath ||
|
|
1368
|
-
|
|
1784
|
+
if (activeLeaderSocketPath !== socketPath ||
|
|
1785
|
+
activeRegistrationGeneration !== generation ||
|
|
1786
|
+
activeAuthSecret !== auth ||
|
|
1787
|
+
deps.registrationState.getGeneration() !== generation) {
|
|
1369
1788
|
throw new Error("Telegram Thread display setting completed for a stale registration.");
|
|
1370
1789
|
}
|
|
1371
|
-
if (response?.kind !== "bus.ack" ||
|
|
1372
|
-
!
|
|
1790
|
+
if (response?.kind !== "bus.ack" ||
|
|
1791
|
+
!response.ok ||
|
|
1792
|
+
response.requestId !== requestId ||
|
|
1793
|
+
!isRecord(response.result) ||
|
|
1794
|
+
response.result.mode !== mode) {
|
|
1373
1795
|
throw new Error(response?.kind === "bus.ack"
|
|
1374
|
-
? response.message ??
|
|
1796
|
+
? (response.message ??
|
|
1797
|
+
"Telegram Thread display setting was rejected.")
|
|
1375
1798
|
: "Telegram Thread display setting was not acknowledged.");
|
|
1376
1799
|
}
|
|
1377
1800
|
},
|
|
@@ -1386,34 +1809,23 @@ export function createTelegramBusFollowerRegistrationRuntime(deps) {
|
|
|
1386
1809
|
const expectedLeaderSocketPath = activeLeaderSocketPath;
|
|
1387
1810
|
const expectedRegistrationGeneration = activeRegistrationGeneration;
|
|
1388
1811
|
const expectedAuthSecret = activeAuthSecret;
|
|
1389
|
-
const response = await
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
requestId: deps.createRequestId(),
|
|
1399
|
-
auth: expectedAuthSecret,
|
|
1400
|
-
instanceId: deps.instanceId,
|
|
1401
|
-
registrationGeneration: expectedRegistrationGeneration,
|
|
1402
|
-
target,
|
|
1403
|
-
threadName,
|
|
1404
|
-
sentAtMs: getNowMs(),
|
|
1405
|
-
},
|
|
1812
|
+
const response = await sendTelegramBusOperation(expectedLeaderSocketPath, registrationTimeoutMs, {
|
|
1813
|
+
kind: "follower.renameThread",
|
|
1814
|
+
requestId: deps.createRequestId(),
|
|
1815
|
+
auth: expectedAuthSecret,
|
|
1816
|
+
instanceId: deps.instanceId,
|
|
1817
|
+
registrationGeneration: expectedRegistrationGeneration,
|
|
1818
|
+
target,
|
|
1819
|
+
threadName,
|
|
1820
|
+
sentAtMs: getNowMs(),
|
|
1406
1821
|
});
|
|
1407
1822
|
if (response?.kind !== "bus.ack" || !response.ok) {
|
|
1408
1823
|
throw new Error(response?.kind === "bus.ack"
|
|
1409
|
-
? (response.message ??
|
|
1824
|
+
? (response.message ??
|
|
1825
|
+
"Telegram Workspace Thread rename was rejected.")
|
|
1410
1826
|
: "Telegram Workspace Thread rename was not acknowledged.");
|
|
1411
1827
|
}
|
|
1412
|
-
if (
|
|
1413
|
-
activeRegistrationGeneration !== expectedRegistrationGeneration ||
|
|
1414
|
-
(deps.registrationState &&
|
|
1415
|
-
deps.registrationState.getGeneration() !==
|
|
1416
|
-
expectedRegistrationGeneration)) {
|
|
1828
|
+
if (!isLeaderSessionCurrent(expectedLeaderSocketPath, expectedRegistrationGeneration)) {
|
|
1417
1829
|
throw new Error("Telegram Workspace Thread rename completed for a stale follower registration.");
|
|
1418
1830
|
}
|
|
1419
1831
|
const renamedThreadName = response.result &&
|
|
@@ -1437,42 +1849,36 @@ export function createTelegramBusFollowerRegistrationRuntime(deps) {
|
|
|
1437
1849
|
if (!activeLeaderSocketPath || !activeRegistrationGeneration) {
|
|
1438
1850
|
throw new Error("Telegram follower is not registered with the leader.");
|
|
1439
1851
|
}
|
|
1440
|
-
if (deps.registrationState &&
|
|
1852
|
+
if (deps.registrationState &&
|
|
1853
|
+
!hasTelegramBusCapability(deps.registrationState.getLeaderProtocol(), TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME)) {
|
|
1441
1854
|
throw new Error("The active Telegram leader does not support Workspace Thread reset. Update or restart that Pi instance.");
|
|
1442
1855
|
}
|
|
1443
1856
|
const expectedLeaderSocketPath = activeLeaderSocketPath;
|
|
1444
1857
|
const expectedRegistrationGeneration = activeRegistrationGeneration;
|
|
1445
|
-
const response = await
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
kind: "follower.resetThreadName",
|
|
1454
|
-
requestId: deps.createRequestId(),
|
|
1455
|
-
auth: activeAuthSecret,
|
|
1456
|
-
instanceId: deps.instanceId,
|
|
1457
|
-
registrationGeneration: expectedRegistrationGeneration,
|
|
1458
|
-
target,
|
|
1459
|
-
sentAtMs: getNowMs(),
|
|
1460
|
-
},
|
|
1858
|
+
const response = await sendTelegramBusOperation(expectedLeaderSocketPath, registrationTimeoutMs, {
|
|
1859
|
+
kind: "follower.resetThreadName",
|
|
1860
|
+
requestId: deps.createRequestId(),
|
|
1861
|
+
auth: activeAuthSecret,
|
|
1862
|
+
instanceId: deps.instanceId,
|
|
1863
|
+
registrationGeneration: expectedRegistrationGeneration,
|
|
1864
|
+
target,
|
|
1865
|
+
sentAtMs: getNowMs(),
|
|
1461
1866
|
});
|
|
1462
|
-
const resetName = response?.kind === "bus.ack" &&
|
|
1463
|
-
response.
|
|
1867
|
+
const resetName = response?.kind === "bus.ack" &&
|
|
1868
|
+
response.ok &&
|
|
1869
|
+
response.result &&
|
|
1870
|
+
typeof response.result === "object" &&
|
|
1464
1871
|
"threadName" in response.result &&
|
|
1465
1872
|
typeof response.result.threadName === "string"
|
|
1466
|
-
? response.result.threadName
|
|
1873
|
+
? response.result.threadName
|
|
1874
|
+
: undefined;
|
|
1467
1875
|
if (!resetName) {
|
|
1468
1876
|
throw new Error(response?.kind === "bus.ack"
|
|
1469
|
-
? response.message ??
|
|
1877
|
+
? (response.message ??
|
|
1878
|
+
"Telegram Workspace Thread reset was rejected.")
|
|
1470
1879
|
: "Telegram Workspace Thread reset was not acknowledged.");
|
|
1471
1880
|
}
|
|
1472
|
-
if (
|
|
1473
|
-
activeRegistrationGeneration !== expectedRegistrationGeneration ||
|
|
1474
|
-
(deps.registrationState && deps.registrationState.getGeneration() !==
|
|
1475
|
-
expectedRegistrationGeneration)) {
|
|
1881
|
+
if (!isLeaderSessionCurrent(expectedLeaderSocketPath, expectedRegistrationGeneration)) {
|
|
1476
1882
|
throw new Error("Telegram Workspace Thread reset completed for a stale follower registration.");
|
|
1477
1883
|
}
|
|
1478
1884
|
const registrationTarget = deps.registrationState?.getTarget();
|
|
@@ -1498,38 +1904,28 @@ export function createTelegramBusFollowerRegistrationRuntime(deps) {
|
|
|
1498
1904
|
const expectedRegistrationGeneration = activeRegistrationGeneration;
|
|
1499
1905
|
const expectedAuthSecret = activeAuthSecret;
|
|
1500
1906
|
const requestId = deps.createRequestId();
|
|
1501
|
-
const response = await
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
: "follower.settleSessionReplacement",
|
|
1512
|
-
requestId,
|
|
1513
|
-
auth: expectedAuthSecret,
|
|
1514
|
-
instanceId: deps.instanceId,
|
|
1515
|
-
registrationGeneration: expectedRegistrationGeneration,
|
|
1516
|
-
intent,
|
|
1517
|
-
sentAtMs: getNowMs(),
|
|
1518
|
-
},
|
|
1907
|
+
const response = await sendTelegramBusOperation(expectedLeaderSocketPath, registrationTimeoutMs, {
|
|
1908
|
+
kind: operation === "publish"
|
|
1909
|
+
? "follower.publishSessionReplacement"
|
|
1910
|
+
: "follower.settleSessionReplacement",
|
|
1911
|
+
requestId,
|
|
1912
|
+
auth: expectedAuthSecret,
|
|
1913
|
+
instanceId: deps.instanceId,
|
|
1914
|
+
registrationGeneration: expectedRegistrationGeneration,
|
|
1915
|
+
intent,
|
|
1916
|
+
sentAtMs: getNowMs(),
|
|
1519
1917
|
});
|
|
1520
|
-
if (response?.kind !== "bus.ack" ||
|
|
1918
|
+
if (response?.kind !== "bus.ack" ||
|
|
1919
|
+
!response.ok ||
|
|
1521
1920
|
response.requestId !== requestId ||
|
|
1522
|
-
!isRecord(response.result) ||
|
|
1921
|
+
!isRecord(response.result) ||
|
|
1922
|
+
response.result.committed !== true) {
|
|
1523
1923
|
throw new Error(response?.kind === "bus.ack"
|
|
1524
1924
|
? (response.message ?? "Telegram session replacement was rejected.")
|
|
1525
1925
|
: "Telegram session replacement was not acknowledged.");
|
|
1526
1926
|
}
|
|
1527
|
-
if (
|
|
1528
|
-
|
|
1529
|
-
activeAuthSecret !== expectedAuthSecret ||
|
|
1530
|
-
(deps.registrationState &&
|
|
1531
|
-
deps.registrationState.getGeneration() !==
|
|
1532
|
-
expectedRegistrationGeneration)) {
|
|
1927
|
+
if (!isLeaderSessionCurrent(expectedLeaderSocketPath, expectedRegistrationGeneration) ||
|
|
1928
|
+
activeAuthSecret !== expectedAuthSecret) {
|
|
1533
1929
|
throw new Error("Telegram session replacement completed for a stale follower registration.");
|
|
1534
1930
|
}
|
|
1535
1931
|
return true;
|
|
@@ -1538,21 +1934,13 @@ export function createTelegramBusFollowerRegistrationRuntime(deps) {
|
|
|
1538
1934
|
if (!activeLeaderSocketPath || !activeRegistrationGeneration) {
|
|
1539
1935
|
return false;
|
|
1540
1936
|
}
|
|
1541
|
-
const response = await
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
envelope: {
|
|
1549
|
-
kind: "follower.disconnect",
|
|
1550
|
-
requestId: deps.createRequestId(),
|
|
1551
|
-
auth: activeAuthSecret,
|
|
1552
|
-
instanceId: deps.instanceId,
|
|
1553
|
-
registrationGeneration: activeRegistrationGeneration,
|
|
1554
|
-
sentAtMs: getNowMs(),
|
|
1555
|
-
},
|
|
1937
|
+
const response = await sendTelegramBusOperation(activeLeaderSocketPath, registrationTimeoutMs, {
|
|
1938
|
+
kind: "follower.disconnect",
|
|
1939
|
+
requestId: deps.createRequestId(),
|
|
1940
|
+
auth: activeAuthSecret,
|
|
1941
|
+
instanceId: deps.instanceId,
|
|
1942
|
+
registrationGeneration: activeRegistrationGeneration,
|
|
1943
|
+
sentAtMs: getNowMs(),
|
|
1556
1944
|
});
|
|
1557
1945
|
if (response?.kind === "bus.ack" && response.ok)
|
|
1558
1946
|
return true;
|
|
@@ -1570,29 +1958,50 @@ export function createTelegramBusFollowerPairedAdmission(deps) {
|
|
|
1570
1958
|
if (updates.length !== 1)
|
|
1571
1959
|
return { admitted: false };
|
|
1572
1960
|
const update = updates[0];
|
|
1573
|
-
const keys = Object.keys(update).filter((key) => key !== "update_id" &&
|
|
1961
|
+
const keys = Object.keys(update).filter((key) => key !== "update_id" &&
|
|
1962
|
+
key !== TELEGRAM_FOLLOWER_FORWARD_BATCH_POSITION_FIELD);
|
|
1574
1963
|
if (keys.length !== 1)
|
|
1575
1964
|
return { admitted: false };
|
|
1576
1965
|
const kind = keys[0];
|
|
1577
|
-
if (kind !== "message" &&
|
|
1578
|
-
kind !== "
|
|
1966
|
+
if (kind !== "message" &&
|
|
1967
|
+
kind !== "edited_message" &&
|
|
1968
|
+
kind !== "callback_query" &&
|
|
1969
|
+
kind !== "message_reaction")
|
|
1579
1970
|
return { admitted: false };
|
|
1580
1971
|
const position = update[TELEGRAM_FOLLOWER_FORWARD_BATCH_POSITION_FIELD];
|
|
1581
|
-
if (position !== undefined &&
|
|
1582
|
-
(position !== "comment" && position !== "forward")))
|
|
1972
|
+
if (position !== undefined &&
|
|
1973
|
+
(kind !== "message" || (position !== "comment" && position !== "forward")))
|
|
1583
1974
|
return { admitted: false };
|
|
1584
1975
|
const carrier = update[kind];
|
|
1585
|
-
if (!Number.isSafeInteger(update.update_id) ||
|
|
1586
|
-
|
|
1587
|
-
carrier
|
|
1976
|
+
if (!Number.isSafeInteger(update.update_id) ||
|
|
1977
|
+
update.update_id < 0 ||
|
|
1978
|
+
!isRecord(carrier) ||
|
|
1979
|
+
carrier.pi_telegram_source_update_id !== update.update_id ||
|
|
1980
|
+
carrier.sender_chat !== undefined ||
|
|
1981
|
+
carrier.actor_chat !== undefined)
|
|
1588
1982
|
return { admitted: false };
|
|
1589
1983
|
const sender = kind === "message_reaction" ? carrier.user : carrier.from;
|
|
1590
|
-
if (!isRecord(sender) ||
|
|
1591
|
-
sender.id
|
|
1984
|
+
if (!isRecord(sender) ||
|
|
1985
|
+
typeof sender.id !== "number" ||
|
|
1986
|
+
!Number.isSafeInteger(sender.id) ||
|
|
1987
|
+
sender.id <= 0 ||
|
|
1988
|
+
sender.is_bot !== false)
|
|
1592
1989
|
return { admitted: false };
|
|
1593
1990
|
return deps.configStore.withPairedUserAdmission(deps.profileName, deps.tokenSha256, sender.id, publish, deps.assertExecutionCurrent);
|
|
1594
1991
|
};
|
|
1595
1992
|
}
|
|
1993
|
+
/** One authenticated local operation request with the shared operation retry policy; no replay semantics. */
|
|
1994
|
+
function sendTelegramBusOperation(socketPath, timeoutMs, envelope) {
|
|
1995
|
+
return sendTelegramBusLocalEnvelope({
|
|
1996
|
+
socketPath,
|
|
1997
|
+
timeoutMs,
|
|
1998
|
+
envelope,
|
|
1999
|
+
retry: getTelegramBusTransportRetryPolicy({
|
|
2000
|
+
endpoint: socketPath,
|
|
2001
|
+
operation: "operation",
|
|
2002
|
+
}),
|
|
2003
|
+
});
|
|
2004
|
+
}
|
|
1596
2005
|
export function prepareTelegramBusFollowerJournaledUpdateForExecution(update, prepareForwardedMessage) {
|
|
1597
2006
|
const position = update[TELEGRAM_FOLLOWER_FORWARD_BATCH_POSITION_FIELD];
|
|
1598
2007
|
if (update.message !== undefined &&
|
|
@@ -1605,14 +2014,475 @@ export function prepareTelegramBusFollowerJournaledUpdateForExecution(update, pr
|
|
|
1605
2014
|
delete prepared[TELEGRAM_FOLLOWER_FORWARD_BATCH_POSITION_FIELD];
|
|
1606
2015
|
return prepared;
|
|
1607
2016
|
}
|
|
2017
|
+
/** Copy the captured command target for the native owner; ordinary input supplies no target constraint. */
|
|
2018
|
+
function copyLiveRebindCommandTarget(saved) {
|
|
2019
|
+
return saved.selectedCommand
|
|
2020
|
+
? { expectedTarget: { ...saved.selectedCommand.target } }
|
|
2021
|
+
: {};
|
|
2022
|
+
}
|
|
2023
|
+
/** Exact operation/recipient/source agreement between a retained save and a later request; identity only, never currency. */
|
|
2024
|
+
function matchesLiveRebindCarrier(saved, envelope) {
|
|
2025
|
+
return (saved.operationId === envelope.operationId &&
|
|
2026
|
+
saved.recipient.instanceId === envelope.recipientInstanceId &&
|
|
2027
|
+
saved.recipient.sessionId === envelope.recipientSessionId &&
|
|
2028
|
+
saved.recipient.generation === envelope.recipientRegistrationGeneration &&
|
|
2029
|
+
saved.recipient.bindingKey === envelope.recipientBindingKey &&
|
|
2030
|
+
isDeepStrictEqual(saved.sourceUpdateIds, [...envelope.sourceUpdateIds].sort((a, b) => a - b)));
|
|
2031
|
+
}
|
|
2032
|
+
/** Exact branch agreement: a command record requires its saved proof; ordinary input refuses any proof. */
|
|
2033
|
+
function matchesLiveRebindBranch(saved, status, envelope) {
|
|
2034
|
+
return (isDeepStrictEqual(saved.selectedCommand, envelope.selectedCommand) &&
|
|
2035
|
+
(status
|
|
2036
|
+
? isDeepStrictEqual(saved.preparedSource, envelope.preparedSource)
|
|
2037
|
+
: envelope.preparedSource === undefined));
|
|
2038
|
+
}
|
|
2039
|
+
/** One current warm preparation, not a prompt queue or restart recovery record. */
|
|
2040
|
+
export function createTelegramBusFollowerLiveRebindRuntime(deps) {
|
|
2041
|
+
let retained;
|
|
2042
|
+
const runtime = {
|
|
2043
|
+
save(envelope, ctx, isCurrent) {
|
|
2044
|
+
const captured = structuredClone(envelope), selectedCommand = captured.selectedCommand;
|
|
2045
|
+
const heldMarker = selectedCommand, getCommandOwner = heldMarker ? deps.getCommandOwner : undefined, applyOwner = deps.applyTarget;
|
|
2046
|
+
if (heldMarker !== undefined &&
|
|
2047
|
+
(!getCommandOwner || !applyOwner || captured.updates.length !== 1))
|
|
2048
|
+
throw new Error("Selected command native consumption is unavailable.");
|
|
2049
|
+
const admission = deps.getAdmission(ctx), prepareInput = admission?.prepareLiveInput, append = admission?.appendBatch;
|
|
2050
|
+
let status = retained?.operationId === captured.operationId
|
|
2051
|
+
? retained.status
|
|
2052
|
+
: undefined;
|
|
2053
|
+
const current = () => isCurrent() &&
|
|
2054
|
+
deps.getAdmission(ctx) === admission &&
|
|
2055
|
+
admission?.getJournalBindingKey() === captured.recipientBindingKey &&
|
|
2056
|
+
(!heldMarker ||
|
|
2057
|
+
(deps.getCommandOwner === getCommandOwner &&
|
|
2058
|
+
deps.applyTarget === applyOwner &&
|
|
2059
|
+
admission?.prepareLiveInput === prepareInput &&
|
|
2060
|
+
admission?.appendBatch === append &&
|
|
2061
|
+
!!status?.current()));
|
|
2062
|
+
if (retained?.status &&
|
|
2063
|
+
retained.operationId !== captured.operationId &&
|
|
2064
|
+
retained.outcome?.status === "released" &&
|
|
2065
|
+
(retained.status.result !== "completed" ||
|
|
2066
|
+
!retained.status.ports?.inspectCompletion.call(retained.status.plan)))
|
|
2067
|
+
throw new Error("Released held command is unconfirmed; no carrier replacement.");
|
|
2068
|
+
if (heldMarker && !status) {
|
|
2069
|
+
const { recipientInstanceId, recipientRegistrationGeneration, recipientSessionId, recipientBindingKey, operationId, updates, } = captured;
|
|
2070
|
+
const owner = getCommandOwner(structuredClone({
|
|
2071
|
+
recipientInstanceId,
|
|
2072
|
+
recipientRegistrationGeneration,
|
|
2073
|
+
recipientSessionId,
|
|
2074
|
+
recipientBindingKey,
|
|
2075
|
+
operationId,
|
|
2076
|
+
updates,
|
|
2077
|
+
selectedCommand,
|
|
2078
|
+
}), ctx);
|
|
2079
|
+
if (!owner ||
|
|
2080
|
+
typeof owner.isCurrent !== "function" ||
|
|
2081
|
+
typeof owner.prepare !== "function" ||
|
|
2082
|
+
typeof owner.assertRecipientCurrent !== "function")
|
|
2083
|
+
throw new Error("Selected command preparation owner is unavailable.");
|
|
2084
|
+
const ports = {
|
|
2085
|
+
isCurrent: owner.isCurrent,
|
|
2086
|
+
prepare: owner.prepare,
|
|
2087
|
+
assertRecipientCurrent: owner.assertRecipientCurrent,
|
|
2088
|
+
};
|
|
2089
|
+
status = {
|
|
2090
|
+
owner,
|
|
2091
|
+
current: () => Object.entries(ports).every(([key, value]) => Reflect.get(owner, key) === value) &&
|
|
2092
|
+
(!status?.plan ||
|
|
2093
|
+
Object.entries(status.ports).every(([key, value]) => Reflect.get(status.plan, key) === value)) &&
|
|
2094
|
+
ports.isCurrent.call(owner),
|
|
2095
|
+
prepare: ports.prepare,
|
|
2096
|
+
recipient: ports.assertRecipientCurrent,
|
|
2097
|
+
};
|
|
2098
|
+
}
|
|
2099
|
+
if (!admission?.prepareLiveInput || !current())
|
|
2100
|
+
throw new Error("Live-rebind recipient admission is unavailable.");
|
|
2101
|
+
const digest = createHash("sha256")
|
|
2102
|
+
.update(JSON.stringify(captured.updates))
|
|
2103
|
+
.digest("hex");
|
|
2104
|
+
if (retained?.status && !retained.current())
|
|
2105
|
+
throw new Error("Prepared command authority changed; no replacement adoption.");
|
|
2106
|
+
if (retained &&
|
|
2107
|
+
(!retained.current() ||
|
|
2108
|
+
(retained.operationId !== envelope.operationId &&
|
|
2109
|
+
(retained.outcome?.status === "released" ||
|
|
2110
|
+
retained.outcome?.status === "discarded"))))
|
|
2111
|
+
retained = undefined;
|
|
2112
|
+
if (heldMarker &&
|
|
2113
|
+
retained &&
|
|
2114
|
+
(retained.ctx !== ctx ||
|
|
2115
|
+
retained.observation.recipient.instanceId !==
|
|
2116
|
+
captured.recipientInstanceId ||
|
|
2117
|
+
retained.observation.recipient.sessionId !==
|
|
2118
|
+
captured.recipientSessionId ||
|
|
2119
|
+
retained.observation.recipient.generation !==
|
|
2120
|
+
captured.recipientRegistrationGeneration ||
|
|
2121
|
+
retained.observation.recipient.bindingKey !==
|
|
2122
|
+
captured.recipientBindingKey))
|
|
2123
|
+
throw new Error("Selected command saved recipient changed.");
|
|
2124
|
+
if (retained &&
|
|
2125
|
+
(retained.operationId !== captured.operationId ||
|
|
2126
|
+
retained.digest !== digest ||
|
|
2127
|
+
!isDeepStrictEqual(retained.observation.selectedCommand, selectedCommand)))
|
|
2128
|
+
throw new Error("Another live-rebind input or branch is already prepared.");
|
|
2129
|
+
if (!retained) {
|
|
2130
|
+
const held = admission.prepareLiveInput(ctx, captured.updates.map((update) => update.update_id), current);
|
|
2131
|
+
if (!held)
|
|
2132
|
+
throw new Error("Live-rebind recipient input cannot be prepared.");
|
|
2133
|
+
if (heldMarker && !held.canPrepareStatusConsumption?.()) {
|
|
2134
|
+
held.cancelEmpty();
|
|
2135
|
+
throw new Error("Selected command native consumption is unavailable.");
|
|
2136
|
+
}
|
|
2137
|
+
retained = {
|
|
2138
|
+
operationId: captured.operationId,
|
|
2139
|
+
digest,
|
|
2140
|
+
current,
|
|
2141
|
+
held,
|
|
2142
|
+
ctx,
|
|
2143
|
+
...(status ? { status } : {}),
|
|
2144
|
+
observation: {
|
|
2145
|
+
operationId: captured.operationId,
|
|
2146
|
+
recipient: {
|
|
2147
|
+
instanceId: captured.recipientInstanceId,
|
|
2148
|
+
sessionId: captured.recipientSessionId,
|
|
2149
|
+
generation: captured.recipientRegistrationGeneration,
|
|
2150
|
+
bindingKey: captured.recipientBindingKey,
|
|
2151
|
+
},
|
|
2152
|
+
sourceUpdateIds: [...held.sourceUpdateIds].sort((a, b) => a - b),
|
|
2153
|
+
...(selectedCommand
|
|
2154
|
+
? { selectedCommand: structuredClone(selectedCommand) }
|
|
2155
|
+
: {}),
|
|
2156
|
+
status: "saved",
|
|
2157
|
+
},
|
|
2158
|
+
};
|
|
2159
|
+
try {
|
|
2160
|
+
admission.appendBatch(captured.updates);
|
|
2161
|
+
}
|
|
2162
|
+
catch (error) {
|
|
2163
|
+
if (!held.confirmSaved())
|
|
2164
|
+
throw error;
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
if (!current() || !retained.held.confirmSaved())
|
|
2168
|
+
throw new Error("Live-rebind recipient save is unconfirmed.");
|
|
2169
|
+
if (status && !status.plan) {
|
|
2170
|
+
if (status.attempted)
|
|
2171
|
+
throw new Error("Selected command preparation is unconfirmed; no replay.");
|
|
2172
|
+
status.attempted = true;
|
|
2173
|
+
const record = retained, readiness = record.held.prepareSourceCompletion?.();
|
|
2174
|
+
const assertSourceCurrent = () => {
|
|
2175
|
+
if (retained !== record ||
|
|
2176
|
+
!record.current() ||
|
|
2177
|
+
!record.held.isOwnerCurrent())
|
|
2178
|
+
throw new Error("Selected command source owner changed.");
|
|
2179
|
+
};
|
|
2180
|
+
const plan = readiness &&
|
|
2181
|
+
status.prepare.call(status.owner, readiness, ctx, {
|
|
2182
|
+
target: { ...heldMarker.target },
|
|
2183
|
+
assertSourceCurrent,
|
|
2184
|
+
assertRecipientCurrent: status.recipient,
|
|
2185
|
+
});
|
|
2186
|
+
if (!plan ||
|
|
2187
|
+
!current() ||
|
|
2188
|
+
!record.held.confirmSaved() ||
|
|
2189
|
+
!isDeepStrictEqual(plan.source, readiness.snapshot.source) ||
|
|
2190
|
+
[plan.bindCarrier, plan.execute, plan.inspectCompletion].some((port) => typeof port !== "function"))
|
|
2191
|
+
throw new Error("Selected command plan is unavailable.");
|
|
2192
|
+
status.plan = plan;
|
|
2193
|
+
status.ports = {
|
|
2194
|
+
bindCarrier: plan.bindCarrier,
|
|
2195
|
+
execute: plan.execute,
|
|
2196
|
+
inspectCompletion: plan.inspectCompletion,
|
|
2197
|
+
};
|
|
2198
|
+
const continuation = {
|
|
2199
|
+
source: plan.source,
|
|
2200
|
+
assertCurrent() {
|
|
2201
|
+
assertSourceCurrent();
|
|
2202
|
+
if (record.outcome?.status !== "released")
|
|
2203
|
+
throw new Error("Selected command has no confirmed peer release.");
|
|
2204
|
+
status.recipient.call(status.owner);
|
|
2205
|
+
},
|
|
2206
|
+
bindCarrier: status.ports.bindCarrier.bind(plan),
|
|
2207
|
+
async execute() {
|
|
2208
|
+
status.result = "unknown";
|
|
2209
|
+
const completed = await status.ports.execute.call(plan);
|
|
2210
|
+
if (completed && record.current() && record.held.isOwnerCurrent())
|
|
2211
|
+
status.result = "completed";
|
|
2212
|
+
return completed;
|
|
2213
|
+
},
|
|
2214
|
+
};
|
|
2215
|
+
if (!record.held.prepareStatusConsumption?.(continuation) ||
|
|
2216
|
+
!current()) {
|
|
2217
|
+
delete status.plan;
|
|
2218
|
+
delete status.ports;
|
|
2219
|
+
throw new Error("Selected command consumption is unconfirmed.");
|
|
2220
|
+
}
|
|
2221
|
+
record.observation.preparedSource = { ...plan.source };
|
|
2222
|
+
}
|
|
2223
|
+
return structuredClone(retained.observation);
|
|
2224
|
+
},
|
|
2225
|
+
async apply(envelope, ctx, isCurrent) {
|
|
2226
|
+
const record = retained;
|
|
2227
|
+
if (envelope.selectedCommand !== undefined && !record?.status?.plan)
|
|
2228
|
+
throw new Error("Selected command native consumption is unavailable.");
|
|
2229
|
+
const current = () => retained === record &&
|
|
2230
|
+
!!record &&
|
|
2231
|
+
record.ctx === ctx &&
|
|
2232
|
+
record.current() &&
|
|
2233
|
+
isCurrent() &&
|
|
2234
|
+
record.held.isCurrent() &&
|
|
2235
|
+
matchesLiveRebindBranch(record.observation, !!record.status, envelope);
|
|
2236
|
+
if (!record ||
|
|
2237
|
+
!current() ||
|
|
2238
|
+
!record.held.confirmSaved() ||
|
|
2239
|
+
!deps.applyTarget ||
|
|
2240
|
+
record.applying ||
|
|
2241
|
+
record.settling ||
|
|
2242
|
+
!matchesLiveRebindCarrier(record.observation, envelope))
|
|
2243
|
+
throw new Error("Live-rebind apply has no exact current saved carrier.");
|
|
2244
|
+
const mode = envelope.mode === "apply" && !record.applyIssued ? "apply" : "inspect";
|
|
2245
|
+
if (mode === "apply")
|
|
2246
|
+
record.applyIssued = true;
|
|
2247
|
+
record.applying = true;
|
|
2248
|
+
try {
|
|
2249
|
+
const observation = await deps.applyTarget({
|
|
2250
|
+
operationId: record.operationId,
|
|
2251
|
+
registrationGeneration: envelope.recipientRegistrationGeneration,
|
|
2252
|
+
mode,
|
|
2253
|
+
liveRebind: {
|
|
2254
|
+
sourceUpdateIds: record.observation.sourceUpdateIds,
|
|
2255
|
+
isCurrent: current,
|
|
2256
|
+
...copyLiveRebindCommandTarget(record.observation),
|
|
2257
|
+
},
|
|
2258
|
+
}, ctx);
|
|
2259
|
+
if (!current() ||
|
|
2260
|
+
!record.held.confirmSaved() ||
|
|
2261
|
+
observation.operationId !== record.operationId ||
|
|
2262
|
+
observation.recipient.instanceId !== envelope.recipientInstanceId ||
|
|
2263
|
+
observation.recipient.sessionId !== envelope.recipientSessionId ||
|
|
2264
|
+
observation.recipient.generation !==
|
|
2265
|
+
envelope.recipientRegistrationGeneration)
|
|
2266
|
+
throw new Error("Live-rebind apply observation changed.");
|
|
2267
|
+
return {
|
|
2268
|
+
...structuredClone(record.observation),
|
|
2269
|
+
target: { ...observation.target },
|
|
2270
|
+
slot: observation.slot,
|
|
2271
|
+
status: observation.ready ? "applied" : "saved",
|
|
2272
|
+
};
|
|
2273
|
+
}
|
|
2274
|
+
finally {
|
|
2275
|
+
delete record.applying;
|
|
2276
|
+
}
|
|
2277
|
+
},
|
|
2278
|
+
async settle(envelope, ctx, isCurrent) {
|
|
2279
|
+
if (envelope.mode === "observe-command")
|
|
2280
|
+
return inspectCommandCompletion(envelope, ctx, isCurrent);
|
|
2281
|
+
const record = retained, status = record?.status, plan = status?.plan;
|
|
2282
|
+
if (envelope.selectedCommand !== undefined && !plan)
|
|
2283
|
+
throw new Error("Selected command native consumption is unavailable.");
|
|
2284
|
+
// Only the marked read-only held work sample may outlive the source worker, through the captured recipient owner.
|
|
2285
|
+
const commandWork = envelope.mode === "observe" && envelope.selectedCommand !== undefined;
|
|
2286
|
+
const recipientCurrent = () => {
|
|
2287
|
+
if (record?.status !== status ||
|
|
2288
|
+
status?.plan !== plan ||
|
|
2289
|
+
!status?.current())
|
|
2290
|
+
return false;
|
|
2291
|
+
try {
|
|
2292
|
+
status.recipient.call(status.owner);
|
|
2293
|
+
return true;
|
|
2294
|
+
}
|
|
2295
|
+
catch {
|
|
2296
|
+
return false;
|
|
2297
|
+
}
|
|
2298
|
+
};
|
|
2299
|
+
const current = () => retained === record &&
|
|
2300
|
+
!!record &&
|
|
2301
|
+
record.ctx === ctx &&
|
|
2302
|
+
record.current() &&
|
|
2303
|
+
isCurrent() &&
|
|
2304
|
+
(commandWork ? recipientCurrent() : record.held.isOwnerCurrent()) &&
|
|
2305
|
+
matchesLiveRebindBranch(record.observation, !!record.status, envelope);
|
|
2306
|
+
if (!record ||
|
|
2307
|
+
!current() ||
|
|
2308
|
+
record.applying ||
|
|
2309
|
+
record.settling ||
|
|
2310
|
+
!matchesLiveRebindCarrier(record.observation, envelope))
|
|
2311
|
+
throw new Error("Live-rebind settlement has no exact current carrier.");
|
|
2312
|
+
if (envelope.mode === "observe") {
|
|
2313
|
+
if (record.outcome?.status !== "released" ||
|
|
2314
|
+
!deps.applyTarget ||
|
|
2315
|
+
!deps.observeWork ||
|
|
2316
|
+
!envelope.oldTarget)
|
|
2317
|
+
throw new Error("Live-rebind work observation has no released carrier or observer.");
|
|
2318
|
+
const oldTarget = { ...envelope.oldTarget };
|
|
2319
|
+
let work;
|
|
2320
|
+
record.settling = true;
|
|
2321
|
+
try {
|
|
2322
|
+
await deps.applyTarget({
|
|
2323
|
+
operationId: record.operationId,
|
|
2324
|
+
registrationGeneration: envelope.recipientRegistrationGeneration,
|
|
2325
|
+
mode: "inspect",
|
|
2326
|
+
liveRebind: {
|
|
2327
|
+
sourceUpdateIds: record.observation.sourceUpdateIds,
|
|
2328
|
+
isCurrent: current,
|
|
2329
|
+
...copyLiveRebindCommandTarget(record.observation),
|
|
2330
|
+
observeWork(canonicalOldTarget) {
|
|
2331
|
+
if (!current() ||
|
|
2332
|
+
!isDeepStrictEqual(canonicalOldTarget, oldTarget))
|
|
2333
|
+
throw new Error("Live-rebind old work target changed.");
|
|
2334
|
+
work = structuredClone(deps.observeWork(canonicalOldTarget, ctx));
|
|
2335
|
+
if (!current() ||
|
|
2336
|
+
!isDeepStrictEqual(canonicalOldTarget, oldTarget))
|
|
2337
|
+
throw new Error("Live-rebind work observer authority or target changed.");
|
|
2338
|
+
},
|
|
2339
|
+
},
|
|
2340
|
+
}, ctx);
|
|
2341
|
+
if (!current() || !work)
|
|
2342
|
+
throw new Error("Live-rebind work observation is unconfirmed.");
|
|
2343
|
+
// Unfinished or unconfirmed selected execution stays unknown; later settled delivery cannot clear it.
|
|
2344
|
+
if (commandWork && status.result !== "completed")
|
|
2345
|
+
work.unknown = true;
|
|
2346
|
+
if (!current())
|
|
2347
|
+
throw new Error("Live-rebind work observer authority changed.");
|
|
2348
|
+
return {
|
|
2349
|
+
...structuredClone(record.observation),
|
|
2350
|
+
status: "observed",
|
|
2351
|
+
oldTarget,
|
|
2352
|
+
work,
|
|
2353
|
+
};
|
|
2354
|
+
}
|
|
2355
|
+
finally {
|
|
2356
|
+
delete record.settling;
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
const action = envelope.mode;
|
|
2360
|
+
if (record.outcome) {
|
|
2361
|
+
if (record.outcome.action !== envelope.mode)
|
|
2362
|
+
throw new Error("Live-rebind input already has another disposition.");
|
|
2363
|
+
return structuredClone(record.outcome);
|
|
2364
|
+
}
|
|
2365
|
+
record.settling = true;
|
|
2366
|
+
const observation = (status) => ({
|
|
2367
|
+
...structuredClone(record.observation),
|
|
2368
|
+
action,
|
|
2369
|
+
status,
|
|
2370
|
+
});
|
|
2371
|
+
try {
|
|
2372
|
+
if (envelope.mode === "discard") {
|
|
2373
|
+
record.outcome = observation("unknown");
|
|
2374
|
+
const status = record.held.discardSaved(current);
|
|
2375
|
+
record.outcome =
|
|
2376
|
+
status === "protected" ? undefined : observation(status);
|
|
2377
|
+
if (!current())
|
|
2378
|
+
throw new Error("Live-rebind disposal authority changed.");
|
|
2379
|
+
return observation(status);
|
|
2380
|
+
}
|
|
2381
|
+
if (!record.held.isCurrent() ||
|
|
2382
|
+
!record.held.confirmSaved() ||
|
|
2383
|
+
!deps.applyTarget)
|
|
2384
|
+
throw new Error("Live-rebind input cannot be released.");
|
|
2385
|
+
await deps.applyTarget({
|
|
2386
|
+
operationId: record.operationId,
|
|
2387
|
+
registrationGeneration: envelope.recipientRegistrationGeneration,
|
|
2388
|
+
mode: "inspect",
|
|
2389
|
+
liveRebind: {
|
|
2390
|
+
sourceUpdateIds: record.observation.sourceUpdateIds,
|
|
2391
|
+
isCurrent: current,
|
|
2392
|
+
...copyLiveRebindCommandTarget(record.observation),
|
|
2393
|
+
release(canRelease) {
|
|
2394
|
+
record.outcome = observation("unknown");
|
|
2395
|
+
record.outcome = record.held.release(() => current() && canRelease())
|
|
2396
|
+
? observation("released")
|
|
2397
|
+
: undefined;
|
|
2398
|
+
},
|
|
2399
|
+
},
|
|
2400
|
+
}, ctx);
|
|
2401
|
+
if (!current())
|
|
2402
|
+
throw new Error("Live-rebind release authority changed.");
|
|
2403
|
+
return structuredClone(record.outcome ?? observation("protected"));
|
|
2404
|
+
}
|
|
2405
|
+
finally {
|
|
2406
|
+
delete record.settling;
|
|
2407
|
+
}
|
|
2408
|
+
},
|
|
2409
|
+
};
|
|
2410
|
+
async function inspectCommandCompletion(envelope, ctx, isCurrent) {
|
|
2411
|
+
const record = retained;
|
|
2412
|
+
const current = () => retained === record &&
|
|
2413
|
+
!!record?.status?.plan &&
|
|
2414
|
+
record.ctx === ctx &&
|
|
2415
|
+
record.current() &&
|
|
2416
|
+
record.held.isOwnerCurrent() &&
|
|
2417
|
+
isCurrent() &&
|
|
2418
|
+
matchesLiveRebindBranch(record.observation, true, envelope);
|
|
2419
|
+
if (!record?.status?.plan ||
|
|
2420
|
+
!deps.applyTarget ||
|
|
2421
|
+
!current() ||
|
|
2422
|
+
record.outcome?.status !== "released" ||
|
|
2423
|
+
record.applying ||
|
|
2424
|
+
record.settling ||
|
|
2425
|
+
!matchesLiveRebindCarrier(record.observation, envelope))
|
|
2426
|
+
throw new Error("Selected command completion has no exact released carrier.");
|
|
2427
|
+
record.settling = true;
|
|
2428
|
+
try {
|
|
2429
|
+
const assertRecipient = () => {
|
|
2430
|
+
record.status.recipient.call(record.status.owner);
|
|
2431
|
+
if (!current())
|
|
2432
|
+
throw new Error("Selected command completion recipient changed.");
|
|
2433
|
+
};
|
|
2434
|
+
assertRecipient();
|
|
2435
|
+
const observed = await deps.applyTarget({
|
|
2436
|
+
operationId: record.operationId,
|
|
2437
|
+
registrationGeneration: record.observation.recipient.generation,
|
|
2438
|
+
mode: "inspect",
|
|
2439
|
+
liveRebind: {
|
|
2440
|
+
sourceUpdateIds: record.observation.sourceUpdateIds,
|
|
2441
|
+
expectedTarget: { ...record.observation.selectedCommand.target },
|
|
2442
|
+
isCurrent: current,
|
|
2443
|
+
},
|
|
2444
|
+
}, ctx);
|
|
2445
|
+
if (!observed.ready || !current())
|
|
2446
|
+
throw new Error("Selected command completion target is unconfirmed.");
|
|
2447
|
+
assertRecipient();
|
|
2448
|
+
const sourceAck = record.status.ports.inspectCompletion.call(record.status.plan);
|
|
2449
|
+
assertRecipient();
|
|
2450
|
+
if (sourceAck &&
|
|
2451
|
+
!isDeepStrictEqual(sourceAck, record.observation.preparedSource))
|
|
2452
|
+
throw new Error("Selected command completion exact ACK changed.");
|
|
2453
|
+
const settled = {
|
|
2454
|
+
preparedSource: { ...record.observation.preparedSource },
|
|
2455
|
+
command: record.status.result ?? "not-issued",
|
|
2456
|
+
...(sourceAck ? { sourceAck: { ...sourceAck } } : {}),
|
|
2457
|
+
};
|
|
2458
|
+
const { selectedCommand } = record.observation;
|
|
2459
|
+
if (envelope.mode !== "observe-command" || !selectedCommand)
|
|
2460
|
+
throw new Error("Held completion mode does not match its marker.");
|
|
2461
|
+
return {
|
|
2462
|
+
...structuredClone(record.observation),
|
|
2463
|
+
status: "command-observed",
|
|
2464
|
+
selectedCommand: structuredClone(selectedCommand),
|
|
2465
|
+
...settled,
|
|
2466
|
+
};
|
|
2467
|
+
}
|
|
2468
|
+
finally {
|
|
2469
|
+
delete record.settling;
|
|
2470
|
+
}
|
|
2471
|
+
}
|
|
2472
|
+
return runtime;
|
|
2473
|
+
}
|
|
1608
2474
|
export function createTelegramBusFollowerDurableAdmissionRuntime(deps) {
|
|
1609
2475
|
// The journal forgets a delivery once its worker completes; this window absorbs a leader retry after a lost ACK.
|
|
1610
2476
|
const recentDeliveries = new Map();
|
|
1611
2477
|
const getNowMs = deps.getNowMs ?? Date.now;
|
|
1612
|
-
const limit = deps.recentDeliveryLimit ?? {
|
|
2478
|
+
const limit = deps.recentDeliveryLimit ?? {
|
|
2479
|
+
maxAgeMs: 24 * 60 * 60 * 1000,
|
|
2480
|
+
maxEntries: 4096,
|
|
2481
|
+
};
|
|
1613
2482
|
const pruneRecentDeliveries = (nowMs) => {
|
|
1614
2483
|
for (const [deliveryId, admittedAtMs] of recentDeliveries) {
|
|
1615
|
-
if (recentDeliveries.size <= limit.maxEntries &&
|
|
2484
|
+
if (recentDeliveries.size <= limit.maxEntries &&
|
|
2485
|
+
nowMs - admittedAtMs < limit.maxAgeMs)
|
|
1616
2486
|
break;
|
|
1617
2487
|
recentDeliveries.delete(deliveryId);
|
|
1618
2488
|
}
|
|
@@ -1683,12 +2553,17 @@ export function createTelegramBusFollowerInputCustodyPorts(deps) {
|
|
|
1683
2553
|
return {
|
|
1684
2554
|
isSourceReferenceAdmissionEnabled: () => Boolean(deps.getInputCustodyBus()),
|
|
1685
2555
|
handleInputCustodyHandoff(envelope, ctx) {
|
|
1686
|
-
return getRequired().acceptHandoff({
|
|
2556
|
+
return getRequired().acceptHandoff({
|
|
2557
|
+
sourceRecoveryKey: envelope.sourceRecoveryKey,
|
|
1687
2558
|
recipientBindingKey: envelope.recipientBindingKey,
|
|
1688
|
-
source: envelope.source,
|
|
2559
|
+
source: envelope.source,
|
|
2560
|
+
handoffId: envelope.handoffId,
|
|
2561
|
+
}, ctx);
|
|
1689
2562
|
},
|
|
1690
2563
|
sourceReferenceAdmission: createTelegramBusFollowerSourceReferenceAdmissionRuntime({
|
|
1691
|
-
wakeSource(input, ctx) {
|
|
2564
|
+
wakeSource(input, ctx) {
|
|
2565
|
+
getRequired().wakeSource(input, ctx);
|
|
2566
|
+
},
|
|
1692
2567
|
}),
|
|
1693
2568
|
resolveInputCustodyReference(input) {
|
|
1694
2569
|
return deps.getInputCustodyBus()?.resolveForwardReference(input);
|
|
@@ -1705,23 +2580,35 @@ export function createTelegramBusFollowerSourceReferenceAdmissionRuntime(deps) {
|
|
|
1705
2580
|
throw new Error("Telegram follower source-reference admission requires a recovery key.");
|
|
1706
2581
|
if (!delivery.sourceClaim)
|
|
1707
2582
|
throw new Error("Telegram follower source-reference admission requires exact claim evidence.");
|
|
1708
|
-
const expected = createTelegramBusFollowerDeliveryIdentity({
|
|
2583
|
+
const expected = createTelegramBusFollowerDeliveryIdentity({
|
|
2584
|
+
kind: envelope.kind,
|
|
1709
2585
|
recipientBindingKey: delivery.recipientBindingKey,
|
|
1710
|
-
sourceUpdateId: delivery.sourceUpdateId
|
|
2586
|
+
sourceUpdateId: delivery.sourceUpdateId,
|
|
2587
|
+
});
|
|
1711
2588
|
if (expected.deliveryId !== delivery.deliveryId)
|
|
1712
2589
|
throw new Error("Invalid Telegram follower source-reference delivery id.");
|
|
1713
|
-
const carrier = envelope.kind === "leader.wakeInputCustody"
|
|
1714
|
-
|
|
1715
|
-
|
|
2590
|
+
const carrier = envelope.kind === "leader.wakeInputCustody"
|
|
2591
|
+
? undefined
|
|
2592
|
+
: envelope.kind === "leader.forwardCallback"
|
|
2593
|
+
? envelope.query
|
|
2594
|
+
: envelope.kind === "leader.forwardReaction"
|
|
2595
|
+
? envelope.reactionUpdate
|
|
2596
|
+
: envelope.message;
|
|
1716
2597
|
if (envelope.kind !== "leader.wakeInputCustody" &&
|
|
1717
|
-
(!isRecord(carrier) ||
|
|
2598
|
+
(!isRecord(carrier) ||
|
|
2599
|
+
carrier.pi_telegram_source_update_id !== delivery.sourceUpdateId))
|
|
1718
2600
|
throw new Error("Telegram follower source-reference update id mismatch.");
|
|
1719
|
-
await deps.wakeSource({
|
|
2601
|
+
await deps.wakeSource({
|
|
2602
|
+
deliveryId: delivery.deliveryId,
|
|
1720
2603
|
sourceUpdateId: delivery.sourceUpdateId,
|
|
1721
2604
|
recipientBindingKey: delivery.recipientBindingKey,
|
|
1722
2605
|
sourceRecoveryKey: delivery.sourceRecoveryKey,
|
|
1723
|
-
sourceClaim: { ...delivery.sourceClaim }
|
|
1724
|
-
|
|
2606
|
+
sourceClaim: { ...delivery.sourceClaim },
|
|
2607
|
+
}, ctx);
|
|
2608
|
+
return {
|
|
2609
|
+
deliveryId: delivery.deliveryId,
|
|
2610
|
+
sourceUpdateId: delivery.sourceUpdateId,
|
|
2611
|
+
};
|
|
1725
2612
|
},
|
|
1726
2613
|
};
|
|
1727
2614
|
}
|
|
@@ -1747,71 +2634,153 @@ export function createTelegramBusForwardedUpdateReceiverRuntime(deps) {
|
|
|
1747
2634
|
envelope.kind !== "leader.wakeInputCustody" &&
|
|
1748
2635
|
envelope.kind !== "leader.offerInputCustodyHandoff" &&
|
|
1749
2636
|
envelope.kind !== "leader.workspaceRestore" &&
|
|
2637
|
+
envelope.kind !== "leader.prepareLiveRebind" &&
|
|
2638
|
+
envelope.kind !== "leader.applyLiveRebind" &&
|
|
2639
|
+
envelope.kind !== "leader.settleLiveRebind" &&
|
|
1750
2640
|
envelope.kind !== "leader.offerQueueHandoff") ||
|
|
1751
2641
|
envelope.recipientInstanceId !== deps.instanceId) {
|
|
1752
|
-
return
|
|
1753
|
-
kind: "bus.ack",
|
|
1754
|
-
requestId: envelope.requestId,
|
|
1755
|
-
ok: false,
|
|
1756
|
-
message: "Telegram bus receiver cannot handle this envelope.",
|
|
1757
|
-
};
|
|
2642
|
+
return rejectTelegramBusRequest(envelope.requestId, "Telegram bus receiver cannot handle this envelope.");
|
|
1758
2643
|
}
|
|
1759
2644
|
const registrationGeneration = deps.getRegistrationGeneration();
|
|
1760
2645
|
if (!registrationGeneration ||
|
|
1761
2646
|
envelope.recipientRegistrationGeneration !== registrationGeneration) {
|
|
1762
|
-
return
|
|
1763
|
-
kind: "bus.ack",
|
|
1764
|
-
requestId: envelope.requestId,
|
|
1765
|
-
ok: false,
|
|
1766
|
-
message: "Stale Telegram bus follower registration generation.",
|
|
1767
|
-
};
|
|
2647
|
+
return rejectTelegramBusRequest(envelope.requestId, "Stale Telegram bus follower registration generation.");
|
|
1768
2648
|
}
|
|
1769
|
-
if (envelope.kind === "leader.
|
|
1770
|
-
|
|
1771
|
-
|
|
2649
|
+
if (envelope.kind === "leader.prepareLiveRebind" ||
|
|
2650
|
+
envelope.kind === "leader.applyLiveRebind" ||
|
|
2651
|
+
envelope.kind === "leader.settleLiveRebind") {
|
|
2652
|
+
const apply = envelope.kind !== "leader.prepareLiveRebind", settle = envelope.kind === "leader.settleLiveRebind";
|
|
2653
|
+
const commandInput = envelope.selectedCommand, commandEnabled = deps.isLiveRebindCommandSetEnabled;
|
|
2654
|
+
const required = [
|
|
2655
|
+
TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SAVE,
|
|
2656
|
+
...(apply ? [TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_APPLY] : []),
|
|
2657
|
+
...(settle ? [TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SETTLE] : []),
|
|
2658
|
+
...(commandInput
|
|
2659
|
+
? [
|
|
2660
|
+
TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_APPLY,
|
|
2661
|
+
TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SETTLE,
|
|
2662
|
+
TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_COMMAND_SET,
|
|
2663
|
+
TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY,
|
|
2664
|
+
]
|
|
2665
|
+
: []),
|
|
2666
|
+
];
|
|
2667
|
+
const ctx = deps.getContext(), sessionId = deps.getSessionId?.(), bindingKey = deps.getLiveRebindJournalBindingKey?.();
|
|
2668
|
+
const protocol = structuredClone(deps.getLeaderProtocol?.()), localProtocol = structuredClone(deps.getLocalProtocol?.());
|
|
2669
|
+
const current = () => !!authSecret &&
|
|
2670
|
+
deps.getAuthSecret?.() === authSecret &&
|
|
2671
|
+
deps.getContext() === ctx &&
|
|
2672
|
+
deps.getRegistrationGeneration() === registrationGeneration &&
|
|
2673
|
+
deps.getSessionId?.() === sessionId &&
|
|
2674
|
+
deps.getLiveRebindJournalBindingKey?.() === bindingKey &&
|
|
2675
|
+
deps.isLiveRebindSaveEnabled?.() === true &&
|
|
2676
|
+
(!apply || deps.isLiveRebindApplyEnabled?.() === true) &&
|
|
2677
|
+
(!settle || deps.isLiveRebindSettleEnabled?.() === true) &&
|
|
2678
|
+
(!commandInput ||
|
|
2679
|
+
(typeof commandEnabled === "function" &&
|
|
2680
|
+
deps.isLiveRebindCommandSetEnabled === commandEnabled &&
|
|
2681
|
+
commandEnabled())) &&
|
|
2682
|
+
isDeepStrictEqual(deps.getLeaderProtocol?.(), protocol) &&
|
|
2683
|
+
isDeepStrictEqual(deps.getLocalProtocol?.(), localProtocol);
|
|
2684
|
+
if (!authSecret ||
|
|
2685
|
+
!ctx ||
|
|
2686
|
+
!bindingKey ||
|
|
2687
|
+
sessionId !== envelope.recipientSessionId ||
|
|
2688
|
+
bindingKey !== envelope.recipientBindingKey ||
|
|
2689
|
+
!required.every((cap) => hasTelegramBusCapability(protocol, cap) &&
|
|
2690
|
+
hasTelegramBusCapability(localProtocol, cap)) ||
|
|
2691
|
+
!localProtocol ||
|
|
2692
|
+
!getTelegramBusProtocolCompatibility({
|
|
2693
|
+
local: localProtocol,
|
|
2694
|
+
remote: protocol,
|
|
2695
|
+
}).compatible ||
|
|
2696
|
+
!current() ||
|
|
2697
|
+
(settle
|
|
2698
|
+
? !deps.handleLiveRebindSettle
|
|
2699
|
+
: apply
|
|
2700
|
+
? !deps.handleLiveRebindApply
|
|
2701
|
+
: !deps.handleLiveRebindSave))
|
|
2702
|
+
return rejectTelegramBusRequest(envelope.requestId, "Live-rebind capability or recipient authority is unavailable.");
|
|
2703
|
+
try {
|
|
2704
|
+
const result = envelope.kind === "leader.settleLiveRebind"
|
|
2705
|
+
? await deps.handleLiveRebindSettle(envelope, ctx, current)
|
|
2706
|
+
: envelope.kind === "leader.applyLiveRebind"
|
|
2707
|
+
? await deps.handleLiveRebindApply(envelope, ctx, current)
|
|
2708
|
+
: await deps.handleLiveRebindSave(envelope, ctx, current);
|
|
2709
|
+
if (!current())
|
|
2710
|
+
throw new Error("Live-rebind receiver authority changed.");
|
|
1772
2711
|
return {
|
|
1773
|
-
kind: "bus.ack",
|
|
2712
|
+
kind: "bus.ack",
|
|
2713
|
+
requestId: envelope.requestId,
|
|
2714
|
+
ok: true,
|
|
2715
|
+
result,
|
|
1774
2716
|
};
|
|
2717
|
+
}
|
|
2718
|
+
catch (error) {
|
|
2719
|
+
deps.recordRuntimeEvent?.("bus", error, {
|
|
2720
|
+
phase: "follower-live-rebind-control",
|
|
2721
|
+
});
|
|
2722
|
+
return {
|
|
2723
|
+
kind: "bus.ack",
|
|
2724
|
+
requestId: envelope.requestId,
|
|
2725
|
+
ok: false,
|
|
2726
|
+
message: error instanceof Error
|
|
2727
|
+
? error.message
|
|
2728
|
+
: "Live-rebind request failed.",
|
|
2729
|
+
};
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
if (envelope.kind === "leader.workspaceRestore") {
|
|
2733
|
+
const ctx = deps.getContext();
|
|
2734
|
+
if (!authSecret ||
|
|
2735
|
+
!ctx ||
|
|
2736
|
+
!deps.isWorkspaceRestoreEnabled?.() ||
|
|
2737
|
+
!deps.handleWorkspaceRestore)
|
|
2738
|
+
return rejectTelegramBusRequest(envelope.requestId, "Workspace Restore capability is unavailable.");
|
|
1775
2739
|
try {
|
|
1776
|
-
const result = await deps.handleWorkspaceRestore({
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
2740
|
+
const result = await deps.handleWorkspaceRestore({
|
|
2741
|
+
operationId: envelope.operationId,
|
|
2742
|
+
registrationGeneration,
|
|
2743
|
+
mode: envelope.mode,
|
|
2744
|
+
}, ctx);
|
|
2745
|
+
if (deps.getAuthSecret?.() !== authSecret ||
|
|
2746
|
+
deps.getRegistrationGeneration() !== registrationGeneration ||
|
|
2747
|
+
deps.getContext() !== ctx ||
|
|
2748
|
+
!deps.isWorkspaceRestoreEnabled())
|
|
1780
2749
|
throw new Error("Workspace Restore receiver authority changed.");
|
|
1781
|
-
return {
|
|
2750
|
+
return {
|
|
2751
|
+
kind: "bus.ack",
|
|
2752
|
+
requestId: envelope.requestId,
|
|
2753
|
+
ok: true,
|
|
2754
|
+
result,
|
|
2755
|
+
};
|
|
1782
2756
|
}
|
|
1783
2757
|
catch (error) {
|
|
1784
|
-
deps.recordRuntimeEvent?.("bus", error, {
|
|
1785
|
-
|
|
1786
|
-
|
|
2758
|
+
deps.recordRuntimeEvent?.("bus", error, {
|
|
2759
|
+
phase: "follower-workspace-restore",
|
|
2760
|
+
});
|
|
2761
|
+
return {
|
|
2762
|
+
kind: "bus.ack",
|
|
2763
|
+
requestId: envelope.requestId,
|
|
2764
|
+
ok: false,
|
|
2765
|
+
message: error instanceof Error
|
|
2766
|
+
? error.message
|
|
2767
|
+
: "Workspace Restore failed.",
|
|
2768
|
+
};
|
|
1787
2769
|
}
|
|
1788
2770
|
}
|
|
1789
2771
|
if (envelope.kind === "leader.offerInputCustodyHandoff" &&
|
|
1790
2772
|
envelope.recipientBindingKey !== deps.getRecipientBindingKey())
|
|
1791
|
-
return
|
|
1792
|
-
kind: "bus.ack", requestId: envelope.requestId, ok: false,
|
|
1793
|
-
message: "Mismatched Telegram follower handoff recipient identity.",
|
|
1794
|
-
};
|
|
2773
|
+
return rejectTelegramBusRequest(envelope.requestId, "Mismatched Telegram follower handoff recipient identity.");
|
|
1795
2774
|
if (envelope.kind !== "leader.offerQueueHandoff" &&
|
|
1796
2775
|
envelope.kind !== "leader.offerInputCustodyHandoff" &&
|
|
1797
2776
|
(!envelope.delivery ||
|
|
1798
2777
|
envelope.delivery.recipientBindingKey !==
|
|
1799
2778
|
deps.getRecipientBindingKey())) {
|
|
1800
|
-
return
|
|
1801
|
-
kind: "bus.ack",
|
|
1802
|
-
requestId: envelope.requestId,
|
|
1803
|
-
ok: false,
|
|
1804
|
-
message: "Mismatched Telegram follower delivery identity.",
|
|
1805
|
-
};
|
|
2779
|
+
return rejectTelegramBusRequest(envelope.requestId, "Mismatched Telegram follower delivery identity.");
|
|
1806
2780
|
}
|
|
1807
2781
|
const ctx = deps.getContext();
|
|
1808
2782
|
if (!ctx) {
|
|
1809
|
-
return
|
|
1810
|
-
kind: "bus.ack",
|
|
1811
|
-
requestId: envelope.requestId,
|
|
1812
|
-
ok: false,
|
|
1813
|
-
message: "Telegram bus follower has no active context.",
|
|
1814
|
-
};
|
|
2783
|
+
return rejectTelegramBusRequest(envelope.requestId, "Telegram bus follower has no active context.");
|
|
1815
2784
|
}
|
|
1816
2785
|
try {
|
|
1817
2786
|
if (envelope.kind === "leader.offerInputCustodyHandoff") {
|
|
@@ -1822,7 +2791,12 @@ export function createTelegramBusForwardedUpdateReceiverRuntime(deps) {
|
|
|
1822
2791
|
if (!deps.handleInputCustodyHandoff)
|
|
1823
2792
|
throw new Error("Telegram input custody handoff acceptance is unavailable.");
|
|
1824
2793
|
const result = await deps.handleInputCustodyHandoff(envelope, ctx);
|
|
1825
|
-
return {
|
|
2794
|
+
return {
|
|
2795
|
+
kind: "bus.ack",
|
|
2796
|
+
requestId: envelope.requestId,
|
|
2797
|
+
ok: true,
|
|
2798
|
+
result,
|
|
2799
|
+
};
|
|
1826
2800
|
}
|
|
1827
2801
|
if (envelope.kind !== "leader.offerQueueHandoff") {
|
|
1828
2802
|
const sourceReferenceEnabled = envelope.kind === "leader.wakeInputCustody" ||
|
|
@@ -1831,7 +2805,8 @@ export function createTelegramBusForwardedUpdateReceiverRuntime(deps) {
|
|
|
1831
2805
|
!deps.hasAuthenticatedSourceReferenceTransport?.())
|
|
1832
2806
|
throw new Error("Telegram follower source-reference admission requires authenticated transport.");
|
|
1833
2807
|
const admission = sourceReferenceEnabled
|
|
1834
|
-
? deps.sourceReferenceAdmission
|
|
2808
|
+
? deps.sourceReferenceAdmission
|
|
2809
|
+
: deps.durableAdmission;
|
|
1835
2810
|
if (!admission)
|
|
1836
2811
|
throw new Error("Telegram follower source-reference admission is enabled without a wake authority.");
|
|
1837
2812
|
const receipt = await admission.admit(envelope, ctx);
|
|
@@ -1892,13 +2867,3 @@ function parseRegistrationResult(value) {
|
|
|
1892
2867
|
: {}),
|
|
1893
2868
|
};
|
|
1894
2869
|
}
|
|
1895
|
-
function parseTarget(value) {
|
|
1896
|
-
if (value === undefined)
|
|
1897
|
-
return undefined;
|
|
1898
|
-
if (!isRecord(value) || typeof value.chatId !== "number")
|
|
1899
|
-
return undefined;
|
|
1900
|
-
const target = { chatId: value.chatId };
|
|
1901
|
-
if (typeof value.threadId === "number")
|
|
1902
|
-
target.threadId = value.threadId;
|
|
1903
|
-
return target;
|
|
1904
|
-
}
|