@llblab/pi-telegram 0.54.4 → 0.55.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 +4 -2
- package/BACKLOG.md +1 -1
- package/CHANGELOG.md +16 -0
- package/README.md +5 -3
- package/dist/lib/activity.d.ts +5 -1
- package/dist/lib/activity.js +31 -2
- package/dist/lib/bindings.d.ts +3 -1
- package/dist/lib/bindings.js +8 -2
- package/dist/lib/bus-api.d.ts +2 -2
- package/dist/lib/bus-api.js +7 -2
- package/dist/lib/bus-follower.d.ts +50 -3
- package/dist/lib/bus-follower.js +313 -146
- package/dist/lib/bus-leader.d.ts +3 -0
- package/dist/lib/bus-leader.js +99 -18
- package/dist/lib/bus.d.ts +7 -3
- package/dist/lib/bus.js +24 -16
- package/dist/lib/delivery.d.ts +4 -0
- package/dist/lib/delivery.js +48 -15
- package/dist/lib/extension.js +48 -6
- package/dist/lib/lifecycle.d.ts +68 -0
- package/dist/lib/lifecycle.js +178 -58
- package/dist/lib/machine.d.ts +23 -0
- package/dist/lib/machine.js +37 -0
- package/dist/lib/media.d.ts +114 -28
- package/dist/lib/media.js +292 -119
- package/dist/lib/model.d.ts +54 -15
- package/dist/lib/model.js +98 -53
- package/dist/lib/polling.js +2 -0
- package/dist/lib/preview.d.ts +128 -113
- package/dist/lib/preview.js +448 -366
- package/dist/lib/queue.d.ts +2 -2
- package/dist/lib/queue.js +3 -3
- package/dist/lib/routing.d.ts +7 -2
- package/dist/lib/routing.js +585 -229
- package/dist/lib/runtime.d.ts +77 -11
- package/dist/lib/runtime.js +174 -92
- package/dist/lib/sections.js +0 -1
- package/dist/lib/telegram-api.d.ts +11 -9
- package/dist/lib/telegram-api.js +53 -4
- package/dist/lib/text-groups.d.ts +0 -26
- package/dist/lib/text-groups.js +51 -98
- package/dist/lib/thread-display.js +14 -6
- package/dist/lib/threads.d.ts +18 -5
- package/dist/lib/threads.js +107 -30
- package/dist/lib/turns.d.ts +6 -0
- package/dist/lib/turns.js +12 -1
- package/dist/lib/updates.d.ts +13 -4
- package/dist/lib/updates.js +54 -19
- package/dist/package.json +1 -1
- package/docs/activity.md +1 -1
- package/docs/architecture.md +14 -11
- package/docs/delivery.md +7 -5
- package/docs/multi-instance-bus.md +20 -8
- package/docs/public-api.md +2 -2
- package/docs/ui-style.md +2 -2
- package/lib/activity.ts +29 -5
- package/lib/bindings.ts +9 -1
- package/lib/bus-api.ts +11 -3
- package/lib/bus-follower.ts +417 -203
- package/lib/bus-leader.ts +88 -14
- package/lib/bus.ts +30 -18
- package/lib/delivery.ts +47 -19
- package/lib/extension.ts +51 -9
- package/lib/lifecycle.ts +229 -55
- package/lib/machine.ts +60 -0
- package/lib/media.ts +438 -172
- package/lib/model.ts +169 -74
- package/lib/polling.ts +2 -0
- package/lib/preview.ts +715 -566
- package/lib/queue.ts +5 -5
- package/lib/routing.ts +614 -257
- package/lib/runtime.ts +260 -104
- package/lib/sections.ts +0 -2
- package/lib/telegram-api.ts +61 -8
- package/lib/text-groups.ts +75 -136
- package/lib/thread-display.ts +15 -4
- package/lib/threads.ts +120 -36
- package/lib/turns.ts +15 -1
- package/lib/updates.ts +60 -21
- package/package.json +1 -1
package/dist/lib/bus-follower.js
CHANGED
|
@@ -9,12 +9,13 @@ import { createHash } from "node:crypto";
|
|
|
9
9
|
import { basename } from "node:path";
|
|
10
10
|
import { isDeepStrictEqual } from "node:util";
|
|
11
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";
|
|
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_PRE_DISPATCH_REBIND_EXECUTION, 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
13
|
import { parseTelegramUpdateJournalQueueOwner, } from "./journal.js";
|
|
14
14
|
import { TELEGRAM_BUS_LEADER_STALE_HEARTBEAT_MS, } from "./locks.js";
|
|
15
|
+
import * as Machine from "./machine.js";
|
|
15
16
|
import { isPiStaleContextError } from "./pi.js";
|
|
16
17
|
import { parseTelegramTarget as parseTarget, } from "./target.js";
|
|
17
|
-
import { isTelegramApiMethodRetrySafe, TelegramApiAuthorityError, TelegramApiCommitUnknownError, TelegramApiStaleTargetError, } from "./telegram-api.js";
|
|
18
|
+
import { isTelegramApiMethodRetrySafe, assertTelegramApiCallAuthority, TelegramApiAuthorityError, TelegramApiCommitUnknownError, TelegramApiStaleTargetError, } from "./telegram-api.js";
|
|
18
19
|
import * as Threads from "./threads.js";
|
|
19
20
|
import { isWireRecord as isRecord } from "./wire.js";
|
|
20
21
|
import { createTelegramWorkspaceAdmissionOperationId, runWithTelegramWorkspaceAdmissionsAsync, } from "./workspace-admission.js";
|
|
@@ -147,8 +148,8 @@ export function createTelegramBusFollowerPromotionHandler(input) {
|
|
|
147
148
|
export function createTelegramBusFollowerRuntimeAssembly(ports) {
|
|
148
149
|
// Binding startup needs registration identity before it can grant inbound authority.
|
|
149
150
|
let readyContext;
|
|
150
|
-
const isReadyContext = (
|
|
151
|
-
(
|
|
151
|
+
const isReadyContext = () => Boolean(readyContext &&
|
|
152
|
+
ports.receiver.getContext() === readyContext.ctx &&
|
|
152
153
|
ports.registrationState.getGeneration() === readyContext.generation &&
|
|
153
154
|
ports.registration.getSessionGeneration?.() ===
|
|
154
155
|
readyContext.sessionGeneration &&
|
|
@@ -159,18 +160,36 @@ export function createTelegramBusFollowerRuntimeAssembly(ports) {
|
|
|
159
160
|
const generation = ports.registrationState.getGeneration();
|
|
160
161
|
const sessionGeneration = ports.registration.getSessionGeneration?.();
|
|
161
162
|
const sessionId = ports.registration.getSessionId?.(ctx);
|
|
163
|
+
const executionContext = ports.receiver.getContext();
|
|
162
164
|
readyContext = undefined;
|
|
163
165
|
if (!generation ||
|
|
166
|
+
executionContext === undefined ||
|
|
167
|
+
ports.registration.isContextActive?.(ctx) === false ||
|
|
168
|
+
ports.registration.isContextActive?.(executionContext) === false ||
|
|
169
|
+
ports.registration.getSessionId?.(executionContext) !== sessionId ||
|
|
170
|
+
(executionContext !== ctx &&
|
|
171
|
+
(sessionId === undefined ||
|
|
172
|
+
!ports.registration.isContextActive ||
|
|
173
|
+
!ports.registration.getSessionGeneration)) ||
|
|
164
174
|
(sessionId !== undefined &&
|
|
165
175
|
ports.registrationState.getSessionId(sessionId) !== sessionId))
|
|
166
|
-
throw new Error("Telegram follower session change requires acknowledged leader registration.");
|
|
167
|
-
|
|
176
|
+
throw new Error("Telegram follower session change requires acknowledged leader registration and a current canonical context.");
|
|
177
|
+
// Connect has a command context; inbound work uses the session's canonical object.
|
|
178
|
+
await ports.registration.onRegistered?.(executionContext);
|
|
168
179
|
if (generation &&
|
|
169
180
|
ports.registrationState.getGeneration() === generation &&
|
|
170
181
|
ports.registration.getSessionGeneration?.() === sessionGeneration &&
|
|
182
|
+
ports.receiver.getContext() === executionContext &&
|
|
171
183
|
ports.registration.getSessionId?.(ctx) === sessionId &&
|
|
172
|
-
ports.registration.
|
|
173
|
-
|
|
184
|
+
ports.registration.getSessionId?.(executionContext) === sessionId &&
|
|
185
|
+
ports.registration.isContextActive?.(ctx) !== false &&
|
|
186
|
+
ports.registration.isContextActive?.(executionContext) !== false)
|
|
187
|
+
readyContext = {
|
|
188
|
+
generation,
|
|
189
|
+
ctx: executionContext,
|
|
190
|
+
sessionGeneration,
|
|
191
|
+
sessionId,
|
|
192
|
+
};
|
|
174
193
|
};
|
|
175
194
|
const sharedRuntimeDeps = {
|
|
176
195
|
instanceId: ports.instanceId,
|
|
@@ -220,7 +239,7 @@ export function createTelegramBusFollowerRuntimeAssembly(ports) {
|
|
|
220
239
|
if (ports.registration.getSessionGeneration?.() !== sessionGeneration ||
|
|
221
240
|
ports.registration.isContextActive?.(ctx) === false)
|
|
222
241
|
return;
|
|
223
|
-
if (!isReadyContext(
|
|
242
|
+
if (!isReadyContext())
|
|
224
243
|
await prepareContext(ctx);
|
|
225
244
|
},
|
|
226
245
|
};
|
|
@@ -290,6 +309,7 @@ export function createTelegramBusFollowerWorkspaceRestoreHandler(deps) {
|
|
|
290
309
|
const captured = deps.getContextAuthority(ctx);
|
|
291
310
|
if (!captured)
|
|
292
311
|
throw new Error("Stale Telegram follower Restore context.");
|
|
312
|
+
const snapshotStore = deps.topicTargetStore, liveSnapshot = snapshotStore.withWorkspaceLiveRebindSnapshot, restoreSnapshot = snapshotStore.withWorkspaceRestoreSnapshot;
|
|
293
313
|
const authority = structuredClone(captured);
|
|
294
314
|
const request = structuredClone({
|
|
295
315
|
operationId: input.operationId,
|
|
@@ -331,6 +351,10 @@ export function createTelegramBusFollowerWorkspaceRestoreHandler(deps) {
|
|
|
331
351
|
throw new Error("Invalid Telegram follower Restore authority.");
|
|
332
352
|
const assertCurrent = () => {
|
|
333
353
|
if (!isDeepStrictEqual(deps.getContextAuthority(ctx), authority) ||
|
|
354
|
+
deps.topicTargetStore !== snapshotStore ||
|
|
355
|
+
(liveCurrent
|
|
356
|
+
? snapshotStore.withWorkspaceLiveRebindSnapshot !== liveSnapshot
|
|
357
|
+
: snapshotStore.withWorkspaceRestoreSnapshot !== restoreSnapshot) ||
|
|
334
358
|
liveCurrent?.() === false ||
|
|
335
359
|
(scope &&
|
|
336
360
|
(input.liveRebind !== scope ||
|
|
@@ -351,6 +375,7 @@ export function createTelegramBusFollowerWorkspaceRestoreHandler(deps) {
|
|
|
351
375
|
(liveCurrent
|
|
352
376
|
? !("kind" in intent) ||
|
|
353
377
|
intent.kind !== "live-rebind" ||
|
|
378
|
+
(intent.inputRelease !== undefined && !hasTelegramBusCapability(authority.leaderProtocol, TELEGRAM_BUS_CAPABILITY_PRE_DISPATCH_REBIND_EXECUTION)) ||
|
|
354
379
|
!isDeepStrictEqual([...intent.request.source.updateIds].sort((a, b) => a - b), sourceUpdateIds) ||
|
|
355
380
|
(request.mode === "apply"
|
|
356
381
|
? intent.phase !== "rebound"
|
|
@@ -374,7 +399,7 @@ export function createTelegramBusFollowerWorkspaceRestoreHandler(deps) {
|
|
|
374
399
|
WorkspaceIdentity.normalizeTelegramWorkspacePath(authority.cwd))
|
|
375
400
|
throw new Error("Telegram follower Restore intent does not match this recipient.");
|
|
376
401
|
const expected = structuredClone(intent);
|
|
377
|
-
await
|
|
402
|
+
await snapshotStore.load();
|
|
378
403
|
assertCurrent();
|
|
379
404
|
const observe = (canonical) => {
|
|
380
405
|
if (!isDeepStrictEqual(readIntent(), expected))
|
|
@@ -440,8 +465,19 @@ export function createTelegramBusFollowerWorkspaceRestoreHandler(deps) {
|
|
|
440
465
|
ready,
|
|
441
466
|
};
|
|
442
467
|
};
|
|
468
|
+
const withSnapshot = (observer) => {
|
|
469
|
+
assertCurrent();
|
|
470
|
+
if ("kind" in expected) {
|
|
471
|
+
if (!liveSnapshot)
|
|
472
|
+
throw new Error("Live-rebind read-only Workspace snapshot is unavailable.");
|
|
473
|
+
liveSnapshot.call(snapshotStore, expected, observer);
|
|
474
|
+
}
|
|
475
|
+
else
|
|
476
|
+
restoreSnapshot.call(snapshotStore, expected, observer);
|
|
477
|
+
assertCurrent();
|
|
478
|
+
};
|
|
443
479
|
let result;
|
|
444
|
-
|
|
480
|
+
withSnapshot((snapshot) => {
|
|
445
481
|
result = observe(snapshot);
|
|
446
482
|
});
|
|
447
483
|
if (!result)
|
|
@@ -449,14 +485,15 @@ export function createTelegramBusFollowerWorkspaceRestoreHandler(deps) {
|
|
|
449
485
|
if (release || workObserver) {
|
|
450
486
|
if (!("kind" in expected) ||
|
|
451
487
|
expected.kind !== "live-rebind" ||
|
|
452
|
-
|
|
488
|
+
(release ? !Threads.isTelegramLiveRebindInputAuthorized(expected)
|
|
489
|
+
: !(Threads.isTelegramLiveRebindInputAuthorized(expected) || expected.phase === "released" && expected.inputRelease === "held" && !expected.cleanup)) ||
|
|
453
490
|
!result.ready ||
|
|
454
491
|
request.mode !== "inspect")
|
|
455
492
|
throw new Error("Live-rebind dispatch has no canonical released/local-applied grant.");
|
|
456
|
-
//
|
|
493
|
+
// Live barriers reobserve read-only Workspace evidence; journal reads never borrow publication authority.
|
|
457
494
|
if (workObserver)
|
|
458
495
|
sampleWork = () => {
|
|
459
|
-
|
|
496
|
+
withSnapshot((snapshot) => {
|
|
460
497
|
if (!observe(snapshot).ready)
|
|
461
498
|
throw new Error("Live-rebind work observation lost local target application.");
|
|
462
499
|
workObserver(expected.request.binding.target);
|
|
@@ -466,12 +503,12 @@ export function createTelegramBusFollowerWorkspaceRestoreHandler(deps) {
|
|
|
466
503
|
release?.(() => {
|
|
467
504
|
assertCurrent();
|
|
468
505
|
let ready = false;
|
|
469
|
-
|
|
506
|
+
withSnapshot((snapshot) => {
|
|
470
507
|
ready = observe(snapshot).ready;
|
|
471
508
|
});
|
|
472
509
|
assertCurrent();
|
|
473
510
|
return ready;
|
|
474
|
-
});
|
|
511
|
+
}, { ...expected.request.target });
|
|
475
512
|
}
|
|
476
513
|
return result;
|
|
477
514
|
});
|
|
@@ -566,8 +603,7 @@ export function createTelegramBusFollowerWorkspaceRestoreHandler(deps) {
|
|
|
566
603
|
const value = read.call(deps, expected.operationId, authority.profileBindingKey);
|
|
567
604
|
if (!value ||
|
|
568
605
|
value.kind !== "live-rebind" ||
|
|
569
|
-
value
|
|
570
|
-
value.cleanup ||
|
|
606
|
+
!Threads.isTelegramLiveRebindInputAuthorized(value) ||
|
|
571
607
|
value.request.operationId !== expected.operationId ||
|
|
572
608
|
value.recipient.kind !== "follower" ||
|
|
573
609
|
value.recipient.instanceId !== instanceId ||
|
|
@@ -911,8 +947,12 @@ export function createTelegramBusFollowerSelectedMenuCaller(deps) {
|
|
|
911
947
|
export function createTelegramBusFollowerApiCaller(deps) {
|
|
912
948
|
const getNowMs = deps.getNowMs ?? Date.now;
|
|
913
949
|
const timeoutMs = deps.timeoutMs ?? TELEGRAM_BUS_FOLLOWER_CLIENT_TIMEOUT_MS;
|
|
914
|
-
return async (method, args) => {
|
|
950
|
+
return async (method, args, localOptions) => {
|
|
951
|
+
const assertion = localOptions?.assertAuthority;
|
|
952
|
+
const assertCurrent = () => assertTelegramApiCallAuthority(assertion, false);
|
|
953
|
+
assertCurrent();
|
|
915
954
|
const registration = await resolveTelegramBusFollowerRegistration(deps, timeoutMs);
|
|
955
|
+
assertCurrent();
|
|
916
956
|
const socketPath = resolveTelegramBusSocketPath(deps.socketPath);
|
|
917
957
|
let response;
|
|
918
958
|
try {
|
|
@@ -925,9 +965,11 @@ export function createTelegramBusFollowerApiCaller(deps) {
|
|
|
925
965
|
method,
|
|
926
966
|
args,
|
|
927
967
|
sentAtMs: getNowMs(),
|
|
928
|
-
});
|
|
968
|
+
}, assertion ? assertCurrent : undefined);
|
|
929
969
|
}
|
|
930
970
|
catch (error) {
|
|
971
|
+
if (error instanceof TelegramBusLocalAuthorityError)
|
|
972
|
+
throw new TelegramApiAuthorityError(error.requestIssued);
|
|
931
973
|
const apiMethod = (method === "call" || method === "callMultipart") &&
|
|
932
974
|
typeof args[0] === "string"
|
|
933
975
|
? args[0]
|
|
@@ -1222,141 +1264,250 @@ export function createTelegramBusFollowerRegistrationState(options = {}) {
|
|
|
1222
1264
|
},
|
|
1223
1265
|
};
|
|
1224
1266
|
}
|
|
1267
|
+
export function createTelegramBusFollowerRecoveryState() {
|
|
1268
|
+
return {
|
|
1269
|
+
epoch: 0,
|
|
1270
|
+
halted: false,
|
|
1271
|
+
recovering: false,
|
|
1272
|
+
retryId: null,
|
|
1273
|
+
nextRetryId: 0,
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
/**
|
|
1277
|
+
* Pure follower-recovery lifecycle. An operator stop halts it and a connect resumes it, each in a new epoch: work
|
|
1278
|
+
* from the old epoch, a recovery still awaiting or a retry still armed, can no longer register or promote.
|
|
1279
|
+
*/
|
|
1280
|
+
export function transitionTelegramBusFollowerRecovery(state, event) {
|
|
1281
|
+
const cancelRetry = state.retryId === null
|
|
1282
|
+
? []
|
|
1283
|
+
: [{ type: "cancelRetry", retryId: state.retryId }];
|
|
1284
|
+
const start = (from) => from.halted || from.recovering
|
|
1285
|
+
? { state: from, effects: [] }
|
|
1286
|
+
: {
|
|
1287
|
+
state: { ...from, recovering: true },
|
|
1288
|
+
effects: [{ type: "runRecovery", epoch: from.epoch }],
|
|
1289
|
+
};
|
|
1290
|
+
switch (event.type) {
|
|
1291
|
+
case "recoverRequested":
|
|
1292
|
+
return start(state);
|
|
1293
|
+
case "recoverySettled":
|
|
1294
|
+
return event.epoch === state.epoch
|
|
1295
|
+
? { state: { ...state, recovering: false }, effects: [] }
|
|
1296
|
+
: { state, effects: [] };
|
|
1297
|
+
case "retryRequested": {
|
|
1298
|
+
if (event.epoch !== state.epoch || state.halted)
|
|
1299
|
+
return { state, effects: [] };
|
|
1300
|
+
const retryId = state.nextRetryId + 1;
|
|
1301
|
+
return {
|
|
1302
|
+
state: { ...state, retryId, nextRetryId: retryId },
|
|
1303
|
+
effects: [...cancelRetry, { type: "armRetry", retryId }],
|
|
1304
|
+
};
|
|
1305
|
+
}
|
|
1306
|
+
case "retryFired":
|
|
1307
|
+
if (state.retryId !== event.retryId)
|
|
1308
|
+
return { state, effects: [] };
|
|
1309
|
+
return start({ ...state, retryId: null });
|
|
1310
|
+
case "halted":
|
|
1311
|
+
case "resumed":
|
|
1312
|
+
return {
|
|
1313
|
+
state: {
|
|
1314
|
+
...state,
|
|
1315
|
+
epoch: state.epoch + 1,
|
|
1316
|
+
halted: event.type === "halted",
|
|
1317
|
+
recovering: false,
|
|
1318
|
+
retryId: null,
|
|
1319
|
+
},
|
|
1320
|
+
effects: cancelRetry,
|
|
1321
|
+
};
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1225
1324
|
export function createTelegramBusFollowerHeartbeatRecoveryHandler(deps) {
|
|
1226
1325
|
// Promotion stops registration internally and must keep retrying; only an operator stop or a confirmed
|
|
1227
1326
|
// switch to classic mode halts recovery, so it never re-registers behind a disconnect.
|
|
1228
|
-
let halted = false;
|
|
1229
1327
|
const promotionGraceMs = deps.promotionGraceMs ?? TELEGRAM_BUS_FOLLOWER_PROMOTION_GRACE_MS;
|
|
1230
1328
|
const sleep = deps.sleep ??
|
|
1231
1329
|
((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
1330
|
+
// A cancelled retry still fires through an injected scheduler; its id no longer matches and it does nothing.
|
|
1232
1331
|
const scheduleRetry = deps.scheduleRetry ??
|
|
1233
1332
|
((retry, delayMs) => {
|
|
1234
1333
|
const timer = setTimeout(retry, delayMs);
|
|
1235
1334
|
timer.unref?.();
|
|
1236
1335
|
});
|
|
1237
|
-
|
|
1238
|
-
|
|
1336
|
+
const retryRequests = new Map();
|
|
1337
|
+
let pendingRetry;
|
|
1338
|
+
let request;
|
|
1339
|
+
let running = Promise.resolve();
|
|
1340
|
+
const machine = Machine.createTelegramMachineRunner({
|
|
1341
|
+
initial: createTelegramBusFollowerRecoveryState(),
|
|
1342
|
+
transition: transitionTelegramBusFollowerRecovery,
|
|
1343
|
+
run(effect) {
|
|
1344
|
+
switch (effect.type) {
|
|
1345
|
+
case "runRecovery": {
|
|
1346
|
+
const started = request;
|
|
1347
|
+
running = runRecovery(started, effect.epoch).finally(() => machine.dispatch({ type: "recoverySettled", epoch: effect.epoch }));
|
|
1348
|
+
return;
|
|
1349
|
+
}
|
|
1350
|
+
case "armRetry": {
|
|
1351
|
+
const retryId = effect.retryId;
|
|
1352
|
+
retryRequests.set(retryId, pendingRetry);
|
|
1353
|
+
const fire = () => {
|
|
1354
|
+
const carried = retryRequests.get(retryId);
|
|
1355
|
+
if (!carried || machine.state.retryId !== retryId)
|
|
1356
|
+
return;
|
|
1357
|
+
const activeCtx = deps.getActiveContext
|
|
1358
|
+
? deps.getActiveContext()
|
|
1359
|
+
: carried.ctx;
|
|
1360
|
+
if (!activeCtx) {
|
|
1361
|
+
scheduleRetry(fire, promotionGraceMs);
|
|
1362
|
+
return;
|
|
1363
|
+
}
|
|
1364
|
+
retryRequests.delete(retryId);
|
|
1365
|
+
void requestRecovery({ ...carried, ctx: activeCtx }, {
|
|
1366
|
+
type: "retryFired",
|
|
1367
|
+
retryId,
|
|
1368
|
+
});
|
|
1369
|
+
};
|
|
1370
|
+
scheduleRetry(fire, promotionGraceMs);
|
|
1371
|
+
return;
|
|
1372
|
+
}
|
|
1373
|
+
case "cancelRetry":
|
|
1374
|
+
retryRequests.delete(effect.retryId);
|
|
1375
|
+
return;
|
|
1376
|
+
}
|
|
1377
|
+
},
|
|
1378
|
+
});
|
|
1379
|
+
const requestRecovery = (next, event) => {
|
|
1380
|
+
const before = running;
|
|
1381
|
+
request = next;
|
|
1239
1382
|
try {
|
|
1240
|
-
|
|
1383
|
+
machine.dispatch(event);
|
|
1241
1384
|
}
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
throw error;
|
|
1245
|
-
deps.recordRuntimeEvent("bus", error, {
|
|
1246
|
-
phase: "follower-stale-context-status",
|
|
1247
|
-
});
|
|
1385
|
+
finally {
|
|
1386
|
+
request = undefined;
|
|
1248
1387
|
}
|
|
1388
|
+
return running === before ? Promise.resolve() : running;
|
|
1249
1389
|
};
|
|
1250
|
-
const
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1390
|
+
const runRecovery = async ({ reason: error, ctx, binding: carriedBinding }, epoch) => {
|
|
1391
|
+
const isCurrent = () => machine.state.epoch === epoch && !machine.state.halted;
|
|
1392
|
+
const scheduleRecovery = (reason, fallbackCtx, binding) => {
|
|
1393
|
+
pendingRetry = { reason, ctx: fallbackCtx, binding };
|
|
1394
|
+
try {
|
|
1395
|
+
machine.dispatch({ type: "retryRequested", epoch });
|
|
1396
|
+
}
|
|
1397
|
+
finally {
|
|
1398
|
+
pendingRetry = undefined;
|
|
1399
|
+
}
|
|
1400
|
+
};
|
|
1401
|
+
const safeUpdateStatus = (ctx) => {
|
|
1402
|
+
try {
|
|
1403
|
+
deps.updateStatus(ctx);
|
|
1404
|
+
}
|
|
1405
|
+
catch (error) {
|
|
1406
|
+
if (!isPiStaleContextError(error))
|
|
1407
|
+
throw error;
|
|
1408
|
+
deps.recordRuntimeEvent("bus", error, {
|
|
1409
|
+
phase: "follower-stale-context-status",
|
|
1410
|
+
});
|
|
1411
|
+
}
|
|
1412
|
+
};
|
|
1413
|
+
const clearRegisteredState = (ctx) => {
|
|
1414
|
+
deps.registrationState.setRegistered(false);
|
|
1415
|
+
safeUpdateStatus(ctx);
|
|
1416
|
+
};
|
|
1417
|
+
const tryRegisterWithLeader = async (ctx, leader, phase, binding) => {
|
|
1418
|
+
if (!isCurrent())
|
|
1260
1419
|
return false;
|
|
1261
|
-
|
|
1262
|
-
deps
|
|
1420
|
+
try {
|
|
1421
|
+
const restored = await deps
|
|
1422
|
+
.getRegistrationRuntime()
|
|
1423
|
+
.registerWithLeader(ctx, leader, binding?.target ? { target: binding.target } : undefined);
|
|
1424
|
+
// Registration owns its attempt cleanup; an obsolete recovery cannot stop the shared replacement runtime.
|
|
1425
|
+
if (!restored || !isCurrent())
|
|
1426
|
+
return false;
|
|
1427
|
+
deps.setLifecyclePhase(undefined);
|
|
1428
|
+
safeUpdateStatus(ctx);
|
|
1429
|
+
deps.recordRuntimeEvent("bus", "Telegram follower registration restored", {
|
|
1430
|
+
phase,
|
|
1431
|
+
});
|
|
1432
|
+
return true;
|
|
1433
|
+
}
|
|
1434
|
+
catch (error) {
|
|
1435
|
+
if (!isCurrent())
|
|
1436
|
+
return false;
|
|
1437
|
+
clearRegisteredState(ctx);
|
|
1438
|
+
deps.recordRuntimeEvent("bus", error, { phase });
|
|
1263
1439
|
return false;
|
|
1264
1440
|
}
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
deps.
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
clearRegisteredState(ctx);
|
|
1274
|
-
deps.recordRuntimeEvent("bus", error, { phase });
|
|
1275
|
-
return false;
|
|
1276
|
-
}
|
|
1277
|
-
};
|
|
1278
|
-
const snapshotBinding = () => ({
|
|
1279
|
-
target: deps.registrationState.getTarget(),
|
|
1280
|
-
slot: deps.registrationState.getSlot(),
|
|
1281
|
-
threadName: deps.registrationState.getThreadName(),
|
|
1282
|
-
});
|
|
1283
|
-
const scheduleRecovery = (reason, fallbackCtx, binding) => {
|
|
1284
|
-
const retry = () => {
|
|
1285
|
-
if (halted)
|
|
1441
|
+
};
|
|
1442
|
+
const snapshotBinding = () => ({
|
|
1443
|
+
target: deps.registrationState.getTarget(),
|
|
1444
|
+
slot: deps.registrationState.getSlot(),
|
|
1445
|
+
threadName: deps.registrationState.getThreadName(),
|
|
1446
|
+
});
|
|
1447
|
+
const promoteToLeader = async (reason, ctx, binding, election) => {
|
|
1448
|
+
if (!isCurrent())
|
|
1286
1449
|
return;
|
|
1287
|
-
const activeCtx = deps.getActiveContext
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
if (!activeCtx) {
|
|
1291
|
-
scheduleRetry(retry, promotionGraceMs);
|
|
1450
|
+
const activeCtx = deps.getActiveContext?.();
|
|
1451
|
+
if (deps.getActiveContext && activeCtx !== ctx) {
|
|
1452
|
+
scheduleRecovery(reason, ctx, binding);
|
|
1292
1453
|
return;
|
|
1293
1454
|
}
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
const promoteToLeader = async (reason, ctx, binding, election) => {
|
|
1299
|
-
const activeCtx = deps.getActiveContext?.();
|
|
1300
|
-
if (deps.getActiveContext && activeCtx !== ctx) {
|
|
1301
|
-
scheduleRecovery(reason, ctx, binding);
|
|
1302
|
-
return;
|
|
1303
|
-
}
|
|
1304
|
-
deps.setLifecyclePhase("electing");
|
|
1305
|
-
safeUpdateStatus(ctx);
|
|
1306
|
-
deps.recordRuntimeEvent("bus", reason, {
|
|
1307
|
-
phase: "follower-promotion-electing",
|
|
1308
|
-
});
|
|
1309
|
-
deps.getRegistrationRuntime().stop();
|
|
1310
|
-
deps.setLifecyclePhase("electing");
|
|
1311
|
-
safeUpdateStatus(ctx);
|
|
1312
|
-
deps.recordRuntimeEvent("bus", "Telegram follower attempting promotion", {
|
|
1313
|
-
phase: "follower-promotion-electing",
|
|
1314
|
-
});
|
|
1315
|
-
const promoted = await deps.promoteToLeader(ctx, binding, election);
|
|
1316
|
-
deps.setLifecyclePhase(undefined);
|
|
1317
|
-
safeUpdateStatus(ctx);
|
|
1318
|
-
deps.recordRuntimeEvent("bus", promoted
|
|
1319
|
-
? "Telegram follower promotion completed"
|
|
1320
|
-
: "Telegram follower promotion lost election", {
|
|
1321
|
-
phase: promoted
|
|
1322
|
-
? "follower-promotion-complete"
|
|
1323
|
-
: "follower-promotion-lost",
|
|
1324
|
-
});
|
|
1325
|
-
if (!promoted)
|
|
1326
|
-
scheduleRecovery(reason, ctx, binding);
|
|
1327
|
-
};
|
|
1328
|
-
const attemptPreferredPromotion = async (reason, ctx, binding, candidateState) => {
|
|
1329
|
-
const slot = binding.slot;
|
|
1330
|
-
const lowerEligibleSlot = slot
|
|
1331
|
-
? deps.registrationState
|
|
1332
|
-
.getEligibleElectionSlots()
|
|
1333
|
-
.find((candidate) => candidate < slot)
|
|
1334
|
-
: undefined;
|
|
1335
|
-
if (lowerEligibleSlot) {
|
|
1336
|
-
deps.recordRuntimeEvent("bus", "Telegram follower deferring to a lower-slot election candidate", {
|
|
1337
|
-
phase: "follower-promotion-slot-priority",
|
|
1338
|
-
slot,
|
|
1339
|
-
lowerEligibleSlot,
|
|
1455
|
+
deps.setLifecyclePhase("electing");
|
|
1456
|
+
safeUpdateStatus(ctx);
|
|
1457
|
+
deps.recordRuntimeEvent("bus", reason, {
|
|
1458
|
+
phase: "follower-promotion-electing",
|
|
1340
1459
|
});
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1460
|
+
deps.getRegistrationRuntime().stop();
|
|
1461
|
+
deps.setLifecyclePhase("electing");
|
|
1462
|
+
safeUpdateStatus(ctx);
|
|
1463
|
+
deps.recordRuntimeEvent("bus", "Telegram follower attempting promotion", {
|
|
1464
|
+
phase: "follower-promotion-electing",
|
|
1465
|
+
});
|
|
1466
|
+
const promoted = await deps.promoteToLeader(ctx, binding, election);
|
|
1467
|
+
if (!isCurrent())
|
|
1347
1468
|
return;
|
|
1469
|
+
deps.setLifecyclePhase(undefined);
|
|
1470
|
+
safeUpdateStatus(ctx);
|
|
1471
|
+
deps.recordRuntimeEvent("bus", promoted
|
|
1472
|
+
? "Telegram follower promotion completed"
|
|
1473
|
+
: "Telegram follower promotion lost election", {
|
|
1474
|
+
phase: promoted
|
|
1475
|
+
? "follower-promotion-complete"
|
|
1476
|
+
: "follower-promotion-lost",
|
|
1477
|
+
});
|
|
1478
|
+
if (!promoted)
|
|
1479
|
+
scheduleRecovery(reason, ctx, binding);
|
|
1480
|
+
};
|
|
1481
|
+
const attemptPreferredPromotion = async (reason, ctx, binding, candidateState) => {
|
|
1482
|
+
const slot = binding.slot;
|
|
1483
|
+
const lowerEligibleSlot = slot
|
|
1484
|
+
? deps.registrationState
|
|
1485
|
+
.getEligibleElectionSlots()
|
|
1486
|
+
.find((candidate) => candidate < slot)
|
|
1487
|
+
: undefined;
|
|
1488
|
+
if (lowerEligibleSlot) {
|
|
1489
|
+
deps.recordRuntimeEvent("bus", "Telegram follower deferring to a lower-slot election candidate", {
|
|
1490
|
+
phase: "follower-promotion-slot-priority",
|
|
1491
|
+
slot,
|
|
1492
|
+
lowerEligibleSlot,
|
|
1493
|
+
});
|
|
1494
|
+
await sleep(promotionGraceMs);
|
|
1495
|
+
if (!isCurrent())
|
|
1496
|
+
return;
|
|
1497
|
+
candidateState = deps.getLeaderState();
|
|
1498
|
+
if (candidateState.kind === "active-elsewhere") {
|
|
1499
|
+
if (!(await tryRegisterWithLeader(ctx, candidateState.lock, "follower-register-preferred-successor", binding))) {
|
|
1500
|
+
scheduleRecovery(reason, ctx, binding);
|
|
1501
|
+
}
|
|
1502
|
+
return;
|
|
1503
|
+
}
|
|
1348
1504
|
}
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
}
|
|
1355
|
-
};
|
|
1356
|
-
const recover = async (error, ctx, carriedBinding) => {
|
|
1357
|
-
if (promotionPending || halted)
|
|
1358
|
-
return;
|
|
1359
|
-
promotionPending = true;
|
|
1505
|
+
if (candidateState.kind !== "stale" && candidateState.kind !== "inactive")
|
|
1506
|
+
return;
|
|
1507
|
+
await promoteToLeader(reason, ctx, binding, {
|
|
1508
|
+
expectedOwner: candidateState.kind === "stale" ? candidateState.lock : undefined,
|
|
1509
|
+
});
|
|
1510
|
+
};
|
|
1360
1511
|
deps.registrationState.beginRecovery();
|
|
1361
1512
|
const initialBinding = carriedBinding ?? snapshotBinding();
|
|
1362
1513
|
try {
|
|
@@ -1369,8 +1520,10 @@ export function createTelegramBusFollowerHeartbeatRecoveryHandler(deps) {
|
|
|
1369
1520
|
phase: "follower-thread-mode-read",
|
|
1370
1521
|
});
|
|
1371
1522
|
}
|
|
1523
|
+
if (!isCurrent())
|
|
1524
|
+
return;
|
|
1372
1525
|
if (threadModeDisabled) {
|
|
1373
|
-
halted
|
|
1526
|
+
machine.dispatch({ type: "halted" });
|
|
1374
1527
|
deps.getRegistrationRuntime().stop();
|
|
1375
1528
|
deps.setLifecyclePhase(undefined);
|
|
1376
1529
|
safeUpdateStatus(ctx);
|
|
@@ -1383,16 +1536,23 @@ export function createTelegramBusFollowerHeartbeatRecoveryHandler(deps) {
|
|
|
1383
1536
|
if (await tryRegisterWithLeader(ctx, state.lock, "follower-register-restore", initialBinding)) {
|
|
1384
1537
|
return;
|
|
1385
1538
|
}
|
|
1539
|
+
if (!isCurrent())
|
|
1540
|
+
return;
|
|
1386
1541
|
deps.setLifecyclePhase("electing");
|
|
1387
1542
|
safeUpdateStatus(ctx);
|
|
1388
1543
|
deps.recordRuntimeEvent("bus", "Telegram follower waiting for leader reload recovery", { phase: "follower-promotion-grace" });
|
|
1389
1544
|
await sleep(promotionGraceMs);
|
|
1545
|
+
if (!isCurrent())
|
|
1546
|
+
return;
|
|
1390
1547
|
const graceState = deps.getLeaderState();
|
|
1391
1548
|
if (graceState.kind === "active-elsewhere") {
|
|
1392
1549
|
if (await tryRegisterWithLeader(ctx, graceState.lock, "follower-register-restore-grace", initialBinding)) {
|
|
1393
1550
|
return;
|
|
1394
1551
|
}
|
|
1395
|
-
|
|
1552
|
+
const unresponsive = await deps.proveLeaderUnresponsive?.(graceState.lock);
|
|
1553
|
+
if (!isCurrent())
|
|
1554
|
+
return;
|
|
1555
|
+
if (unresponsive) {
|
|
1396
1556
|
await promoteToLeader(error, ctx, initialBinding, {
|
|
1397
1557
|
expectedOwner: graceState.lock,
|
|
1398
1558
|
unresponsive: true,
|
|
@@ -1415,6 +1575,8 @@ export function createTelegramBusFollowerHeartbeatRecoveryHandler(deps) {
|
|
|
1415
1575
|
await attemptPreferredPromotion(error, ctx, initialBinding, state);
|
|
1416
1576
|
}
|
|
1417
1577
|
catch (promotionError) {
|
|
1578
|
+
if (!isCurrent())
|
|
1579
|
+
return;
|
|
1418
1580
|
deps.setLifecyclePhase(undefined);
|
|
1419
1581
|
safeUpdateStatus(ctx);
|
|
1420
1582
|
if (isPiStaleContextError(promotionError)) {
|
|
@@ -1428,16 +1590,13 @@ export function createTelegramBusFollowerHeartbeatRecoveryHandler(deps) {
|
|
|
1428
1590
|
});
|
|
1429
1591
|
scheduleRecovery(promotionError, ctx, initialBinding);
|
|
1430
1592
|
}
|
|
1431
|
-
finally {
|
|
1432
|
-
promotionPending = false;
|
|
1433
|
-
}
|
|
1434
1593
|
};
|
|
1435
|
-
return Object.assign(
|
|
1594
|
+
return Object.assign((error, ctx) => requestRecovery({ reason: error, ctx }, { type: "recoverRequested" }), {
|
|
1436
1595
|
halt() {
|
|
1437
|
-
halted
|
|
1596
|
+
machine.dispatch({ type: "halted" });
|
|
1438
1597
|
},
|
|
1439
1598
|
resume() {
|
|
1440
|
-
|
|
1599
|
+
machine.dispatch({ type: "resumed" });
|
|
1441
1600
|
},
|
|
1442
1601
|
});
|
|
1443
1602
|
}
|
|
@@ -1504,6 +1663,7 @@ export function createTelegramBusFollowerRegistrationRuntime(deps) {
|
|
|
1504
1663
|
const leaderSocketPath = activeLeaderSocketPath;
|
|
1505
1664
|
const registrationGeneration = activeRegistrationGeneration;
|
|
1506
1665
|
const heartbeatContext = activeContext;
|
|
1666
|
+
const heartbeatTarget = deps.registrationState?.getTarget();
|
|
1507
1667
|
if (!leaderSocketPath || !registrationGeneration)
|
|
1508
1668
|
return;
|
|
1509
1669
|
const isCurrentHeartbeat = () => activeLeaderSocketPath === leaderSocketPath &&
|
|
@@ -1529,6 +1689,7 @@ export function createTelegramBusFollowerRegistrationRuntime(deps) {
|
|
|
1529
1689
|
: [];
|
|
1530
1690
|
deps.registrationState?.setEligibleElectionSlots(slots);
|
|
1531
1691
|
if (typeof heartbeatResult?.displayTitle === "string" &&
|
|
1692
|
+
isDeepStrictEqual(deps.registrationState?.getTarget(), heartbeatTarget) &&
|
|
1532
1693
|
deps.registrationState?.setDisplayTitle(heartbeatResult.displayTitle, registrationGeneration) &&
|
|
1533
1694
|
heartbeatContext) {
|
|
1534
1695
|
try {
|
|
@@ -2040,12 +2201,13 @@ export function createTelegramBusFollowerPairedAdmission(deps) {
|
|
|
2040
2201
|
};
|
|
2041
2202
|
}
|
|
2042
2203
|
/** One authenticated local operation request with the shared operation retry policy; no replay semantics. */
|
|
2043
|
-
function sendTelegramBusOperation(socketPath, timeoutMs, envelope) {
|
|
2204
|
+
function sendTelegramBusOperation(socketPath, timeoutMs, envelope, assertAuthority) {
|
|
2044
2205
|
return sendTelegramBusLocalEnvelope({
|
|
2045
2206
|
socketPath,
|
|
2046
2207
|
timeoutMs,
|
|
2047
2208
|
envelope,
|
|
2048
|
-
|
|
2209
|
+
assertAuthority,
|
|
2210
|
+
retry: assertAuthority ? { attempts: 1, delayMs: 0 } : getTelegramBusTransportRetryPolicy({
|
|
2049
2211
|
endpoint: socketPath,
|
|
2050
2212
|
operation: "operation",
|
|
2051
2213
|
}),
|
|
@@ -2358,8 +2520,9 @@ export function createTelegramBusFollowerLiveRebindRuntime(deps) {
|
|
|
2358
2520
|
record.settling ||
|
|
2359
2521
|
!matchesLiveRebindCarrier(record.observation, envelope))
|
|
2360
2522
|
throw new Error("Live-rebind settlement has no exact current carrier.");
|
|
2361
|
-
if (envelope.mode === "observe") {
|
|
2362
|
-
|
|
2523
|
+
if (envelope.mode === "observe" || envelope.mode === "observe-held") {
|
|
2524
|
+
const beforeRelease = envelope.mode === "observe-held";
|
|
2525
|
+
if ((beforeRelease ? !!record.outcome || !record.applyIssued || !record.held.confirmSaved() : record.outcome?.status !== "released") ||
|
|
2363
2526
|
!deps.applyTarget ||
|
|
2364
2527
|
!deps.observeWork ||
|
|
2365
2528
|
!envelope.oldTarget)
|
|
@@ -2390,7 +2553,7 @@ export function createTelegramBusFollowerLiveRebindRuntime(deps) {
|
|
|
2390
2553
|
if (!current() || !work)
|
|
2391
2554
|
throw new Error("Live-rebind work observation is unconfirmed.");
|
|
2392
2555
|
// Unfinished or unconfirmed selected execution stays unknown; later settled delivery cannot clear it.
|
|
2393
|
-
if (commandWork && status.result !== "completed")
|
|
2556
|
+
if (!beforeRelease && commandWork && status.result !== "completed")
|
|
2394
2557
|
work.unknown = true;
|
|
2395
2558
|
if (!current())
|
|
2396
2559
|
throw new Error("Live-rebind work observer authority changed.");
|
|
@@ -2439,9 +2602,11 @@ export function createTelegramBusFollowerLiveRebindRuntime(deps) {
|
|
|
2439
2602
|
sourceUpdateIds: record.observation.sourceUpdateIds,
|
|
2440
2603
|
isCurrent: current,
|
|
2441
2604
|
...copyLiveRebindCommandTarget(record.observation),
|
|
2442
|
-
release(canRelease) {
|
|
2605
|
+
release(canRelease, target) {
|
|
2606
|
+
if (!target)
|
|
2607
|
+
throw new Error("Live-rebind release target is unconfirmed.");
|
|
2443
2608
|
record.outcome = observation("unknown");
|
|
2444
|
-
record.outcome = record.held.release(() => current() && canRelease())
|
|
2609
|
+
record.outcome = record.held.release(() => current() && canRelease(), target)
|
|
2445
2610
|
? observation("released")
|
|
2446
2611
|
: undefined;
|
|
2447
2612
|
},
|
|
@@ -2704,6 +2869,8 @@ export function createTelegramBusForwardedUpdateReceiverRuntime(deps) {
|
|
|
2704
2869
|
TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SAVE,
|
|
2705
2870
|
...(apply ? [TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_APPLY] : []),
|
|
2706
2871
|
...(settle ? [TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SETTLE] : []),
|
|
2872
|
+
...(envelope.kind === "leader.settleLiveRebind" && envelope.mode === "observe-held"
|
|
2873
|
+
? [TELEGRAM_BUS_CAPABILITY_PRE_DISPATCH_REBIND_EXECUTION] : []),
|
|
2707
2874
|
...(commandInput
|
|
2708
2875
|
? [
|
|
2709
2876
|
TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_APPLY,
|