@llblab/pi-telegram 0.52.2 → 0.53.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +9 -7
- package/BACKLOG.md +1 -18
- package/CHANGELOG.md +11 -0
- package/README.md +7 -11
- package/api/commands.ts +3 -0
- package/dist/api/commands.d.ts +1 -1
- package/dist/lib/activity-verbosity.d.ts +1 -5
- package/dist/lib/activity-verbosity.js +15 -42
- package/dist/lib/activity.d.ts +9 -0
- package/dist/lib/activity.js +39 -10
- package/dist/lib/agent-messages.js +7 -3
- package/dist/lib/bindings.d.ts +43 -5
- package/dist/lib/bindings.js +163 -20
- package/dist/lib/bus-api.d.ts +12 -0
- package/dist/lib/bus-api.js +182 -79
- package/dist/lib/bus-follower.d.ts +111 -31
- package/dist/lib/bus-follower.js +1324 -359
- package/dist/lib/bus-leader.d.ts +41 -91
- package/dist/lib/bus-leader.js +969 -637
- package/dist/lib/bus-transport.js +16 -9
- package/dist/lib/bus.d.ts +215 -18
- package/dist/lib/bus.js +1048 -327
- package/dist/lib/channel-posts.d.ts +5 -0
- package/dist/lib/channel-posts.js +434 -261
- package/dist/lib/command-templates.d.ts +26 -5
- package/dist/lib/command-templates.js +92 -176
- package/dist/lib/commands.d.ts +168 -82
- package/dist/lib/commands.js +1409 -291
- package/dist/lib/config.d.ts +1 -8
- package/dist/lib/config.js +34 -76
- package/dist/lib/delivery.d.ts +2 -2
- package/dist/lib/delivery.js +16 -16
- package/dist/lib/extension.js +639 -171
- package/dist/lib/generative-app-worker.mjs +19 -7
- package/dist/lib/generative-apps.d.ts +0 -5
- package/dist/lib/generative-apps.js +61 -49
- package/dist/lib/inbound.d.ts +5 -0
- package/dist/lib/inbound.js +35 -59
- package/dist/lib/journal.d.ts +33 -3
- package/dist/lib/journal.js +2063 -782
- package/dist/lib/lifecycle.d.ts +2 -6
- package/dist/lib/lifecycle.js +75 -58
- package/dist/lib/locks.d.ts +29 -3
- package/dist/lib/locks.js +289 -108
- package/dist/lib/logging.js +50 -14
- package/dist/lib/media.d.ts +22 -1
- package/dist/lib/media.js +46 -28
- package/dist/lib/menu-model.d.ts +19 -5
- package/dist/lib/menu-model.js +53 -11
- package/dist/lib/menu-queue.d.ts +5 -4
- package/dist/lib/menu-queue.js +45 -63
- package/dist/lib/menu-settings.d.ts +5 -4
- package/dist/lib/menu-settings.js +114 -78
- package/dist/lib/menu-status.d.ts +1 -1
- package/dist/lib/menu-status.js +8 -25
- package/dist/lib/menu-thinking.d.ts +6 -1
- package/dist/lib/menu-thinking.js +14 -21
- package/dist/lib/menu.d.ts +8 -61
- package/dist/lib/menu.js +92 -164
- package/dist/lib/model.d.ts +1 -1
- package/dist/lib/model.js +2 -16
- package/dist/lib/outbound-attachments.d.ts +6 -27
- package/dist/lib/outbound-attachments.js +45 -38
- package/dist/lib/outbound-buttons.js +22 -16
- package/dist/lib/outbound-markup.d.ts +0 -2
- package/dist/lib/outbound-markup.js +51 -64
- package/dist/lib/outbound-voice.js +6 -3
- package/dist/lib/outbound.d.ts +6 -6
- package/dist/lib/outbound.js +61 -51
- package/dist/lib/ownership.js +6 -2
- package/dist/lib/paths.d.ts +5 -0
- package/dist/lib/paths.js +37 -21
- package/dist/lib/pi.d.ts +2 -0
- package/dist/lib/pi.js +8 -1
- package/dist/lib/polling.d.ts +21 -37
- package/dist/lib/polling.js +59 -50
- package/dist/lib/preview.js +39 -19
- package/dist/lib/process-identity.d.ts +24 -0
- package/dist/lib/process-identity.js +133 -0
- package/dist/lib/prompts.js +2 -7
- package/dist/lib/queue.d.ts +14 -16
- package/dist/lib/queue.js +249 -126
- package/dist/lib/recovery.d.ts +2 -83
- package/dist/lib/recovery.js +20 -274
- package/dist/lib/replies.d.ts +16 -24
- package/dist/lib/replies.js +99 -116
- package/dist/lib/routing.d.ts +120 -7
- package/dist/lib/routing.js +4667 -1152
- package/dist/lib/runtime.d.ts +1 -11
- package/dist/lib/sections.d.ts +4 -29
- package/dist/lib/sections.js +7 -20
- package/dist/lib/skills.js +4 -3
- package/dist/lib/status.js +86 -57
- package/dist/lib/sync.d.ts +1 -3
- package/dist/lib/sync.js +67 -43
- package/dist/lib/target.d.ts +4 -0
- package/dist/lib/target.js +18 -0
- package/dist/lib/telegram-api.d.ts +18 -13
- package/dist/lib/telegram-api.js +148 -53
- package/dist/lib/text-groups.js +4 -39
- package/dist/lib/thread-cleanup-manager.d.ts +6 -27
- package/dist/lib/thread-cleanup-manager.js +369 -186
- package/dist/lib/thread-display.d.ts +1 -2
- package/dist/lib/thread-display.js +58 -38
- package/dist/lib/thread-naming.d.ts +28 -8
- package/dist/lib/thread-naming.js +175 -66
- package/dist/lib/thread-reconciler.d.ts +42 -16
- package/dist/lib/thread-reconciler.js +90 -50
- package/dist/lib/threads.d.ts +82 -20
- package/dist/lib/threads.js +2247 -855
- package/dist/lib/time-injection.js +1 -2
- package/dist/lib/turns.js +1 -19
- package/dist/lib/updates.d.ts +169 -80
- package/dist/lib/updates.js +2418 -641
- package/dist/lib/voice.d.ts +7 -22
- package/dist/lib/voice.js +7 -22
- package/dist/lib/wire.js +1 -1
- package/dist/lib/workspace-admission.d.ts +3 -1
- package/dist/lib/workspace-admission.js +425 -211
- package/dist/lib/workspace-identity.js +8 -13
- package/dist/lib/workspace-retirement.d.ts +1 -1
- package/dist/lib/workspace-retirement.js +328 -140
- package/dist/lib/workspace-slots.js +8 -4
- package/dist/package.json +1 -1
- package/docs/architecture.md +31 -29
- package/docs/inbound.md +1 -1
- package/docs/multi-instance-bus.md +15 -3
- package/docs/outbound.md +1 -1
- package/docs/public-api.md +29 -1
- package/docs/ui-style.md +25 -8
- package/lib/activity-verbosity.ts +59 -82
- package/lib/activity.ts +146 -102
- package/lib/agent-messages.ts +12 -5
- package/lib/bindings.ts +389 -82
- package/lib/bus-api.ts +263 -102
- package/lib/bus-follower.ts +2440 -601
- package/lib/bus-leader.ts +2209 -1290
- package/lib/bus-transport.ts +46 -12
- package/lib/bus.ts +1673 -444
- package/lib/channel-posts.ts +848 -327
- package/lib/command-templates.ts +152 -265
- package/lib/commands.ts +2398 -555
- package/lib/config.ts +177 -170
- package/lib/delivery.ts +41 -29
- package/lib/extension.ts +1206 -433
- package/lib/generative-app-worker.mjs +19 -7
- package/lib/generative-apps.ts +324 -211
- package/lib/inbound.ts +95 -97
- package/lib/journal.ts +5311 -1763
- package/lib/keyboard.ts +1 -3
- package/lib/lifecycle.ts +128 -78
- package/lib/locks.ts +677 -203
- package/lib/logging.ts +147 -58
- package/lib/media.ts +82 -43
- package/lib/menu-model.ts +85 -18
- package/lib/menu-queue.ts +88 -129
- package/lib/menu-settings.ts +260 -155
- package/lib/menu-status.ts +20 -60
- package/lib/menu-thinking.ts +41 -56
- package/lib/menu.ts +112 -285
- package/lib/model.ts +8 -22
- package/lib/outbound-attachments.ts +156 -125
- package/lib/outbound-buttons.ts +76 -49
- package/lib/outbound-markup.ts +96 -94
- package/lib/outbound-voice.ts +33 -27
- package/lib/outbound.ts +125 -125
- package/lib/ownership.ts +19 -10
- package/lib/paths.ts +148 -49
- package/lib/pi.ts +35 -15
- package/lib/polling.ts +144 -147
- package/lib/preview.ts +93 -40
- package/lib/process-identity.ts +164 -0
- package/lib/prompts.ts +4 -11
- package/lib/queue.ts +511 -300
- package/lib/recovery.ts +50 -454
- package/lib/rendering.ts +1 -3
- package/lib/replies.ts +248 -209
- package/lib/routing.ts +8509 -2186
- package/lib/runtime.ts +1 -12
- package/lib/sections.ts +24 -89
- package/lib/setup.ts +1 -3
- package/lib/skills.ts +9 -4
- package/lib/status.ts +215 -110
- package/lib/sync.ts +448 -361
- package/lib/target.ts +22 -3
- package/lib/telegram-api.ts +302 -127
- package/lib/text-groups.ts +5 -39
- package/lib/thread-cleanup-manager.ts +833 -331
- package/lib/thread-display.ts +213 -97
- package/lib/thread-naming.ts +232 -87
- package/lib/thread-reconciler.ts +155 -72
- package/lib/threads.ts +5318 -1845
- package/lib/time-injection.ts +1 -4
- package/lib/turns.ts +15 -28
- package/lib/updates.ts +5184 -1612
- package/lib/voice.ts +7 -35
- package/lib/wire.ts +1 -1
- package/lib/workspace-admission.ts +996 -416
- package/lib/workspace-identity.ts +14 -17
- package/lib/workspace-retirement.ts +878 -376
- package/lib/workspace-slots.ts +22 -13
- package/package.json +1 -1
- package/scripts/check-downgrade.mjs +0 -472
- package/scripts/measure-workspace.mjs +0 -108
package/dist/lib/updates.js
CHANGED
|
@@ -5,11 +5,12 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { randomUUID } from "node:crypto";
|
|
7
7
|
import { isDeepStrictEqual } from "node:util";
|
|
8
|
-
import { isWireRecord as isTelegramUpdateAdmissionRecord, isNonEmptyWireString as isTelegramUpdateAdmissionString, } from "./wire.js";
|
|
9
|
-
import { createTelegramPrivateTarget, createTelegramThreadTarget, } from "./target.js";
|
|
10
|
-
import { TELEGRAM_UPDATE_JOURNAL_VERSION, TELEGRAM_UPDATE_JOURNAL_EXCLUSION_VERSION, TELEGRAM_UPDATE_JOURNAL_CUSTODY_VERSION, isTelegramUpdateJournalLegacyFamilyVersion, TELEGRAM_UPDATE_JOURNAL_FAILURE_CLASS_MAX_LENGTH, TELEGRAM_UPDATE_JOURNAL_FAILURE_SUMMARY_MAX_LENGTH, TELEGRAM_UPDATE_JOURNAL_QUEUE_OWNER_ID_MAX_LENGTH, areTelegramUpdateJournalQueueOwnersEqual, createTelegramUpdateJournalEntryDigest, getTelegramUpdateJournalBindingPath, isTelegramUpdateJournalQueueOwnerProcess, parseTelegramUpdateJournalQueueOwner, } from "./journal.js";
|
|
11
|
-
import { areTelegramQueueAdmissionReceiptsEqual, createTelegramQueueHandoff, removeTelegramQueueItemByReceipt, } from "./queue.js";
|
|
12
8
|
import { createTelegramUserPairingRuntime, getTelegramAuthorizationState, } from "./config.js";
|
|
9
|
+
import { TELEGRAM_UPDATE_JOURNAL_CUSTODY_VERSION, TELEGRAM_UPDATE_JOURNAL_EXCLUSION_VERSION, TELEGRAM_UPDATE_JOURNAL_FAILURE_CLASS_MAX_LENGTH, TELEGRAM_UPDATE_JOURNAL_FAILURE_SUMMARY_MAX_LENGTH, TELEGRAM_UPDATE_JOURNAL_QUEUE_OWNER_ID_MAX_LENGTH, TELEGRAM_UPDATE_JOURNAL_VERSION, areTelegramUpdateJournalQueueOwnersEqual, createTelegramUpdateJournalEntryDigest, doesTelegramJournalUpdateNameThread, isTelegramJournalLoneCallbackUpdate, getTelegramUpdateJournalBindingPath, isTelegramUpdateJournalLegacyFamilyVersion, isTelegramUpdateJournalQueueOwnerProcess, parseTelegramUpdateJournalQueueOwner, } from "./journal.js";
|
|
10
|
+
import { isPiStaleContextError } from "./pi.js";
|
|
11
|
+
import { areTelegramQueueAdmissionReceiptsEqual, createTelegramQueueHandoff, removeTelegramQueueItemByReceipt, } from "./queue.js";
|
|
12
|
+
import { createTelegramPrivateTarget, createTelegramThreadTarget, } from "./target.js";
|
|
13
|
+
import { isWireRecord as isTelegramUpdateAdmissionRecord, isNonEmptyWireString as isTelegramUpdateAdmissionString, } from "./wire.js";
|
|
13
14
|
export const TELEGRAM_PRIORITY_REACTIONS = [
|
|
14
15
|
{ id: 10, name: "like", emoji: "👍" },
|
|
15
16
|
{ id: 11, name: "lightning", emoji: "⚡" },
|
|
@@ -26,9 +27,6 @@ export const TELEGRAM_REMOVAL_REACTIONS = [
|
|
|
26
27
|
];
|
|
27
28
|
export const TELEGRAM_PRIORITY_REACTION_EMOJIS = TELEGRAM_PRIORITY_REACTIONS.map((reaction) => reaction.emoji);
|
|
28
29
|
export const TELEGRAM_REMOVAL_REACTION_EMOJIS = TELEGRAM_REMOVAL_REACTIONS.map((reaction) => reaction.emoji);
|
|
29
|
-
function isTelegramMessageIdList(value) {
|
|
30
|
-
return Array.isArray(value) && value.every((item) => Number.isInteger(item));
|
|
31
|
-
}
|
|
32
30
|
export function normalizeTelegramReactionEmoji(emoji) {
|
|
33
31
|
return emoji.replace(/\uFE0F/g, "");
|
|
34
32
|
}
|
|
@@ -65,13 +63,6 @@ function getTelegramQueueReactionTransition(oldReactions, newReactions) {
|
|
|
65
63
|
}
|
|
66
64
|
return transition;
|
|
67
65
|
}
|
|
68
|
-
export function extractDeletedTelegramMessageIds(update) {
|
|
69
|
-
const deletedBusinessMessageIds = update.deleted_business_messages?.message_ids;
|
|
70
|
-
if (isTelegramMessageIdList(deletedBusinessMessageIds)) {
|
|
71
|
-
return deletedBusinessMessageIds;
|
|
72
|
-
}
|
|
73
|
-
return [];
|
|
74
|
-
}
|
|
75
66
|
export function getTelegramTopicLifecycleUpdate(message) {
|
|
76
67
|
if (!message ||
|
|
77
68
|
typeof message.chat.id !== "number" ||
|
|
@@ -111,21 +102,18 @@ export function getAuthorizedTelegramCallbackQuery(update, allowedUserId) {
|
|
|
111
102
|
return query;
|
|
112
103
|
return query.from.id === allowedUserId ? query : undefined;
|
|
113
104
|
}
|
|
114
|
-
|
|
115
|
-
const message = update.message;
|
|
105
|
+
function authorizeTelegramHumanMessage(message, allowedUserId) {
|
|
116
106
|
if (!message || !message.from || message.from.is_bot)
|
|
117
107
|
return undefined;
|
|
118
108
|
if (message.chat.type === "private")
|
|
119
109
|
return message;
|
|
120
110
|
return message.from.id === allowedUserId ? message : undefined;
|
|
121
111
|
}
|
|
112
|
+
export function getAuthorizedTelegramMessage(update, allowedUserId) {
|
|
113
|
+
return authorizeTelegramHumanMessage(update.message, allowedUserId);
|
|
114
|
+
}
|
|
122
115
|
export function getAuthorizedTelegramEditedMessage(update, allowedUserId) {
|
|
123
|
-
|
|
124
|
-
if (!message || !message.from || message.from.is_bot)
|
|
125
|
-
return undefined;
|
|
126
|
-
if (message.chat.type === "private")
|
|
127
|
-
return message;
|
|
128
|
-
return message.from.id === allowedUserId ? message : undefined;
|
|
116
|
+
return authorizeTelegramHumanMessage(update.edited_message, allowedUserId);
|
|
129
117
|
}
|
|
130
118
|
export function getAuthorizedTelegramGuestMessage(update) {
|
|
131
119
|
const guestMessage = update.guest_message;
|
|
@@ -221,21 +209,15 @@ export function bindTelegramUpdateAdmissionSource(update, report, controls) {
|
|
|
221
209
|
}
|
|
222
210
|
/**
|
|
223
211
|
* Conservative read-only census: any retained entry naming this Thread, in any nested Telegram object or state,
|
|
224
|
-
* is unresolved custody
|
|
212
|
+
* is unresolved custody, except a lone button tap, which carries no input (the in-flight Cancel itself).
|
|
213
|
+
* It cannot see updates still in transit before journal append.
|
|
225
214
|
*/
|
|
226
215
|
export function collectTelegramJournalThreadUpdateIds(entries, target) {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
const chat = record.chat;
|
|
233
|
-
if (!chat || typeof chat !== "object" || chat.id === target.chatId)
|
|
234
|
-
return true;
|
|
235
|
-
}
|
|
236
|
-
return Object.values(record).some(child => matches(child, depth + 1));
|
|
237
|
-
};
|
|
238
|
-
return entries.filter(entry => matches(entry.update, 0)).map(entry => entry.updateId).sort((left, right) => left - right);
|
|
216
|
+
return entries
|
|
217
|
+
.filter((entry) => doesTelegramJournalUpdateNameThread(entry.update, target) &&
|
|
218
|
+
!isTelegramJournalLoneCallbackUpdate(entry.update))
|
|
219
|
+
.map((entry) => entry.updateId)
|
|
220
|
+
.sort((left, right) => left - right);
|
|
239
221
|
}
|
|
240
222
|
export function collectTelegramAdmissionSourceUpdateIds(values) {
|
|
241
223
|
const sourceUpdateIds = new Set();
|
|
@@ -253,15 +235,62 @@ export function inspectTelegramDeferredSource(value) {
|
|
|
253
235
|
return undefined;
|
|
254
236
|
return getTelegramUpdateAdmissionBinding(value)?.inspectSource?.();
|
|
255
237
|
}
|
|
238
|
+
/** Read an exact current original for peer preparation without another payload authority. */
|
|
239
|
+
export function inspectTelegramDeferredSourceSnapshot(value) {
|
|
240
|
+
const execution = getTelegramUpdateExecutionFence(value);
|
|
241
|
+
if (execution?.isCurrent() !== true)
|
|
242
|
+
return undefined;
|
|
243
|
+
const snapshot = getTelegramUpdateAdmissionBinding(value)?.inspectSourceSnapshot?.();
|
|
244
|
+
return execution.isCurrent() ? snapshot : undefined;
|
|
245
|
+
}
|
|
246
|
+
/** Exact warm removal ACK only; reporting, source absence and a replaced worker cannot supply it. */
|
|
247
|
+
export function inspectTelegramDeferredSourceCompletion(value) {
|
|
248
|
+
const execution = getTelegramUpdateExecutionFence(value);
|
|
249
|
+
if (execution?.isCurrent() !== true)
|
|
250
|
+
return undefined;
|
|
251
|
+
const source = getTelegramUpdateAdmissionBinding(value)?.inspectCompletion?.();
|
|
252
|
+
return execution.isCurrent() && source ? { ...source } : undefined;
|
|
253
|
+
}
|
|
254
|
+
/** Reuse only live bound deferred originals; raw IDs or cold/adopted input cannot mint this carrier. */
|
|
255
|
+
export function prepareTelegramLiveDeferredInput(values, isCurrent) {
|
|
256
|
+
const carriers = [...values], first = carriers[0], execution = getTelegramUpdateExecutionFence(first);
|
|
257
|
+
const ids = collectTelegramAdmissionSourceUpdateIds(carriers);
|
|
258
|
+
if (!execution ||
|
|
259
|
+
ids.length !== carriers.length ||
|
|
260
|
+
!ids.length ||
|
|
261
|
+
!isCurrent() ||
|
|
262
|
+
carriers.some((value) => getTelegramUpdateExecutionFence(value)?.signal !== execution.signal ||
|
|
263
|
+
getTelegramUpdateExecutionFence(value)?.isCurrent() !== true))
|
|
264
|
+
return undefined;
|
|
265
|
+
const unsettled = () => carriers.every((value) => getTelegramUpdateAdmissionBinding(value)?.isSourceUnsettled?.() ===
|
|
266
|
+
true);
|
|
267
|
+
if (!unsettled())
|
|
268
|
+
return undefined;
|
|
269
|
+
const prepared = getTelegramUpdateAdmissionBinding(first)?.prepareLiveInput?.(ids, () => isCurrent() &&
|
|
270
|
+
carriers.every((value) => getTelegramUpdateExecutionFence(value)?.isCurrent() === true));
|
|
271
|
+
return (prepared && {
|
|
272
|
+
...prepared,
|
|
273
|
+
isCurrent: () => unsettled() && prepared.isCurrent(),
|
|
274
|
+
confirmSaved: () => unsettled() && prepared.confirmSaved(),
|
|
275
|
+
beginRelease: (canRelease) => unsettled() && prepared.beginRelease(() => unsettled() && canRelease()),
|
|
276
|
+
settleTransferred: (canSettle) => unsettled()
|
|
277
|
+
? prepared.settleTransferred(() => unsettled() && canSettle())
|
|
278
|
+
: "protected",
|
|
279
|
+
cancel: () => unsettled() && prepared.cancel(),
|
|
280
|
+
});
|
|
281
|
+
}
|
|
256
282
|
/** A route-owned pre-disposition acceptance publisher; wraps only this carrier, never the shared worker binding. */
|
|
257
283
|
export function bindTelegramUpdateCompletionAcceptance(value, publish) {
|
|
258
284
|
const binding = getTelegramUpdateAdmissionBinding(value);
|
|
259
|
-
if (!binding ||
|
|
285
|
+
if (!binding ||
|
|
286
|
+
getTelegramUpdateExecutionFence(value)?.isCurrent() !== true) {
|
|
260
287
|
throw new TelegramUpdateAdmissionOutcomeError("Telegram completion acceptance requires a live source carrier.");
|
|
261
288
|
}
|
|
262
289
|
let retained;
|
|
263
290
|
let queued = false;
|
|
264
|
-
const scoped = {
|
|
291
|
+
const scoped = {
|
|
292
|
+
...binding,
|
|
293
|
+
report(outcome) {
|
|
265
294
|
if (outcome.kind !== "complete") {
|
|
266
295
|
if (outcome.kind === "queued")
|
|
267
296
|
queued = true;
|
|
@@ -277,19 +306,26 @@ export function bindTelegramUpdateCompletionAcceptance(value, publish) {
|
|
|
277
306
|
}
|
|
278
307
|
if (!retained) {
|
|
279
308
|
const published = validateTelegramUpdateAdmissionOutcome({ kind: "complete", expectedSource: publish() }, binding.sourceUpdateId, new Set([binding.sourceUpdateId]));
|
|
280
|
-
if (published.kind !== "complete" ||
|
|
309
|
+
if (published.kind !== "complete" ||
|
|
310
|
+
!published.expectedSource?.completionSha256) {
|
|
281
311
|
throw new TelegramUpdateAdmissionOutcomeError("Telegram completion acceptance lacks its scoped journal proof.");
|
|
282
312
|
}
|
|
283
313
|
retained = published.expectedSource;
|
|
284
314
|
}
|
|
285
315
|
assertTelegramUpdateExecutionCurrent(value);
|
|
286
|
-
if (outcome.expectedSource &&
|
|
287
|
-
outcome.expectedSource.
|
|
288
|
-
|
|
316
|
+
if (outcome.expectedSource &&
|
|
317
|
+
(outcome.expectedSource.updateId !== retained.updateId ||
|
|
318
|
+
outcome.expectedSource.journalBindingKey !==
|
|
319
|
+
retained.journalBindingKey ||
|
|
320
|
+
outcome.expectedSource.sourceSha256 !== retained.sourceSha256 ||
|
|
321
|
+
(outcome.expectedSource.completionSha256 !== undefined &&
|
|
322
|
+
outcome.expectedSource.completionSha256 !==
|
|
323
|
+
retained.completionSha256))) {
|
|
289
324
|
throw new TelegramUpdateAdmissionOutcomeError("Telegram completion acceptance conflicts with its reported source.");
|
|
290
325
|
}
|
|
291
326
|
binding.report({ kind: "complete", expectedSource: { ...retained } });
|
|
292
|
-
}
|
|
327
|
+
},
|
|
328
|
+
};
|
|
293
329
|
return carryTelegramUpdateExecutionFence(value, bindTelegramUpdateAdmissionCarrier(value, scoped));
|
|
294
330
|
}
|
|
295
331
|
/** Report source completion; true means reported, not a durable settlement acknowledgement. */
|
|
@@ -300,7 +336,10 @@ export function reportTelegramUpdateCompleted(value, expectedSource) {
|
|
|
300
336
|
const execution = getTelegramUpdateExecutionFence(value);
|
|
301
337
|
if (execution && !execution.isCurrent())
|
|
302
338
|
return false;
|
|
303
|
-
binding.report(validateTelegramUpdateAdmissionOutcome({
|
|
339
|
+
binding.report(validateTelegramUpdateAdmissionOutcome({
|
|
340
|
+
kind: "complete",
|
|
341
|
+
...(expectedSource !== undefined ? { expectedSource } : {}),
|
|
342
|
+
}, binding.sourceUpdateId, new Set([binding.sourceUpdateId])));
|
|
304
343
|
return true;
|
|
305
344
|
}
|
|
306
345
|
export function reportTelegramUpdateDeferred(value) {
|
|
@@ -319,7 +358,7 @@ export function abandonTelegramDeferredUpdate(value, authority) {
|
|
|
319
358
|
}
|
|
320
359
|
/** Capability for chooser publication; the eventual action still requires exact deferred authority. */
|
|
321
360
|
export function supportsTelegramDeferredAbandonment(value, journalBindingKey) {
|
|
322
|
-
return getTelegramUpdateAdmissionBinding(value)?.supportsAbandonment?.(journalBindingKey) === true;
|
|
361
|
+
return (getTelegramUpdateAdmissionBinding(value)?.supportsAbandonment?.(journalBindingKey) === true);
|
|
323
362
|
}
|
|
324
363
|
/** Observe protected attempts through a current carrier; the UI still owns human authorization. */
|
|
325
364
|
export function inspectTelegramAbandoningUpdates(value, request) {
|
|
@@ -330,31 +369,97 @@ export function inspectTelegramHistoricalInputs(value, request) {
|
|
|
330
369
|
return getTelegramUpdateAdmissionBinding(value)?.inspectHistorical?.(request);
|
|
331
370
|
}
|
|
332
371
|
export function isTelegramHistoricalInput(value, matchesOriginal) {
|
|
333
|
-
return getTelegramUpdateAdmissionBinding(value)?.isHistorical?.(matchesOriginal) === true;
|
|
372
|
+
return (getTelegramUpdateAdmissionBinding(value)?.isHistorical?.(matchesOriginal) === true);
|
|
334
373
|
}
|
|
335
374
|
/** Routing's last-boundary hold when ownership changed after early classification. */
|
|
336
375
|
export function reportTelegramHistoricalRoutingReview(value) {
|
|
337
|
-
if (!isTelegramHistoricalInput(value) ||
|
|
376
|
+
if (!isTelegramHistoricalInput(value) ||
|
|
377
|
+
getTelegramUpdateExecutionFence(value)?.isCurrent() !== true)
|
|
338
378
|
return false;
|
|
339
|
-
getTelegramUpdateAdmissionBinding(value).report({
|
|
379
|
+
getTelegramUpdateAdmissionBinding(value).report({
|
|
380
|
+
kind: "deferred",
|
|
381
|
+
routingReview: true,
|
|
382
|
+
});
|
|
340
383
|
return true;
|
|
341
384
|
}
|
|
342
385
|
/** Arm only a positively published chooser; the journal owns the immutable hour deadline. */
|
|
343
|
-
export function armTelegramRoutingInputs(values, operatorUserId) {
|
|
386
|
+
export function armTelegramRoutingInputs(values, operatorUserId, chooser) {
|
|
344
387
|
const first = values[0], execution = getTelegramUpdateExecutionFence(first);
|
|
345
|
-
if (!execution ||
|
|
388
|
+
if (!execution ||
|
|
389
|
+
values.some((value) => getTelegramUpdateExecutionFence(value)?.signal !== execution.signal))
|
|
346
390
|
return undefined;
|
|
347
391
|
for (const value of values)
|
|
348
392
|
assertTelegramUpdateExecutionCurrent(value);
|
|
349
393
|
const ids = collectTelegramAdmissionSourceUpdateIds(values);
|
|
350
394
|
if (!ids.length)
|
|
351
395
|
return undefined;
|
|
352
|
-
return getTelegramUpdateAdmissionBinding(first)?.armRoutingInput?.(operatorUserId, ids);
|
|
396
|
+
return getTelegramUpdateAdmissionBinding(first)?.armRoutingInput?.(operatorUserId, ids, chooser);
|
|
397
|
+
}
|
|
398
|
+
/** A routed carrier's saved routing clock; a revived chooser reuses its recorded message. */
|
|
399
|
+
export function getTelegramUpdateRoutingInput(value) {
|
|
400
|
+
return getTelegramUpdateAdmissionBinding(value)?.getRoutingInput?.();
|
|
353
401
|
}
|
|
354
402
|
/** Reserve source execution; an actual choice must also freeze its durable TTL before any effect. */
|
|
355
403
|
export function acquireTelegramUpdateRouting(value, select = false, sourceUpdateIds) {
|
|
356
404
|
assertTelegramUpdateExecutionCurrent(value);
|
|
357
|
-
return getTelegramUpdateAdmissionBinding(value)?.acquireRouting?.(select, sourceUpdateIds) ?? (() => { });
|
|
405
|
+
return (getTelegramUpdateAdmissionBinding(value)?.acquireRouting?.(select, sourceUpdateIds) ?? (() => { }));
|
|
406
|
+
}
|
|
407
|
+
/** Capture existing warm completion authority; only explicit reporting may dispose this exact original. */
|
|
408
|
+
export function prepareTelegramDeferredSourceCompletion(value) {
|
|
409
|
+
const execution = getTelegramUpdateExecutionFence(value), binding = getTelegramUpdateAdmissionBinding(value);
|
|
410
|
+
if (!execution ||
|
|
411
|
+
execution.isCurrent() !== true ||
|
|
412
|
+
!binding?.prepareSourceCompletion ||
|
|
413
|
+
!binding.inspectCompletion ||
|
|
414
|
+
!binding.inspectSource)
|
|
415
|
+
return undefined;
|
|
416
|
+
const prepare = binding.prepareSourceCompletion, inspect = binding.inspectCompletion, readSource = binding.inspectSource, report = binding.report;
|
|
417
|
+
const available = prepare(), source = available && { ...available.source };
|
|
418
|
+
const current = () => !!available &&
|
|
419
|
+
available.isCurrent() &&
|
|
420
|
+
execution.isCurrent() &&
|
|
421
|
+
getTelegramUpdateExecutionFence(value) === execution &&
|
|
422
|
+
getTelegramUpdateAdmissionBinding(value) === binding &&
|
|
423
|
+
binding.prepareSourceCompletion === prepare &&
|
|
424
|
+
binding.inspectCompletion === inspect &&
|
|
425
|
+
binding.inspectSource === readSource &&
|
|
426
|
+
binding.report === report;
|
|
427
|
+
if (!source ||
|
|
428
|
+
!current() ||
|
|
429
|
+
!isDeepStrictEqual(readSource(), source) ||
|
|
430
|
+
!current())
|
|
431
|
+
return undefined;
|
|
432
|
+
let attempted = false, reported = false;
|
|
433
|
+
return {
|
|
434
|
+
get source() {
|
|
435
|
+
return { ...source };
|
|
436
|
+
},
|
|
437
|
+
isCurrent: current,
|
|
438
|
+
reportCompleted() {
|
|
439
|
+
if (attempted)
|
|
440
|
+
return false;
|
|
441
|
+
attempted = true;
|
|
442
|
+
if (!current() || !isDeepStrictEqual(readSource(), source) || !current())
|
|
443
|
+
return false;
|
|
444
|
+
reported = true;
|
|
445
|
+
// Acceptance remains separate from the asynchronously issued worker removal ACK, including after a lost report.
|
|
446
|
+
return reportTelegramUpdateCompleted(value, { ...source });
|
|
447
|
+
},
|
|
448
|
+
inspectCompletion() {
|
|
449
|
+
if (!reported || !current())
|
|
450
|
+
return undefined;
|
|
451
|
+
const completed = inspect();
|
|
452
|
+
return current() && isDeepStrictEqual(completed, source)
|
|
453
|
+
? { ...source }
|
|
454
|
+
: undefined;
|
|
455
|
+
},
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
/** Read-only warm owner availability; no source freeze, admission, report or receipt reconstruction. */
|
|
459
|
+
export function prepareTelegramDeferredQueueAdmission(value) {
|
|
460
|
+
if (getTelegramUpdateExecutionFence(value)?.isCurrent() !== true)
|
|
461
|
+
return undefined;
|
|
462
|
+
return getTelegramUpdateAdmissionBinding(value)?.prepareQueueAdmission?.();
|
|
358
463
|
}
|
|
359
464
|
export function reportTelegramQueueAdmission(values, receipts) {
|
|
360
465
|
const bindings = new Map();
|
|
@@ -492,12 +597,9 @@ export function buildTelegramUpdateExecutionPlan(action) {
|
|
|
492
597
|
export function buildTelegramUpdateExecutionPlanFromUpdate(update, allowedUserId) {
|
|
493
598
|
return buildTelegramUpdateExecutionPlan(buildTelegramUpdateFlowAction(update, allowedUserId));
|
|
494
599
|
}
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
? `<b>🚫 ${TELEGRAM_UNAUTHORIZED_DENIAL_COPY}</b>`
|
|
499
|
-
: `🚫 ${TELEGRAM_UNAUTHORIZED_DENIAL_COPY}`;
|
|
500
|
-
}
|
|
600
|
+
/** The fleeting toast and the in-chat notice are separate copy, each written in its own form. */
|
|
601
|
+
const TELEGRAM_UNAUTHORIZED_DENIAL_TOAST = "Access denied";
|
|
602
|
+
const TELEGRAM_UNAUTHORIZED_DENIAL_NOTICE = "<b>🚫 Access denied.</b>";
|
|
501
603
|
function getTelegramCallbackQueryId(query) {
|
|
502
604
|
return typeof query.id === "string" ? query.id : undefined;
|
|
503
605
|
}
|
|
@@ -529,7 +631,8 @@ function getForeignTelegramMessageOwnership(target, deps) {
|
|
|
529
631
|
function getForeignTelegramCallbackOwnership(query, deps) {
|
|
530
632
|
const messageTarget = getTelegramCallbackMessageTarget(query);
|
|
531
633
|
const currentInstanceId = deps.getCurrentInstanceId?.();
|
|
532
|
-
const knownOwner = messageTarget &&
|
|
634
|
+
const knownOwner = messageTarget &&
|
|
635
|
+
deps.getMessageOwnership?.(messageTarget.chatId, messageTarget.messageId);
|
|
533
636
|
// A locally published chooser stays local when Restore moves its Thread to a follower.
|
|
534
637
|
if (currentInstanceId && knownOwner?.instanceId === currentInstanceId)
|
|
535
638
|
return undefined;
|
|
@@ -644,8 +747,12 @@ export function createTelegramUpdateRuntime(deps) {
|
|
|
644
747
|
export async function handleAuthorizedTelegramReactionUpdate(reactionUpdate, deps) {
|
|
645
748
|
const reactionUser = reactionUpdate.user;
|
|
646
749
|
const allowedUserId = deps.allowedUserId;
|
|
647
|
-
if (allowedUserId === undefined ||
|
|
648
|
-
!
|
|
750
|
+
if (allowedUserId === undefined ||
|
|
751
|
+
!Number.isSafeInteger(allowedUserId) ||
|
|
752
|
+
allowedUserId <= 0 ||
|
|
753
|
+
!reactionUser ||
|
|
754
|
+
reactionUser.is_bot ||
|
|
755
|
+
reactionUser.id !== allowedUserId ||
|
|
649
756
|
reactionUpdate.actor_chat !== undefined)
|
|
650
757
|
return;
|
|
651
758
|
const foreignOwnership = getForeignTelegramMessageOwnership(getTelegramReactionMessageTarget(reactionUpdate), deps);
|
|
@@ -675,11 +782,6 @@ export async function handleAuthorizedTelegramReactionUpdate(reactionUpdate, dep
|
|
|
675
782
|
deps.assertExecutionCurrent?.();
|
|
676
783
|
deps.applyQueuedTelegramTurnReactionByMessageId(reactionUpdate.message_id, reactionTransition, deps.ctx, reactionScope);
|
|
677
784
|
}
|
|
678
|
-
function isTelegramStaleContextError(error) {
|
|
679
|
-
return (error instanceof Error &&
|
|
680
|
-
(error.message.includes("stale after session") ||
|
|
681
|
-
error.message.includes("stale ctx")));
|
|
682
|
-
}
|
|
683
785
|
export async function executeTelegramUpdatePlan(plan, deps) {
|
|
684
786
|
try {
|
|
685
787
|
const assertExecutionCurrent = () => deps.execution?.assertCurrent();
|
|
@@ -712,7 +814,7 @@ export async function executeTelegramUpdatePlan(plan, deps) {
|
|
|
712
814
|
const callbackQueryId = getTelegramCallbackQueryId(plan.query);
|
|
713
815
|
if (callbackQueryId) {
|
|
714
816
|
assertExecutionCurrent();
|
|
715
|
-
await deps.answerCallbackQuery(callbackQueryId,
|
|
817
|
+
await deps.answerCallbackQuery(callbackQueryId, TELEGRAM_UNAUTHORIZED_DENIAL_TOAST);
|
|
716
818
|
}
|
|
717
819
|
return;
|
|
718
820
|
}
|
|
@@ -729,7 +831,7 @@ export async function executeTelegramUpdatePlan(plan, deps) {
|
|
|
729
831
|
try {
|
|
730
832
|
if (callbackQueryId) {
|
|
731
833
|
assertExecutionCurrent();
|
|
732
|
-
await deps.answerCallbackQuery(callbackQueryId, "This Telegram message belongs to another Pi instance
|
|
834
|
+
await deps.answerCallbackQuery(callbackQueryId, "This Telegram message belongs to another Pi instance");
|
|
733
835
|
}
|
|
734
836
|
}
|
|
735
837
|
finally {
|
|
@@ -747,7 +849,7 @@ export async function executeTelegramUpdatePlan(plan, deps) {
|
|
|
747
849
|
if (plan.kind === "guest") {
|
|
748
850
|
if (plan.shouldDeny) {
|
|
749
851
|
assertExecutionCurrent();
|
|
750
|
-
await deps.answerGuestQuery(plan.guestMessage.guest_query_id,
|
|
852
|
+
await deps.answerGuestQuery(plan.guestMessage.guest_query_id, TELEGRAM_UNAUTHORIZED_DENIAL_NOTICE, { parseMode: "HTML" });
|
|
751
853
|
return;
|
|
752
854
|
}
|
|
753
855
|
if (deps.handleAuthorizedTelegramGuestMessage) {
|
|
@@ -766,7 +868,7 @@ export async function executeTelegramUpdatePlan(plan, deps) {
|
|
|
766
868
|
if (plan.shouldDeny || (plan.shouldPair && !pairedNow)) {
|
|
767
869
|
if (replyTarget) {
|
|
768
870
|
assertExecutionCurrent();
|
|
769
|
-
await deps.sendTextReply(replyTarget.chatId, replyTarget.messageId,
|
|
871
|
+
await deps.sendTextReply(replyTarget.chatId, replyTarget.messageId, TELEGRAM_UNAUTHORIZED_DENIAL_NOTICE, { parseMode: "HTML", target: replyTarget });
|
|
770
872
|
}
|
|
771
873
|
return;
|
|
772
874
|
}
|
|
@@ -851,7 +953,7 @@ export async function executeTelegramUpdatePlan(plan, deps) {
|
|
|
851
953
|
assertExecutionCurrent();
|
|
852
954
|
}
|
|
853
955
|
catch (error) {
|
|
854
|
-
if (!
|
|
956
|
+
if (!isPiStaleContextError(error))
|
|
855
957
|
throw error;
|
|
856
958
|
}
|
|
857
959
|
}
|
|
@@ -860,22 +962,35 @@ const TELEGRAM_UPDATE_RETRY_BASE_DELAY_MS = 1_000;
|
|
|
860
962
|
const TELEGRAM_UPDATE_RETRY_MAX_DELAY_MS = 60_000;
|
|
861
963
|
const TELEGRAM_UPDATE_WORKER_BATCH_SIZE = 64;
|
|
862
964
|
function normalizeTelegramQueueSourceCompletions(value, sourceUpdateIds, journalBindingKey, full = true) {
|
|
863
|
-
if (!Array.isArray(value) ||
|
|
965
|
+
if (!Array.isArray(value) ||
|
|
966
|
+
(full
|
|
967
|
+
? value.length !== sourceUpdateIds.size
|
|
968
|
+
: value.length > sourceUpdateIds.size) ||
|
|
969
|
+
value.length === 0)
|
|
864
970
|
throw new Error("Telegram scoped queue completion requires full source coverage.");
|
|
865
971
|
const seen = new Set(), scopes = new Set();
|
|
866
|
-
return value
|
|
867
|
-
|
|
972
|
+
return value
|
|
973
|
+
.map((source) => {
|
|
974
|
+
if (!Number.isSafeInteger(source?.updateId) ||
|
|
975
|
+
!sourceUpdateIds.has(source.updateId) ||
|
|
976
|
+
seen.has(source.updateId)) {
|
|
868
977
|
throw new Error("Telegram scoped queue completion has invalid source membership.");
|
|
869
978
|
}
|
|
870
979
|
const normalized = validateTelegramUpdateAdmissionOutcome({ kind: "complete", expectedSource: source }, source.updateId, sourceUpdateIds);
|
|
871
|
-
if (normalized.kind !== "complete" ||
|
|
872
|
-
normalized.expectedSource
|
|
980
|
+
if (normalized.kind !== "complete" ||
|
|
981
|
+
!normalized.expectedSource?.completionSha256 ||
|
|
982
|
+
normalized.expectedSource.journalBindingKey !== journalBindingKey ||
|
|
983
|
+
scopes.has(normalized.expectedSource.completionSha256)) {
|
|
873
984
|
throw new Error("Telegram scoped queue completion has invalid source scope.");
|
|
874
985
|
}
|
|
875
986
|
seen.add(source.updateId);
|
|
876
987
|
scopes.add(normalized.expectedSource.completionSha256);
|
|
877
|
-
return {
|
|
878
|
-
|
|
988
|
+
return {
|
|
989
|
+
...normalized.expectedSource,
|
|
990
|
+
completionSha256: normalized.expectedSource.completionSha256,
|
|
991
|
+
};
|
|
992
|
+
})
|
|
993
|
+
.sort((a, b) => a.updateId - b.updateId);
|
|
879
994
|
}
|
|
880
995
|
class TelegramUpdateAdmissionOutcomeError extends Error {
|
|
881
996
|
constructor(message) {
|
|
@@ -884,6 +999,30 @@ class TelegramUpdateAdmissionOutcomeError extends Error {
|
|
|
884
999
|
}
|
|
885
1000
|
}
|
|
886
1001
|
const TELEGRAM_UPDATE_WORKER_EXECUTION_ABORTED = Symbol("telegram.update-worker.execution-aborted");
|
|
1002
|
+
/** The receipt's only journal entry when it is exactly this prompt update, queued unchanged under this owner. */
|
|
1003
|
+
function findExactQueuedPromptEntry(snapshot, receiptId, updateId, queueOwner, update) {
|
|
1004
|
+
if (!isTelegramUpdateJournalLegacyFamilyVersion(snapshot.version))
|
|
1005
|
+
return undefined;
|
|
1006
|
+
const entries = snapshot.entries.filter((entry) => entry.queueReceiptId === receiptId);
|
|
1007
|
+
const entry = entries[0];
|
|
1008
|
+
return entries.length === 1 &&
|
|
1009
|
+
entry?.updateId === updateId &&
|
|
1010
|
+
entry.state === "queued" &&
|
|
1011
|
+
entry.queueKind === "prompt" &&
|
|
1012
|
+
!entry.queueHandoff &&
|
|
1013
|
+
isDeepStrictEqual(entry.queueOwner, queueOwner) &&
|
|
1014
|
+
isDeepStrictEqual(entry.update, update)
|
|
1015
|
+
? entry
|
|
1016
|
+
: undefined;
|
|
1017
|
+
}
|
|
1018
|
+
/** A saved chooser clock that proves an untouched, unexpired choice whose chooser message can be revived. */
|
|
1019
|
+
export function isRevivableTelegramRoutingInput(entry, nowMs) {
|
|
1020
|
+
const lifetime = entry.routingInput;
|
|
1021
|
+
return (entry.state === "pending" &&
|
|
1022
|
+
lifetime?.phase === "waiting" &&
|
|
1023
|
+
lifetime.chooser !== undefined &&
|
|
1024
|
+
nowMs < lifetime.expiresAtMs);
|
|
1025
|
+
}
|
|
887
1026
|
function getTelegramUpdateWorkerStateSnapshot(state) {
|
|
888
1027
|
return { ...state };
|
|
889
1028
|
}
|
|
@@ -895,20 +1034,41 @@ function validateTelegramUpdateAdmissionOutcome(value, currentUpdateId, claimabl
|
|
|
895
1034
|
if (value.expectedSource === undefined)
|
|
896
1035
|
return { kind: "complete" };
|
|
897
1036
|
const source = value.expectedSource;
|
|
898
|
-
if (!isTelegramUpdateAdmissionRecord(source) ||
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
1037
|
+
if (!isTelegramUpdateAdmissionRecord(source) ||
|
|
1038
|
+
Object.keys(source).some((key) => ![
|
|
1039
|
+
"journalBindingKey",
|
|
1040
|
+
"updateId",
|
|
1041
|
+
"sourceSha256",
|
|
1042
|
+
"completionSha256",
|
|
1043
|
+
].includes(key)) ||
|
|
1044
|
+
source.updateId !== currentUpdateId ||
|
|
1045
|
+
!isTelegramUpdateAdmissionString(source.journalBindingKey) ||
|
|
1046
|
+
typeof source.sourceSha256 !== "string" ||
|
|
1047
|
+
!/^[a-f0-9]{64}$/u.test(source.sourceSha256) ||
|
|
1048
|
+
(source.completionSha256 !== undefined &&
|
|
1049
|
+
(typeof source.completionSha256 !== "string" ||
|
|
1050
|
+
!/^[a-f0-9]{64}$/u.test(source.completionSha256)))) {
|
|
902
1051
|
throw new TelegramUpdateAdmissionOutcomeError(`Telegram update ${currentUpdateId} returned an invalid completion source.`);
|
|
903
1052
|
}
|
|
904
|
-
return {
|
|
905
|
-
|
|
906
|
-
|
|
1053
|
+
return {
|
|
1054
|
+
kind: "complete",
|
|
1055
|
+
expectedSource: {
|
|
1056
|
+
journalBindingKey: source.journalBindingKey,
|
|
1057
|
+
updateId: currentUpdateId,
|
|
1058
|
+
sourceSha256: source.sourceSha256,
|
|
1059
|
+
...(source.completionSha256 !== undefined
|
|
1060
|
+
? { completionSha256: source.completionSha256 }
|
|
1061
|
+
: {}),
|
|
1062
|
+
},
|
|
1063
|
+
};
|
|
907
1064
|
}
|
|
908
1065
|
if (value.kind === "deferred") {
|
|
909
1066
|
if (value.routingReview !== undefined && value.routingReview !== true)
|
|
910
1067
|
throw new TelegramUpdateAdmissionOutcomeError("Invalid historical review outcome.");
|
|
911
|
-
return {
|
|
1068
|
+
return {
|
|
1069
|
+
kind: "deferred",
|
|
1070
|
+
...(value.routingReview === true ? { routingReview: true } : {}),
|
|
1071
|
+
};
|
|
912
1072
|
}
|
|
913
1073
|
if (value.kind === "queued") {
|
|
914
1074
|
if ((value.queueKind !== "prompt" && value.queueKind !== "control") ||
|
|
@@ -1002,9 +1162,30 @@ function normalizeTelegramUpdateQueueOwnerIdentity(value) {
|
|
|
1002
1162
|
}
|
|
1003
1163
|
return { ...value };
|
|
1004
1164
|
}
|
|
1165
|
+
/** Clear claim, retry and failure projections after the worker forgets its local claims. */
|
|
1166
|
+
function clearTelegramUpdateWorkerClaimState(state, unsettledExecutionCount) {
|
|
1167
|
+
state.deferredClaimCount = 0;
|
|
1168
|
+
delete state.abandoningClaimCount;
|
|
1169
|
+
delete state.historicalClaimCount;
|
|
1170
|
+
state.queuedClaimCount = 0;
|
|
1171
|
+
state.foreignQueuedCount = 0;
|
|
1172
|
+
delete state.foreignQueuedOwner;
|
|
1173
|
+
state.retryWaitCount = 0;
|
|
1174
|
+
state.failedCount = 0;
|
|
1175
|
+
state.nextRetryUpdateId = undefined;
|
|
1176
|
+
state.nextRetryAtMs = undefined;
|
|
1177
|
+
state.nextRetryAttemptCount = undefined;
|
|
1178
|
+
state.nextRetryFailureClass = undefined;
|
|
1179
|
+
state.failedUpdateId = undefined;
|
|
1180
|
+
state.failedFailureId = undefined;
|
|
1181
|
+
state.failedAttemptCount = undefined;
|
|
1182
|
+
state.failedClass = undefined;
|
|
1183
|
+
state.failedSummary = undefined;
|
|
1184
|
+
state.terminalFailureAtMs = undefined;
|
|
1185
|
+
state.unsettledExecutionCount = unsettledExecutionCount;
|
|
1186
|
+
}
|
|
1005
1187
|
export function createTelegramUpdateWorkerRuntime(deps) {
|
|
1006
|
-
if ((deps.scheduleRetry === undefined) !==
|
|
1007
|
-
(deps.cancelRetry === undefined)) {
|
|
1188
|
+
if ((deps.scheduleRetry === undefined) !== (deps.cancelRetry === undefined)) {
|
|
1008
1189
|
throw new Error("Telegram update retry scheduling requires matching schedule and cancel ports.");
|
|
1009
1190
|
}
|
|
1010
1191
|
const getNowMs = deps.getNowMs ?? Date.now;
|
|
@@ -1021,9 +1202,11 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1021
1202
|
}
|
|
1022
1203
|
const yieldToEventLoop = deps.yieldToEventLoop ??
|
|
1023
1204
|
(() => new Promise((resolve) => setTimeout(resolve, 0)));
|
|
1024
|
-
const
|
|
1205
|
+
const queueCompletionPort = deps.journal.completeQueuedExact;
|
|
1206
|
+
const completionReaderPort = deps.journal.inspectSourceCompletion;
|
|
1207
|
+
const completeQueuedExact = queueCompletionPort?.bind(deps.journal);
|
|
1025
1208
|
const completeExact = deps.journal.removeCompletedExact?.bind(deps.journal);
|
|
1026
|
-
const inspectCompletion =
|
|
1209
|
+
const inspectCompletion = completionReaderPort?.bind(deps.journal);
|
|
1027
1210
|
const observeQueuedCompletion = deps.onQueueReceiptCompleted;
|
|
1028
1211
|
const fallbackQueueOwnerInstanceId = `worker-${randomUUID()}`;
|
|
1029
1212
|
const fallbackQueueOwnerProcessId = process.pid > 0 ? process.pid : 1;
|
|
@@ -1042,8 +1225,11 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1042
1225
|
if (!deps.getQueueOwnerIdentity)
|
|
1043
1226
|
return true;
|
|
1044
1227
|
const live = deps.getQueueOwnerIdentity(expected.ctx), captured = expected.queueOwnerIdentity;
|
|
1045
|
-
return !!live &&
|
|
1046
|
-
live.
|
|
1228
|
+
return (!!live &&
|
|
1229
|
+
live.instanceId === captured.instanceId &&
|
|
1230
|
+
live.processId === captured.processId &&
|
|
1231
|
+
live.processBirthId === captured.processBirthId &&
|
|
1232
|
+
live.sessionGeneration === captured.sessionGeneration);
|
|
1047
1233
|
};
|
|
1048
1234
|
const state = {
|
|
1049
1235
|
phase: "stopped",
|
|
@@ -1065,6 +1251,7 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1065
1251
|
const scopedQueueCompletionAttempts = new Map();
|
|
1066
1252
|
const unsettledExecutions = new Set();
|
|
1067
1253
|
let owner;
|
|
1254
|
+
let livePreparation;
|
|
1068
1255
|
let drainPromise;
|
|
1069
1256
|
let pendingSignal = false;
|
|
1070
1257
|
let blocked = false;
|
|
@@ -1091,6 +1278,14 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1091
1278
|
recordRuntimeEvent(error, { phase: "state-observer" });
|
|
1092
1279
|
}
|
|
1093
1280
|
};
|
|
1281
|
+
/** Fresh live input this generation admitted itself: not startup history, no live release issued, not executing. */
|
|
1282
|
+
const isFreshLiveSource = (sourceOwner, updateId) => {
|
|
1283
|
+
const source = deferredSources.get(updateId);
|
|
1284
|
+
return (!sourceOwner.startupUpdateIds?.has(updateId) &&
|
|
1285
|
+
!source?.historical &&
|
|
1286
|
+
!source?.liveReleaseIssued &&
|
|
1287
|
+
!unsettledExecutionsByUpdateId.has(updateId));
|
|
1288
|
+
};
|
|
1094
1289
|
const updateClaimCounts = () => {
|
|
1095
1290
|
let deferredClaimCount = 0;
|
|
1096
1291
|
let queuedClaimCount = 0;
|
|
@@ -1116,6 +1311,10 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1116
1311
|
state.historicalClaimCount = historicalClaimCount;
|
|
1117
1312
|
else
|
|
1118
1313
|
delete state.historicalClaimCount;
|
|
1314
|
+
if (livePreparation && !livePreparation.released)
|
|
1315
|
+
state.preparedInputCount = livePreparation.ids.size;
|
|
1316
|
+
else
|
|
1317
|
+
delete state.preparedInputCount;
|
|
1119
1318
|
};
|
|
1120
1319
|
const releaseDeferredClaims = (updateIds) => {
|
|
1121
1320
|
let changed = false;
|
|
@@ -1167,7 +1366,10 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1167
1366
|
? { journalBindingKey: deps.getJournalBindingKey() }
|
|
1168
1367
|
: {}),
|
|
1169
1368
|
});
|
|
1170
|
-
const
|
|
1369
|
+
const queueAdmissionJournal = deps.journal;
|
|
1370
|
+
const queueAdmissionRead = queueAdmissionJournal.read, queueAdmissionCommit = queueAdmissionJournal.markQueued;
|
|
1371
|
+
const queueAdmissionInspect = deps.journal.isQueueReceiptCurrent;
|
|
1372
|
+
const inspectQueueReceipt = queueAdmissionInspect?.bind(deps.journal);
|
|
1171
1373
|
const inspectQueuedSources = deps.journal.inspectQueuedReceipt?.bind(deps.journal);
|
|
1172
1374
|
const prepareQueueReceipt = deps.beforeQueueReceiptPublished;
|
|
1173
1375
|
const pendingQueuePublications = new Map();
|
|
@@ -1186,7 +1388,8 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1186
1388
|
if (prepareQueueReceipt) {
|
|
1187
1389
|
const pending = pendingQueuePublications.get(normalized.receiptId);
|
|
1188
1390
|
if (pending) {
|
|
1189
|
-
if (pending.owner !== expectedOwner ||
|
|
1391
|
+
if (pending.owner !== expectedOwner ||
|
|
1392
|
+
!areTelegramQueueAdmissionReceiptsEqual(pending.receipt, normalized) ||
|
|
1190
1393
|
!areTelegramUpdateJournalQueueOwnersEqual(pending.queueOwner, queueOwner)) {
|
|
1191
1394
|
throw new TelegramUpdateAdmissionOutcomeError("Telegram queue publication has conflicting in-flight authority.");
|
|
1192
1395
|
}
|
|
@@ -1194,13 +1397,23 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1194
1397
|
return false;
|
|
1195
1398
|
}
|
|
1196
1399
|
const journalBindingKey = deps.getJournalBindingKey?.();
|
|
1197
|
-
const current = () => owner === expectedOwner &&
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1400
|
+
const current = () => owner === expectedOwner &&
|
|
1401
|
+
!expectedOwner.controller.signal.aborted &&
|
|
1402
|
+
deps.hasAuthority(ctx) &&
|
|
1403
|
+
deps.isContextCurrent?.(ctx) !== false &&
|
|
1404
|
+
isQueueOwnerIdentityCurrent(expectedOwner) &&
|
|
1405
|
+
deps.getJournalBindingKey?.() === journalBindingKey;
|
|
1406
|
+
if (!journalBindingKey ||
|
|
1407
|
+
!current() ||
|
|
1408
|
+
!inspectQueueReceipt ||
|
|
1409
|
+
inspectQueueReceipt(normalizeQueueReceipt(normalized), {
|
|
1410
|
+
...queueOwner,
|
|
1411
|
+
}) !== true ||
|
|
1412
|
+
!current()) {
|
|
1201
1413
|
throw new TelegramUpdateAdmissionOutcomeError("Telegram queue publication requires exact current receipt inspection.");
|
|
1202
1414
|
}
|
|
1203
|
-
const task = Promise.resolve()
|
|
1415
|
+
const task = Promise.resolve()
|
|
1416
|
+
.then(async () => {
|
|
1204
1417
|
if (!current())
|
|
1205
1418
|
throw new TelegramUpdateAdmissionOutcomeError("Telegram queue publication authority ended before acceptance.");
|
|
1206
1419
|
const prepared = await prepareQueueReceipt(normalizeQueueReceipt(normalized), { ...queueOwner }, ctx, current);
|
|
@@ -1211,29 +1424,52 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1211
1424
|
if (sourceCompletions.length !== normalized.sourceUpdateIds.length) {
|
|
1212
1425
|
if (!current() || !inspectQueuedSources)
|
|
1213
1426
|
throw new Error("Telegram partial queue scopes require strict current whole-receipt origin inspection.");
|
|
1214
|
-
const expected = {
|
|
1215
|
-
|
|
1427
|
+
const expected = {
|
|
1428
|
+
queueKind: normalized.queueKind,
|
|
1429
|
+
receiptId: normalized.receiptId,
|
|
1430
|
+
sourceUpdateIds: [...normalized.sourceUpdateIds],
|
|
1431
|
+
queueOwner: { ...queueOwner },
|
|
1432
|
+
};
|
|
1216
1433
|
const proof = inspectQueuedSources(structuredClone(expected));
|
|
1217
|
-
if (!current() ||
|
|
1218
|
-
!
|
|
1219
|
-
proof.
|
|
1220
|
-
|
|
1434
|
+
if (!current() ||
|
|
1435
|
+
!proof ||
|
|
1436
|
+
!isDeepStrictEqual(proof.receipt, expected) ||
|
|
1437
|
+
!Array.isArray(proof.sources) ||
|
|
1438
|
+
proof.sources.length !== expected.sourceUpdateIds.length ||
|
|
1439
|
+
proof.sources.some((source, index) => source.updateId !== expected.sourceUpdateIds[index] ||
|
|
1440
|
+
typeof source.sourceSha256 !== "string" ||
|
|
1441
|
+
!/^[a-f0-9]{64}$/u.test(source.sourceSha256)) ||
|
|
1442
|
+
sourceCompletions.some((scope) => proof.sources.find((source) => source.updateId === scope.updateId)?.sourceSha256 !== scope.sourceSha256)) {
|
|
1221
1443
|
throw new Error("Telegram partial queue scope origin was not confirmed under whole-receipt authority.");
|
|
1222
1444
|
}
|
|
1223
1445
|
}
|
|
1224
1446
|
for (const { journalBindingKey: _binding, ...completion } of sourceCompletions) {
|
|
1225
|
-
if (!current() ||
|
|
1447
|
+
if (!current() ||
|
|
1448
|
+
inspectCompletion({ ...completion }) !== undefined ||
|
|
1449
|
+
!current()) {
|
|
1226
1450
|
throw new Error("Telegram queued source completion authority is contradictory.");
|
|
1227
1451
|
}
|
|
1228
1452
|
}
|
|
1229
1453
|
}
|
|
1230
|
-
if (!current() ||
|
|
1454
|
+
if (!current() ||
|
|
1455
|
+
inspectQueueReceipt(normalizeQueueReceipt(normalized), {
|
|
1456
|
+
...queueOwner,
|
|
1457
|
+
}) !== true ||
|
|
1458
|
+
!current()) {
|
|
1231
1459
|
throw new TelegramUpdateAdmissionOutcomeError("Telegram queue publication authority changed after acceptance.");
|
|
1232
1460
|
}
|
|
1233
1461
|
publishReady();
|
|
1234
|
-
})
|
|
1235
|
-
|
|
1236
|
-
|
|
1462
|
+
})
|
|
1463
|
+
.finally(() => {
|
|
1464
|
+
if (pendingQueuePublications.get(normalized.receiptId)?.task === task)
|
|
1465
|
+
pendingQueuePublications.delete(normalized.receiptId);
|
|
1466
|
+
});
|
|
1467
|
+
pendingQueuePublications.set(normalized.receiptId, {
|
|
1468
|
+
receipt: normalizeQueueReceipt(normalized),
|
|
1469
|
+
queueOwner: { ...queueOwner },
|
|
1470
|
+
owner: expectedOwner,
|
|
1471
|
+
task,
|
|
1472
|
+
});
|
|
1237
1473
|
await task;
|
|
1238
1474
|
return true;
|
|
1239
1475
|
}
|
|
@@ -1243,7 +1479,13 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1243
1479
|
committedQueueReceipts.set(receipt.receiptId, {
|
|
1244
1480
|
receipt: normalized,
|
|
1245
1481
|
queueOwner: { ...queueOwner },
|
|
1246
|
-
...(sourceCompletions
|
|
1482
|
+
...(sourceCompletions
|
|
1483
|
+
? {
|
|
1484
|
+
sourceCompletions: sourceCompletions.map((value) => ({
|
|
1485
|
+
...value,
|
|
1486
|
+
})),
|
|
1487
|
+
}
|
|
1488
|
+
: {}),
|
|
1247
1489
|
});
|
|
1248
1490
|
if (sourceCompletions)
|
|
1249
1491
|
requiredScopedQueueReceipts.add(receipt.receiptId);
|
|
@@ -1262,11 +1504,14 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1262
1504
|
if (scopedQueueCompletionAttempts.has(receipt.receiptId))
|
|
1263
1505
|
return undefined;
|
|
1264
1506
|
const committed = committedQueueReceipts.get(receipt.receiptId);
|
|
1265
|
-
if (!committed ||
|
|
1507
|
+
if (!committed ||
|
|
1508
|
+
!areTelegramQueueAdmissionReceiptsEqual(committed.receipt, normalizeQueueReceipt(receipt)))
|
|
1266
1509
|
return undefined;
|
|
1267
1510
|
try {
|
|
1268
1511
|
if (inspectQueueReceipt &&
|
|
1269
|
-
inspectQueueReceipt(normalizeQueueReceipt(committed.receipt), {
|
|
1512
|
+
inspectQueueReceipt(normalizeQueueReceipt(committed.receipt), {
|
|
1513
|
+
...committed.queueOwner,
|
|
1514
|
+
}) !== true) {
|
|
1270
1515
|
return undefined;
|
|
1271
1516
|
}
|
|
1272
1517
|
}
|
|
@@ -1316,7 +1561,8 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1316
1561
|
snapshot.version !== TELEGRAM_UPDATE_JOURNAL_CUSTODY_VERSION) ||
|
|
1317
1562
|
(snapshot.version === TELEGRAM_UPDATE_JOURNAL_CUSTODY_VERSION &&
|
|
1318
1563
|
!deps.executeCustodiedUpdate) ||
|
|
1319
|
-
snapshot.entries.some((entry) => ((snapshot.version === TELEGRAM_UPDATE_JOURNAL_EXCLUSION_VERSION ||
|
|
1564
|
+
snapshot.entries.some((entry) => ((snapshot.version === TELEGRAM_UPDATE_JOURNAL_EXCLUSION_VERSION ||
|
|
1565
|
+
entry.preApprovalExcluded !== undefined) &&
|
|
1320
1566
|
typeof entry.preApprovalExcluded !== "boolean") ||
|
|
1321
1567
|
(entry.preApprovalExcluded === true && entry.state === "queued"))) {
|
|
1322
1568
|
throw new TelegramUpdateAdmissionOutcomeError("Telegram journal snapshot has invalid pairing exclusion evidence.");
|
|
@@ -1338,7 +1584,9 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1338
1584
|
oldestAdmittedAtMs === undefined
|
|
1339
1585
|
? entry.admittedAtMs
|
|
1340
1586
|
: Math.min(oldestAdmittedAtMs, entry.admittedAtMs);
|
|
1341
|
-
if (entry.state === "retry-wait" &&
|
|
1587
|
+
if (entry.state === "retry-wait" &&
|
|
1588
|
+
entry.nextRetryAtMs !== undefined &&
|
|
1589
|
+
!claims.has(entry.updateId)) {
|
|
1342
1590
|
scheduledRetryAtMs =
|
|
1343
1591
|
scheduledRetryAtMs === undefined
|
|
1344
1592
|
? entry.nextRetryAtMs
|
|
@@ -1406,10 +1654,15 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1406
1654
|
}
|
|
1407
1655
|
for (const updateId of deferredSources.keys()) {
|
|
1408
1656
|
const claim = claims.get(updateId);
|
|
1409
|
-
if (claim !== "deferred" &&
|
|
1657
|
+
if (claim !== "deferred" &&
|
|
1658
|
+
claim !== "abandoning" &&
|
|
1659
|
+
claim !== "historical" &&
|
|
1660
|
+
claim !== "retained")
|
|
1410
1661
|
deferredSources.delete(updateId);
|
|
1411
1662
|
}
|
|
1412
1663
|
for (const [receiptId, receipt] of queuedReceiptEntries) {
|
|
1664
|
+
if (receipt.sourceUpdateIds.some((id) => livePreparation?.ids.has(id)))
|
|
1665
|
+
continue;
|
|
1413
1666
|
await publishCommittedQueueReceipt({
|
|
1414
1667
|
queueKind: receipt.queueKind,
|
|
1415
1668
|
receiptId,
|
|
@@ -1461,17 +1714,23 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1461
1714
|
transition("blocked", currentUpdateId, "authority-lost");
|
|
1462
1715
|
return "blocked";
|
|
1463
1716
|
};
|
|
1464
|
-
const executeWithinOwner = async (expectedOwner, update) => {
|
|
1717
|
+
const executeWithinOwner = async (expectedOwner, update, preparedExecute) => {
|
|
1465
1718
|
if (expectedOwner.controller.signal.aborted) {
|
|
1466
1719
|
return TELEGRAM_UPDATE_WORKER_EXECUTION_ABORTED;
|
|
1467
1720
|
}
|
|
1468
1721
|
const custodied = deps.executeCustodiedUpdate !== undefined;
|
|
1469
|
-
const execution = Promise.resolve().then(async () =>
|
|
1470
|
-
?
|
|
1471
|
-
: deps.
|
|
1722
|
+
const execution = Promise.resolve().then(async () => preparedExecute
|
|
1723
|
+
? preparedExecute(update, expectedOwner.ctx, expectedOwner.controller.signal)
|
|
1724
|
+
: deps.executeCustodiedUpdate
|
|
1725
|
+
? deps.executeCustodiedUpdate(update, expectedOwner.ctx, expectedOwner.controller.signal)
|
|
1726
|
+
: deps.executeUpdate(update, expectedOwner.ctx, expectedOwner.controller.signal));
|
|
1472
1727
|
const settlement = execution.then((outcome) => custodied
|
|
1473
|
-
?
|
|
1474
|
-
|
|
1728
|
+
? {
|
|
1729
|
+
ok: true,
|
|
1730
|
+
outcome: outcome,
|
|
1731
|
+
custodied: true,
|
|
1732
|
+
}
|
|
1733
|
+
: { ok: true, outcome: outcome }, (error) => ({ ok: false, error }));
|
|
1475
1734
|
unsettledExecutions.add(settlement);
|
|
1476
1735
|
const updateExecutions = unsettledExecutionsByUpdateId.get(update.update_id) ?? new Set();
|
|
1477
1736
|
updateExecutions.add(settlement);
|
|
@@ -1555,7 +1814,7 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1555
1814
|
for (const sourceUpdateId of normalized.sourceUpdateIds) {
|
|
1556
1815
|
claims.set(sourceUpdateId, "queued");
|
|
1557
1816
|
}
|
|
1558
|
-
return publishCommittedQueueReceipt(normalized, queueOwner, expectedOwner).then(() => "committed", error => blockWithFailure("invalid-outcome", "queue-receipt-publish", error, currentUpdateId));
|
|
1817
|
+
return publishCommittedQueueReceipt(normalized, queueOwner, expectedOwner).then(() => "committed", (error) => blockWithFailure("invalid-outcome", "queue-receipt-publish", error, currentUpdateId));
|
|
1559
1818
|
};
|
|
1560
1819
|
const persistExecutionFailure = (expectedOwner, entry, error) => {
|
|
1561
1820
|
const authorityResult = checkAuthority(expectedOwner, entry.updateId);
|
|
@@ -1614,8 +1873,8 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1614
1873
|
transition(disposition, entry.updateId);
|
|
1615
1874
|
return disposition;
|
|
1616
1875
|
};
|
|
1617
|
-
const commitCompletedBatch = (expectedOwner, completions) => {
|
|
1618
|
-
const updateIds = completions.map(value => value.updateId);
|
|
1876
|
+
const commitCompletedBatch = (expectedOwner, completions, isCurrent) => {
|
|
1877
|
+
const updateIds = completions.map((value) => value.updateId);
|
|
1619
1878
|
if (updateIds.length === 0)
|
|
1620
1879
|
return undefined;
|
|
1621
1880
|
const commitAuthority = checkAuthority(expectedOwner, updateIds[updateIds.length - 1]);
|
|
@@ -1623,28 +1882,43 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1623
1882
|
return commitAuthority;
|
|
1624
1883
|
const journalBindingKey = deps.getJournalBindingKey?.();
|
|
1625
1884
|
try {
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1885
|
+
if (isCurrent?.() === false)
|
|
1886
|
+
throw new Error("Telegram exact completion authority changed.");
|
|
1887
|
+
const expectedSources = completions.flatMap((value) => value.expectedSource ? [value.expectedSource] : []);
|
|
1888
|
+
if (expectedSources.some((value) => value.journalBindingKey !== journalBindingKey) ||
|
|
1889
|
+
(expectedSources.length > 0 &&
|
|
1890
|
+
(deps.isContextCurrent?.(expectedOwner.ctx) === false ||
|
|
1891
|
+
!isQueueOwnerIdentityCurrent(expectedOwner)))) {
|
|
1629
1892
|
throw new Error("Telegram guarded completion source authority changed.");
|
|
1630
1893
|
}
|
|
1631
|
-
const scoped = expectedSources
|
|
1632
|
-
|
|
1894
|
+
const scoped = expectedSources
|
|
1895
|
+
.flatMap(({ updateId, sourceSha256, completionSha256 }) => completionSha256 !== undefined
|
|
1896
|
+
? [{ updateId, sourceSha256, completionSha256 }]
|
|
1897
|
+
: [])
|
|
1898
|
+
.sort((a, b) => a.updateId - b.updateId);
|
|
1633
1899
|
if (scoped.length > 0 && !inspectCompletion)
|
|
1634
1900
|
throw new Error("Telegram source completion inspection is unavailable.");
|
|
1635
1901
|
let removed;
|
|
1636
1902
|
if (expectedSources.length > 0) {
|
|
1637
1903
|
if (!completeExact)
|
|
1638
1904
|
throw new Error("Telegram exact source completion is unavailable.");
|
|
1639
|
-
removed = completeExact(updateIds, expectedSources.map(({ updateId, sourceSha256 }) => ({
|
|
1905
|
+
removed = completeExact(updateIds, expectedSources.map(({ updateId, sourceSha256 }) => ({
|
|
1906
|
+
updateId,
|
|
1907
|
+
sourceSha256,
|
|
1908
|
+
})), scoped.length > 0
|
|
1909
|
+
? scoped.map((completion) => ({ ...completion }))
|
|
1910
|
+
: undefined, isCurrent);
|
|
1640
1911
|
}
|
|
1641
1912
|
else
|
|
1642
1913
|
removed = deps.journal.removeCompleted(updateIds);
|
|
1914
|
+
if (isCurrent?.() === false)
|
|
1915
|
+
throw new Error("Telegram exact completion ACK authority changed.");
|
|
1643
1916
|
if (scoped.length > 0) {
|
|
1644
1917
|
const afterCommit = checkAuthority(expectedOwner, updateIds.at(-1));
|
|
1645
1918
|
if (afterCommit)
|
|
1646
1919
|
return afterCommit;
|
|
1647
|
-
if (deps.isContextCurrent?.(expectedOwner.ctx) === false ||
|
|
1920
|
+
if (deps.isContextCurrent?.(expectedOwner.ctx) === false ||
|
|
1921
|
+
!isQueueOwnerIdentityCurrent(expectedOwner) ||
|
|
1648
1922
|
deps.getJournalBindingKey?.() !== journalBindingKey)
|
|
1649
1923
|
throw new Error("Telegram source completion ACK authority changed.");
|
|
1650
1924
|
if (!isDeepStrictEqual(removed.sourceCompletions, scoped))
|
|
@@ -1655,7 +1929,8 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1655
1929
|
const afterRead = checkAuthority(expectedOwner, completion.updateId);
|
|
1656
1930
|
if (afterRead)
|
|
1657
1931
|
return afterRead;
|
|
1658
|
-
if (deps.isContextCurrent?.(expectedOwner.ctx) === false ||
|
|
1932
|
+
if (deps.isContextCurrent?.(expectedOwner.ctx) === false ||
|
|
1933
|
+
!isQueueOwnerIdentityCurrent(expectedOwner) ||
|
|
1659
1934
|
deps.getJournalBindingKey?.() !== journalBindingKey)
|
|
1660
1935
|
throw new Error("Telegram source completion ACK authority changed.");
|
|
1661
1936
|
}
|
|
@@ -1697,21 +1972,32 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1697
1972
|
};
|
|
1698
1973
|
const reconcileRoutingInputs = async (snapshot, expectedOwner) => {
|
|
1699
1974
|
clearRoutingTimer();
|
|
1700
|
-
if (!isTelegramUpdateJournalLegacyFamilyVersion(snapshot.version) ||
|
|
1975
|
+
if (!isTelegramUpdateJournalLegacyFamilyVersion(snapshot.version) ||
|
|
1976
|
+
!expireRouting ||
|
|
1977
|
+
!expireRoutingObserver)
|
|
1701
1978
|
return false;
|
|
1702
1979
|
let nextAtMs, changed = false;
|
|
1703
|
-
const binding = deps.getJournalBindingKey?.(), snapshotIds = new Set(snapshot.entries.map(entry => entry.updateId));
|
|
1980
|
+
const binding = deps.getJournalBindingKey?.(), snapshotIds = new Set(snapshot.entries.map((entry) => entry.updateId));
|
|
1704
1981
|
// Lost expiry ACKs reconcile the exact held original against its body-free discard tombstone.
|
|
1705
|
-
const candidates = [
|
|
1706
|
-
|
|
1707
|
-
|
|
1982
|
+
const candidates = [
|
|
1983
|
+
...snapshot.entries,
|
|
1984
|
+
...[...deferredSources.values()]
|
|
1985
|
+
.filter((source) => source.journalBindingKey === binding &&
|
|
1986
|
+
!snapshotIds.has(source.entry.updateId))
|
|
1987
|
+
.map((source) => source.entry),
|
|
1988
|
+
];
|
|
1708
1989
|
for (const entry of candidates) {
|
|
1709
1990
|
const lifetime = entry.routingInput;
|
|
1710
|
-
if (!lifetime ||
|
|
1991
|
+
if (!lifetime ||
|
|
1992
|
+
entry.state !== "pending" ||
|
|
1993
|
+
!binding ||
|
|
1994
|
+
claims.get(entry.updateId) === "queued" ||
|
|
1711
1995
|
unsettledExecutionsByUpdateId.get(entry.updateId)?.size)
|
|
1712
1996
|
continue;
|
|
1713
1997
|
// Classify the first startup snapshot before constructing any historical expiry carrier.
|
|
1714
|
-
if (expectedOwner.startupUpdateIds?.has(entry.updateId) &&
|
|
1998
|
+
if (expectedOwner.startupUpdateIds?.has(entry.updateId) &&
|
|
1999
|
+
!claims.has(entry.updateId) &&
|
|
2000
|
+
deps.shouldReviewHistoricalInput) {
|
|
1715
2001
|
nextAtMs = Math.min(nextAtMs ?? Infinity, Math.max(getNowMs() + 1, lifetime.expiresAtMs));
|
|
1716
2002
|
continue;
|
|
1717
2003
|
}
|
|
@@ -1724,30 +2010,48 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1724
2010
|
// Lost clock/selection ACKs may leave only this immutable metadata behind its exact source; reconciliation grants expiry, never another dispatch.
|
|
1725
2011
|
const previous = existing.entry.routingInput;
|
|
1726
2012
|
if (!isDeepStrictEqual({ ...existing.entry, routingInput: lifetime }, entry) ||
|
|
1727
|
-
(previous &&
|
|
1728
|
-
(previous
|
|
2013
|
+
(previous &&
|
|
2014
|
+
(!isDeepStrictEqual({ ...previous, phase: lifetime.phase }, lifetime) ||
|
|
2015
|
+
(previous.phase === "selected" && lifetime.phase !== "selected"))))
|
|
1729
2016
|
continue;
|
|
1730
2017
|
existing.entry = structuredClone(entry);
|
|
1731
2018
|
}
|
|
1732
|
-
const source = existing ?? {
|
|
2019
|
+
const source = existing ?? {
|
|
2020
|
+
entry: structuredClone(entry),
|
|
2021
|
+
journalBindingKey: binding,
|
|
2022
|
+
historical: true,
|
|
2023
|
+
};
|
|
1733
2024
|
deferredSources.set(entry.updateId, source);
|
|
1734
2025
|
if (!claims.has(entry.updateId))
|
|
1735
2026
|
claims.set(entry.updateId, "historical");
|
|
1736
|
-
const current = () => owner === expectedOwner &&
|
|
1737
|
-
|
|
1738
|
-
deps.
|
|
1739
|
-
|
|
2027
|
+
const current = () => owner === expectedOwner &&
|
|
2028
|
+
!expectedOwner.controller.signal.aborted &&
|
|
2029
|
+
deps.isContextCurrent?.(expectedOwner.ctx) !== false &&
|
|
2030
|
+
deps.getJournalBindingKey?.() === binding &&
|
|
2031
|
+
deps.hasAuthority(expectedOwner.ctx) &&
|
|
2032
|
+
isQueueOwnerIdentityCurrent(expectedOwner) &&
|
|
2033
|
+
deferredSources.get(entry.updateId) === source &&
|
|
2034
|
+
isDeepStrictEqual(source.entry, entry) &&
|
|
2035
|
+
claims.get(entry.updateId) !== "queued";
|
|
1740
2036
|
let committed;
|
|
1741
2037
|
try {
|
|
1742
2038
|
if (current())
|
|
1743
|
-
await expireRoutingObserver({
|
|
2039
|
+
await expireRoutingObserver({
|
|
2040
|
+
original: structuredClone(entry),
|
|
2041
|
+
journalBindingKey: binding,
|
|
2042
|
+
isCurrent: current,
|
|
1744
2043
|
expire() {
|
|
1745
2044
|
if (!current())
|
|
1746
2045
|
return undefined;
|
|
1747
2046
|
if (committed)
|
|
1748
2047
|
return { ...committed, duplicate: true };
|
|
1749
2048
|
claims.set(entry.updateId, "abandoning");
|
|
1750
|
-
committed = expireRouting({
|
|
2049
|
+
committed = expireRouting({
|
|
2050
|
+
entry: source.entry,
|
|
2051
|
+
journalBindingKey: binding,
|
|
2052
|
+
operatorUserId: lifetime.operatorUserId,
|
|
2053
|
+
isCurrent: current,
|
|
2054
|
+
});
|
|
1751
2055
|
// Source expiry is terminal before chooser/cleanup awaits; later failures must not retain a deferred/retry source.
|
|
1752
2056
|
claims.delete(entry.updateId);
|
|
1753
2057
|
deferredSources.delete(entry.updateId);
|
|
@@ -1757,19 +2061,27 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1757
2061
|
notifyStateChange();
|
|
1758
2062
|
changed ||= !committed.duplicate;
|
|
1759
2063
|
return committed;
|
|
1760
|
-
}
|
|
2064
|
+
},
|
|
2065
|
+
}, expectedOwner.ctx, expectedOwner.controller.signal);
|
|
1761
2066
|
}
|
|
1762
2067
|
catch (error) {
|
|
1763
|
-
recordRuntimeEvent(error, {
|
|
2068
|
+
recordRuntimeEvent(error, {
|
|
2069
|
+
phase: "routing-input-expiry",
|
|
2070
|
+
updateId: entry.updateId,
|
|
2071
|
+
});
|
|
1764
2072
|
}
|
|
1765
2073
|
if (!committed)
|
|
1766
2074
|
nextAtMs = Math.min(nextAtMs ?? Infinity, getNowMs() + 60_000);
|
|
1767
2075
|
}
|
|
1768
|
-
if (nextAtMs !== undefined &&
|
|
2076
|
+
if (nextAtMs !== undefined &&
|
|
2077
|
+
owner === expectedOwner &&
|
|
2078
|
+
!expectedOwner.controller.signal.aborted) {
|
|
1769
2079
|
const token = {};
|
|
1770
2080
|
routingTimerToken = token;
|
|
1771
2081
|
routingTimer = scheduleRetry(() => {
|
|
1772
|
-
if (routingTimerToken !== token ||
|
|
2082
|
+
if (routingTimerToken !== token ||
|
|
2083
|
+
owner !== expectedOwner ||
|
|
2084
|
+
expectedOwner.controller.signal.aborted)
|
|
1773
2085
|
return;
|
|
1774
2086
|
routingTimer = undefined;
|
|
1775
2087
|
routingTimerToken = undefined;
|
|
@@ -1781,32 +2093,69 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1781
2093
|
};
|
|
1782
2094
|
const mutateRoutingSource = (input, select) => {
|
|
1783
2095
|
const expectedOwner = owner, ids = [...input.sourceUpdateIds];
|
|
1784
|
-
if (!ids.length ||
|
|
2096
|
+
if (!ids.length ||
|
|
2097
|
+
!ids.includes(input.updateId) ||
|
|
2098
|
+
new Set(ids).size !== ids.length ||
|
|
2099
|
+
ids.some((id) => !Number.isSafeInteger(id) || id < 0))
|
|
1785
2100
|
return undefined;
|
|
1786
|
-
const sources = ids.map(id => deferredSources.get(id)), binding = sources[0]?.journalBindingKey;
|
|
1787
|
-
const current = () => !!expectedOwner &&
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
2101
|
+
const sources = ids.map((id) => deferredSources.get(id)), binding = sources[0]?.journalBindingKey;
|
|
2102
|
+
const current = () => !!expectedOwner &&
|
|
2103
|
+
owner === expectedOwner &&
|
|
2104
|
+
expectedOwner.controller.signal === input.signal &&
|
|
2105
|
+
!input.signal.aborted &&
|
|
2106
|
+
!!binding &&
|
|
2107
|
+
sources.every((source, index) => !!source &&
|
|
2108
|
+
deferredSources.get(ids[index]) === source &&
|
|
2109
|
+
!source.historical &&
|
|
2110
|
+
source.journalBindingKey === binding &&
|
|
2111
|
+
claims.get(ids[index]) !== "queued" &&
|
|
2112
|
+
claims.get(ids[index]) !== "abandoning") &&
|
|
2113
|
+
deps.getJournalBindingKey?.() === binding &&
|
|
2114
|
+
deps.isContextCurrent?.(expectedOwner.ctx) !== false &&
|
|
2115
|
+
deps.hasAuthority(expectedOwner.ctx) &&
|
|
2116
|
+
isQueueOwnerIdentityCurrent(expectedOwner);
|
|
1792
2117
|
if (!current() || !armRouting || !selectRouting || !expireRoutingObserver)
|
|
1793
2118
|
return undefined;
|
|
1794
|
-
const authority = {
|
|
1795
|
-
|
|
1796
|
-
|
|
2119
|
+
const authority = {
|
|
2120
|
+
journalBindingKey: binding,
|
|
2121
|
+
entries: sources.map((source) => structuredClone(source.entry)),
|
|
2122
|
+
operatorUserId: input.operatorUserId,
|
|
2123
|
+
isCurrent: current,
|
|
2124
|
+
};
|
|
2125
|
+
const result = select
|
|
2126
|
+
? selectRouting(authority)
|
|
2127
|
+
: {
|
|
2128
|
+
issued: false,
|
|
2129
|
+
entries: armRouting({
|
|
2130
|
+
...authority,
|
|
2131
|
+
publishedAtMs: getNowMs(),
|
|
2132
|
+
...(input.chooser ? { chooser: input.chooser } : {}),
|
|
2133
|
+
}),
|
|
2134
|
+
};
|
|
2135
|
+
if (result.entries.length !== ids.length ||
|
|
2136
|
+
result.entries.some((entry, index) => entry.updateId !== ids[index] || !entry.routingInput) ||
|
|
2137
|
+
!current()) {
|
|
1797
2138
|
throw new Error("Telegram routing input lifetime ACK was not confirmed.");
|
|
1798
2139
|
}
|
|
1799
2140
|
const retained = deps.journal.read().entries;
|
|
1800
|
-
if (result.entries.some(entry => !isDeepStrictEqual(retained.find(value => value.updateId === entry.updateId), entry)) ||
|
|
2141
|
+
if (result.entries.some((entry) => !isDeepStrictEqual(retained.find((value) => value.updateId === entry.updateId), entry)) ||
|
|
2142
|
+
!current()) {
|
|
1801
2143
|
throw new Error("Telegram routing input lifetime ACK was not retained.");
|
|
1802
2144
|
}
|
|
1803
|
-
|
|
2145
|
+
// Retain the decoder's exact source representation, not the mutation result's property ordering.
|
|
2146
|
+
result.entries.forEach((entry, index) => {
|
|
2147
|
+
sources[index].entry = structuredClone(retained.find((value) => value.updateId === entry.updateId));
|
|
2148
|
+
});
|
|
1804
2149
|
pendingSignal = true;
|
|
1805
2150
|
launchDrain();
|
|
1806
|
-
return {
|
|
2151
|
+
return {
|
|
2152
|
+
issued: result.issued,
|
|
2153
|
+
lifetime: { ...result.entries[0].routingInput },
|
|
2154
|
+
};
|
|
1807
2155
|
};
|
|
1808
2156
|
const drain = async (expectedOwner) => {
|
|
1809
|
-
while (owner === expectedOwner &&
|
|
2157
|
+
while (owner === expectedOwner &&
|
|
2158
|
+
!expectedOwner.controller.signal.aborted) {
|
|
1810
2159
|
const authorityResult = checkAuthority(expectedOwner);
|
|
1811
2160
|
if (authorityResult)
|
|
1812
2161
|
return authorityResult;
|
|
@@ -1817,7 +2166,7 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1817
2166
|
scheduledRetryAtMs = await refreshJournalState(snapshot, expectedOwner);
|
|
1818
2167
|
// One complete validated baseline per generation, before batching or execution.
|
|
1819
2168
|
// Replayed v1 entries do not prove that a prior process never forwarded them.
|
|
1820
|
-
expectedOwner.startupUpdateIds ??= new Set(snapshot.entries.map(entry => entry.updateId));
|
|
2169
|
+
expectedOwner.startupUpdateIds ??= new Set(snapshot.entries.map((entry) => entry.updateId));
|
|
1821
2170
|
}
|
|
1822
2171
|
catch (error) {
|
|
1823
2172
|
return blockWithFailure("journal-read", "journal-read", error);
|
|
@@ -1834,24 +2183,39 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1834
2183
|
let blockedInputCustody;
|
|
1835
2184
|
delete state.blockedInputCustody;
|
|
1836
2185
|
for (const candidate of snapshot.entries) {
|
|
2186
|
+
if (livePreparation?.ids.has(candidate.updateId) &&
|
|
2187
|
+
(!livePreparation.released || livePreparation.status?.issued))
|
|
2188
|
+
continue;
|
|
1837
2189
|
if (snapshot.version === TELEGRAM_UPDATE_JOURNAL_CUSTODY_VERSION &&
|
|
1838
2190
|
(candidate.state === "retry-wait" || candidate.state === "failed")) {
|
|
1839
2191
|
hasUnavailableInputCustody = true;
|
|
1840
|
-
blockedInputCustody ??= {
|
|
2192
|
+
blockedInputCustody ??= {
|
|
2193
|
+
updateId: candidate.updateId,
|
|
2194
|
+
kind: "legacy-retry-state",
|
|
2195
|
+
};
|
|
1841
2196
|
continue;
|
|
1842
2197
|
}
|
|
1843
|
-
if (snapshot.version === TELEGRAM_UPDATE_JOURNAL_CUSTODY_VERSION &&
|
|
2198
|
+
if (snapshot.version === TELEGRAM_UPDATE_JOURNAL_CUSTODY_VERSION &&
|
|
2199
|
+
candidate.inputClaim) {
|
|
1844
2200
|
if (candidate.inputClaim.phase === "running") {
|
|
1845
2201
|
hasOutcomeUnknownInput = true;
|
|
1846
|
-
if (!blockedInputCustody ||
|
|
1847
|
-
blockedInputCustody
|
|
1848
|
-
|
|
2202
|
+
if (!blockedInputCustody ||
|
|
2203
|
+
blockedInputCustody.kind !== "running-outcome-unknown")
|
|
2204
|
+
blockedInputCustody = {
|
|
2205
|
+
updateId: candidate.updateId,
|
|
2206
|
+
kind: "running-outcome-unknown",
|
|
2207
|
+
};
|
|
1849
2208
|
continue;
|
|
1850
2209
|
}
|
|
1851
|
-
if (candidate.inputClaim.handoff ||
|
|
2210
|
+
if (candidate.inputClaim.handoff ||
|
|
2211
|
+
!isTelegramUpdateJournalQueueOwnerProcess(candidate.inputClaim.owner, expectedOwner.queueOwnerIdentity)) {
|
|
1852
2212
|
hasUnavailableInputCustody = true;
|
|
1853
|
-
blockedInputCustody ??= {
|
|
1854
|
-
|
|
2213
|
+
blockedInputCustody ??= {
|
|
2214
|
+
updateId: candidate.updateId,
|
|
2215
|
+
kind: candidate.inputClaim.handoff
|
|
2216
|
+
? "handoff-frozen"
|
|
2217
|
+
: "foreign-ready",
|
|
2218
|
+
};
|
|
1855
2219
|
continue;
|
|
1856
2220
|
}
|
|
1857
2221
|
}
|
|
@@ -1881,6 +2245,9 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1881
2245
|
const completedUpdateIds = [];
|
|
1882
2246
|
let snapshotInvalidated = false;
|
|
1883
2247
|
for (const entry of entries) {
|
|
2248
|
+
if (livePreparation?.ids.has(entry.updateId) &&
|
|
2249
|
+
(!livePreparation.released || livePreparation.status?.issued))
|
|
2250
|
+
continue;
|
|
1884
2251
|
const priorExecutions = unsettledExecutionsByUpdateId.get(entry.updateId);
|
|
1885
2252
|
if (priorExecutions?.size) {
|
|
1886
2253
|
const completionResult = commitCompletedBatch(expectedOwner, completedUpdateIds);
|
|
@@ -1899,7 +2266,9 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1899
2266
|
completedUpdateIds.push({ updateId: entry.updateId });
|
|
1900
2267
|
continue;
|
|
1901
2268
|
}
|
|
1902
|
-
if (isTelegramUpdateJournalLegacyFamilyVersion(snapshot.version) &&
|
|
2269
|
+
if (isTelegramUpdateJournalLegacyFamilyVersion(snapshot.version) &&
|
|
2270
|
+
entry.state === "pending" &&
|
|
2271
|
+
deps.journal.inspectPendingRetention) {
|
|
1903
2272
|
const authorityResult = checkAuthority(expectedOwner, entry.updateId);
|
|
1904
2273
|
if (authorityResult)
|
|
1905
2274
|
return authorityResult;
|
|
@@ -1910,7 +2279,10 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1910
2279
|
}
|
|
1911
2280
|
catch (error) {
|
|
1912
2281
|
unverifiable = true;
|
|
1913
|
-
recordRuntimeEvent(error, {
|
|
2282
|
+
recordRuntimeEvent(error, {
|
|
2283
|
+
phase: "pending-retention-inspection",
|
|
2284
|
+
updateId: entry.updateId,
|
|
2285
|
+
});
|
|
1914
2286
|
}
|
|
1915
2287
|
if (retained || unverifiable) {
|
|
1916
2288
|
// A copy is protection evidence only. A fresh owner must explicitly retry
|
|
@@ -1923,19 +2295,28 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1923
2295
|
continue;
|
|
1924
2296
|
}
|
|
1925
2297
|
}
|
|
1926
|
-
if (isTelegramUpdateJournalLegacyFamilyVersion(snapshot.version) &&
|
|
2298
|
+
if (isTelegramUpdateJournalLegacyFamilyVersion(snapshot.version) &&
|
|
2299
|
+
(entry.state === "pending" || entry.state === "retry-wait")) {
|
|
1927
2300
|
const historical = expectedOwner.startupUpdateIds.has(entry.updateId);
|
|
2301
|
+
let revived = false;
|
|
1928
2302
|
const journalBindingKey = deps.getJournalBindingKey?.();
|
|
1929
2303
|
// Classifiers and handler projections receive no mutable alias to CAS evidence.
|
|
1930
2304
|
if (journalBindingKey)
|
|
1931
|
-
deferredSources.set(entry.updateId, {
|
|
1932
|
-
|
|
1933
|
-
|
|
2305
|
+
deferredSources.set(entry.updateId, {
|
|
2306
|
+
entry: structuredClone(entry),
|
|
2307
|
+
journalBindingKey,
|
|
2308
|
+
...(historical ? { historical: true } : {}),
|
|
2309
|
+
});
|
|
2310
|
+
const classifyPending = historical && entry.state === "pending"
|
|
2311
|
+
? deps.shouldReviewHistoricalInput
|
|
2312
|
+
: deps.shouldHoldPendingInput;
|
|
1934
2313
|
if (classifyPending) {
|
|
1935
2314
|
const checkHistoricalAuthority = () => {
|
|
1936
|
-
if (owner !== expectedOwner ||
|
|
2315
|
+
if (owner !== expectedOwner ||
|
|
2316
|
+
expectedOwner.controller.signal.aborted)
|
|
1937
2317
|
return "aborted";
|
|
1938
|
-
if (deps.getJournalBindingKey?.() !== journalBindingKey ||
|
|
2318
|
+
if (deps.getJournalBindingKey?.() !== journalBindingKey ||
|
|
2319
|
+
deps.isContextCurrent?.(expectedOwner.ctx) === false) {
|
|
1939
2320
|
transition("blocked", entry.updateId, "authority-lost");
|
|
1940
2321
|
return "blocked";
|
|
1941
2322
|
}
|
|
@@ -1957,12 +2338,18 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1957
2338
|
const authorityResult = checkHistoricalAuthority();
|
|
1958
2339
|
if (authorityResult)
|
|
1959
2340
|
return authorityResult;
|
|
1960
|
-
if (typeof review !== "boolean" &&
|
|
2341
|
+
if (typeof review !== "boolean" &&
|
|
2342
|
+
!(historical &&
|
|
2343
|
+
entry.state === "pending" &&
|
|
2344
|
+
(review === "retain" ||
|
|
2345
|
+
(review === "revive" &&
|
|
2346
|
+
isRevivableTelegramRoutingInput(entry, getNowMs())))))
|
|
1961
2347
|
return blockWithFailure("execution", "historical-review-classification", new TelegramUpdateAdmissionOutcomeError("Historical classification must return a boolean or retain verdict."), entry.updateId);
|
|
1962
2348
|
// Classification may await a domain read; accepted/changed source evidence wins.
|
|
1963
2349
|
try {
|
|
1964
2350
|
const current = deps.journal.read();
|
|
1965
|
-
if (current.version !== snapshot.version ||
|
|
2351
|
+
if (current.version !== snapshot.version ||
|
|
2352
|
+
JSON.stringify(current.entries.find((candidate) => candidate.updateId === entry.updateId)) !== JSON.stringify(entry)) {
|
|
1966
2353
|
snapshotInvalidated = true;
|
|
1967
2354
|
break;
|
|
1968
2355
|
}
|
|
@@ -1970,34 +2357,96 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1970
2357
|
catch (error) {
|
|
1971
2358
|
return blockWithFailure("journal-read", "historical-source-recheck", error, entry.updateId);
|
|
1972
2359
|
}
|
|
1973
|
-
if (review === "
|
|
2360
|
+
if (review === "revive") {
|
|
2361
|
+
// A still-waiting saved clock proves no selection ran, so this generation owns the source as live input.
|
|
2362
|
+
revived = true;
|
|
2363
|
+
const source = deferredSources.get(entry.updateId);
|
|
2364
|
+
if (source)
|
|
2365
|
+
delete source.historical;
|
|
2366
|
+
expectedOwner.startupUpdateIds = new Set([...expectedOwner.startupUpdateIds].filter((id) => id !== entry.updateId));
|
|
2367
|
+
}
|
|
2368
|
+
else if (review === "retain") {
|
|
1974
2369
|
claims.set(entry.updateId, "retained");
|
|
1975
2370
|
transition("deferred", entry.updateId);
|
|
1976
2371
|
continue;
|
|
1977
2372
|
}
|
|
1978
|
-
if (review &&
|
|
1979
|
-
|
|
2373
|
+
if (review &&
|
|
2374
|
+
historical &&
|
|
2375
|
+
spendHistorical &&
|
|
2376
|
+
!entry.routingInput &&
|
|
2377
|
+
classifyPending === deps.shouldReviewHistoricalInput) {
|
|
2378
|
+
completedUpdateIds.push({
|
|
2379
|
+
updateId: entry.updateId,
|
|
2380
|
+
spent: true,
|
|
2381
|
+
});
|
|
1980
2382
|
continue;
|
|
1981
2383
|
}
|
|
1982
|
-
if (review) {
|
|
2384
|
+
if (review === true) {
|
|
1983
2385
|
claims.set(entry.updateId, "historical");
|
|
1984
2386
|
transition("deferred", entry.updateId);
|
|
1985
2387
|
continue;
|
|
1986
2388
|
}
|
|
1987
2389
|
}
|
|
1988
|
-
if (entry.state === "pending" && entry.routingInput) {
|
|
2390
|
+
if (!revived && entry.state === "pending" && entry.routingInput) {
|
|
1989
2391
|
claims.set(entry.updateId, "historical");
|
|
1990
2392
|
transition("deferred", entry.updateId);
|
|
1991
2393
|
continue;
|
|
1992
2394
|
}
|
|
1993
2395
|
}
|
|
2396
|
+
if (livePreparation?.ids.has(entry.updateId) &&
|
|
2397
|
+
(!livePreparation.released || livePreparation.status?.issued))
|
|
2398
|
+
continue;
|
|
2399
|
+
const prepared = livePreparation?.released && livePreparation.ids.has(entry.updateId)
|
|
2400
|
+
? livePreparation
|
|
2401
|
+
: undefined;
|
|
2402
|
+
const status = prepared?.status;
|
|
2403
|
+
if (status) {
|
|
2404
|
+
status.issued = true;
|
|
2405
|
+
try {
|
|
2406
|
+
if (!status.current() ||
|
|
2407
|
+
!isDeepStrictEqual(prepared.saved?.[0], entry))
|
|
2408
|
+
throw new Error("Prepared status original changed.");
|
|
2409
|
+
status.assertCurrent();
|
|
2410
|
+
if (!status.current())
|
|
2411
|
+
throw new Error("Prepared status owner changed after assertion.");
|
|
2412
|
+
}
|
|
2413
|
+
catch (error) {
|
|
2414
|
+
claims.set(entry.updateId, "retained");
|
|
2415
|
+
recordRuntimeEvent(error, {
|
|
2416
|
+
phase: "prepared-status-authority",
|
|
2417
|
+
updateId: entry.updateId,
|
|
2418
|
+
});
|
|
2419
|
+
continue;
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
1994
2422
|
clearRetryTimer();
|
|
1995
2423
|
transition("executing", entry.updateId);
|
|
1996
|
-
const execution = await executeWithinOwner(expectedOwner, entry.update
|
|
2424
|
+
const execution = await executeWithinOwner(expectedOwner, entry.update, status
|
|
2425
|
+
? async (update, ctx, signal) => {
|
|
2426
|
+
const admitted = await status.admit(update, ctx, signal);
|
|
2427
|
+
status.carrier = admitted.carrier;
|
|
2428
|
+
if (!status.current() ||
|
|
2429
|
+
admitted.outcome.kind !== "deferred" ||
|
|
2430
|
+
!isDeepStrictEqual(deps.journal
|
|
2431
|
+
.read()
|
|
2432
|
+
.entries.find((value) => value.updateId === entry.updateId), entry) ||
|
|
2433
|
+
!status.current())
|
|
2434
|
+
throw new Error("Prepared status admission is unconfirmed.");
|
|
2435
|
+
return admitted.outcome;
|
|
2436
|
+
}
|
|
2437
|
+
: undefined);
|
|
1997
2438
|
if (execution === TELEGRAM_UPDATE_WORKER_EXECUTION_ABORTED) {
|
|
1998
2439
|
return "aborted";
|
|
1999
2440
|
}
|
|
2000
2441
|
if (!execution.ok) {
|
|
2442
|
+
if (status) {
|
|
2443
|
+
claims.set(entry.updateId, "retained");
|
|
2444
|
+
recordRuntimeEvent(execution.error, {
|
|
2445
|
+
phase: "prepared-status-admission",
|
|
2446
|
+
updateId: entry.updateId,
|
|
2447
|
+
});
|
|
2448
|
+
continue;
|
|
2449
|
+
}
|
|
2001
2450
|
if (deps.executeCustodiedUpdate) {
|
|
2002
2451
|
transition("blocked", entry.updateId, "execution");
|
|
2003
2452
|
return "blocked";
|
|
@@ -2046,7 +2495,12 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
2046
2495
|
return postExecutionAuthority;
|
|
2047
2496
|
const claimableUpdateIds = new Set([
|
|
2048
2497
|
entry.updateId,
|
|
2049
|
-
...[...claims]
|
|
2498
|
+
...[...claims]
|
|
2499
|
+
.filter(([id, claim]) => !livePreparation?.ids.has(id) &&
|
|
2500
|
+
claim !== "abandoning" &&
|
|
2501
|
+
claim !== "historical" &&
|
|
2502
|
+
claim !== "retained")
|
|
2503
|
+
.map(([id]) => id),
|
|
2050
2504
|
]);
|
|
2051
2505
|
let outcome;
|
|
2052
2506
|
try {
|
|
@@ -2056,11 +2510,37 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
2056
2510
|
return blockWithFailure("invalid-outcome", "invalid-outcome", error, entry.updateId);
|
|
2057
2511
|
}
|
|
2058
2512
|
if (outcome.kind === "deferred") {
|
|
2059
|
-
if (outcome.routingReview &&
|
|
2513
|
+
if (outcome.routingReview &&
|
|
2514
|
+
!deferredSources.get(entry.updateId)?.historical) {
|
|
2060
2515
|
return blockWithFailure("invalid-outcome", "historical-review-source", new TelegramUpdateAdmissionOutcomeError("Historical review needs an exact startup source."), entry.updateId);
|
|
2061
2516
|
}
|
|
2062
2517
|
claims.set(entry.updateId, outcome.routingReview ? "historical" : "deferred");
|
|
2063
2518
|
transition("deferred", entry.updateId);
|
|
2519
|
+
if (status) {
|
|
2520
|
+
// Native warm deferral precedes consumption; late reports retain the existing ACK owner.
|
|
2521
|
+
if (livePreparation === prepared)
|
|
2522
|
+
livePreparation = undefined;
|
|
2523
|
+
updateClaimCounts();
|
|
2524
|
+
try {
|
|
2525
|
+
if (!status.current())
|
|
2526
|
+
throw new Error("Prepared status consumer owner changed.");
|
|
2527
|
+
status.assertCurrent();
|
|
2528
|
+
if (!status.bindCarrier(status.carrier) || !status.current())
|
|
2529
|
+
throw new Error("Prepared status carrier binding refused.");
|
|
2530
|
+
status.assertCurrent();
|
|
2531
|
+
if (!status.current())
|
|
2532
|
+
throw new Error("Prepared status owner changed before consumption.");
|
|
2533
|
+
await status.execute();
|
|
2534
|
+
if (!status.current())
|
|
2535
|
+
throw new Error("Prepared status consumption result is unconfirmed.");
|
|
2536
|
+
}
|
|
2537
|
+
catch (error) {
|
|
2538
|
+
recordRuntimeEvent(error, {
|
|
2539
|
+
phase: "prepared-status-consume",
|
|
2540
|
+
updateId: entry.updateId,
|
|
2541
|
+
});
|
|
2542
|
+
}
|
|
2543
|
+
}
|
|
2064
2544
|
continue;
|
|
2065
2545
|
}
|
|
2066
2546
|
if (outcome.kind === "queued") {
|
|
@@ -2075,7 +2555,12 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
2075
2555
|
snapshotInvalidated = true;
|
|
2076
2556
|
break;
|
|
2077
2557
|
}
|
|
2078
|
-
completedUpdateIds.push({
|
|
2558
|
+
completedUpdateIds.push({
|
|
2559
|
+
updateId: entry.updateId,
|
|
2560
|
+
...(outcome.expectedSource
|
|
2561
|
+
? { expectedSource: outcome.expectedSource }
|
|
2562
|
+
: {}),
|
|
2563
|
+
});
|
|
2079
2564
|
}
|
|
2080
2565
|
const completionResult = commitCompletedBatch(expectedOwner, completedUpdateIds);
|
|
2081
2566
|
if (completionResult)
|
|
@@ -2083,8 +2568,7 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
2083
2568
|
if (!snapshotInvalidated && !hasMoreEntries)
|
|
2084
2569
|
continue;
|
|
2085
2570
|
await yieldToEventLoop();
|
|
2086
|
-
if (owner !== expectedOwner ||
|
|
2087
|
-
expectedOwner.controller.signal.aborted) {
|
|
2571
|
+
if (owner !== expectedOwner || expectedOwner.controller.signal.aborted) {
|
|
2088
2572
|
return "aborted";
|
|
2089
2573
|
}
|
|
2090
2574
|
}
|
|
@@ -2099,7 +2583,9 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
2099
2583
|
try {
|
|
2100
2584
|
startupKey = deps.getJournalBindingKey?.();
|
|
2101
2585
|
}
|
|
2102
|
-
catch {
|
|
2586
|
+
catch {
|
|
2587
|
+
/* Missing startup identity grants no notification. */
|
|
2588
|
+
}
|
|
2103
2589
|
}
|
|
2104
2590
|
let lastResult;
|
|
2105
2591
|
const run = async () => {
|
|
@@ -2120,30 +2606,55 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
2120
2606
|
if (pendingSignal && !expectedOwner.controller.signal.aborted) {
|
|
2121
2607
|
launchDrain();
|
|
2122
2608
|
}
|
|
2123
|
-
else if (lastResult === "idle" &&
|
|
2609
|
+
else if (lastResult === "idle" &&
|
|
2610
|
+
startupKey &&
|
|
2611
|
+
onHeldSourcesPrepared &&
|
|
2612
|
+
!expectedOwner.heldSourcesPreparedIssued) {
|
|
2124
2613
|
try {
|
|
2125
2614
|
const ctx = expectedOwner.ctx, key = startupKey;
|
|
2126
|
-
const isCurrent = () => owner === expectedOwner &&
|
|
2127
|
-
|
|
2615
|
+
const isCurrent = () => owner === expectedOwner &&
|
|
2616
|
+
!blocked &&
|
|
2617
|
+
!expectedOwner.controller.signal.aborted &&
|
|
2618
|
+
deps.getJournalBindingKey?.() === key &&
|
|
2619
|
+
deps.isContextCurrent?.(ctx) !== false &&
|
|
2620
|
+
deps.hasAuthority(ctx);
|
|
2128
2621
|
if (!isCurrent())
|
|
2129
2622
|
return;
|
|
2130
2623
|
expectedOwner.heldSourcesPreparedIssued = true;
|
|
2131
2624
|
// Do not couple lifecycle/stop or the worker drain to a held controller/API reply.
|
|
2132
|
-
void Promise.resolve()
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2625
|
+
void Promise.resolve()
|
|
2626
|
+
.then(() => isCurrent()
|
|
2627
|
+
? onHeldSourcesPrepared({
|
|
2628
|
+
ctx,
|
|
2629
|
+
journalBindingKey: key,
|
|
2630
|
+
signal: expectedOwner.controller.signal,
|
|
2631
|
+
isCurrent,
|
|
2632
|
+
routingSourceIds: [...deferredSources]
|
|
2633
|
+
.filter(([, source]) => source.journalBindingKey === key &&
|
|
2634
|
+
!!source.entry.routingInput)
|
|
2635
|
+
.map(([id]) => id),
|
|
2636
|
+
})
|
|
2637
|
+
: undefined)
|
|
2638
|
+
.catch((error) => {
|
|
2136
2639
|
try {
|
|
2137
|
-
deps.recordRuntimeEvent?.("inbound-worker", error, {
|
|
2640
|
+
deps.recordRuntimeEvent?.("inbound-worker", error, {
|
|
2641
|
+
phase: "held-source-preparation",
|
|
2642
|
+
});
|
|
2643
|
+
}
|
|
2644
|
+
catch {
|
|
2645
|
+
/* Diagnostic only. */
|
|
2138
2646
|
}
|
|
2139
|
-
catch { /* Diagnostic only. */ }
|
|
2140
2647
|
});
|
|
2141
2648
|
}
|
|
2142
2649
|
catch (error) {
|
|
2143
2650
|
try {
|
|
2144
|
-
deps.recordRuntimeEvent?.("inbound-worker", error, {
|
|
2651
|
+
deps.recordRuntimeEvent?.("inbound-worker", error, {
|
|
2652
|
+
phase: "held-source-preparation",
|
|
2653
|
+
});
|
|
2654
|
+
}
|
|
2655
|
+
catch {
|
|
2656
|
+
/* Diagnostic only. */
|
|
2145
2657
|
}
|
|
2146
|
-
catch { /* Diagnostic only. */ }
|
|
2147
2658
|
}
|
|
2148
2659
|
}
|
|
2149
2660
|
}
|
|
@@ -2162,22 +2673,34 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
2162
2673
|
const source = deferredSources.get(input.updateId);
|
|
2163
2674
|
const abandonPending = deps.journal.abandonPending;
|
|
2164
2675
|
const isCurrent = () => {
|
|
2165
|
-
if (!expectedOwner ||
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2676
|
+
if (!expectedOwner ||
|
|
2677
|
+
owner !== expectedOwner ||
|
|
2678
|
+
input.signal.aborted ||
|
|
2679
|
+
expectedOwner.controller.signal !== input.signal ||
|
|
2680
|
+
!source ||
|
|
2681
|
+
source.entry.routingInput?.phase === "selected" ||
|
|
2682
|
+
(source.historical && historicalSource !== source) ||
|
|
2683
|
+
(claims.get(input.updateId) !== "deferred" &&
|
|
2684
|
+
claims.get(input.updateId) !== "abandoning" &&
|
|
2685
|
+
!(claims.get(input.updateId) === "historical" &&
|
|
2686
|
+
historicalSource === source)))
|
|
2170
2687
|
return false;
|
|
2171
|
-
return input.isCurrent() &&
|
|
2172
|
-
deps.
|
|
2688
|
+
return (input.isCurrent() &&
|
|
2689
|
+
deps.isContextCurrent?.(expectedOwner.ctx) !== false &&
|
|
2690
|
+
deps.getJournalBindingKey?.() === source.journalBindingKey &&
|
|
2691
|
+
deps.hasAuthority(expectedOwner.ctx));
|
|
2173
2692
|
};
|
|
2174
2693
|
if (!source || !abandonPending || !isCurrent())
|
|
2175
2694
|
return undefined;
|
|
2176
2695
|
// Retain this claim on failure, including unknown commit: never reopen dispatch.
|
|
2177
2696
|
claims.set(input.updateId, "abandoning");
|
|
2178
2697
|
try {
|
|
2179
|
-
const result = abandonPending({
|
|
2180
|
-
|
|
2698
|
+
const result = abandonPending({
|
|
2699
|
+
...input,
|
|
2700
|
+
entry: source.entry,
|
|
2701
|
+
journalBindingKey: source.journalBindingKey,
|
|
2702
|
+
isCurrent,
|
|
2703
|
+
});
|
|
2181
2704
|
claims.delete(input.updateId);
|
|
2182
2705
|
deferredSources.delete(input.updateId);
|
|
2183
2706
|
state.journalEntryCount = result.entryCount;
|
|
@@ -2188,7 +2711,10 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
2188
2711
|
return result;
|
|
2189
2712
|
}
|
|
2190
2713
|
catch (error) {
|
|
2191
|
-
recordRuntimeEvent(error, {
|
|
2714
|
+
recordRuntimeEvent(error, {
|
|
2715
|
+
phase: "pending-abandonment",
|
|
2716
|
+
updateId: input.updateId,
|
|
2717
|
+
});
|
|
2192
2718
|
updateClaimCounts();
|
|
2193
2719
|
notifyStateChange();
|
|
2194
2720
|
throw error;
|
|
@@ -2198,34 +2724,93 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
2198
2724
|
const expectedOwner = owner;
|
|
2199
2725
|
if (!expectedOwner)
|
|
2200
2726
|
return undefined;
|
|
2201
|
-
const { signal, journalBindingKey, isCurrent: callerIsCurrent, afterUpdateId } = input;
|
|
2202
|
-
const isOwnerCurrent = () => owner === expectedOwner &&
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
(
|
|
2727
|
+
const { signal, journalBindingKey, isCurrent: callerIsCurrent, afterUpdateId, } = input;
|
|
2728
|
+
const isOwnerCurrent = () => owner === expectedOwner &&
|
|
2729
|
+
expectedOwner.controller.signal === signal &&
|
|
2730
|
+
!signal.aborted;
|
|
2731
|
+
const isCurrent = () => isOwnerCurrent() &&
|
|
2732
|
+
callerIsCurrent() &&
|
|
2733
|
+
deps.isContextCurrent?.(expectedOwner.ctx) !== false &&
|
|
2734
|
+
deps.getJournalBindingKey?.() === journalBindingKey &&
|
|
2735
|
+
deps.hasAuthority(expectedOwner.ctx);
|
|
2736
|
+
if (!isCurrent() ||
|
|
2737
|
+
!deps.journal.abandonPending ||
|
|
2738
|
+
!deps.journal.inspectPendingRetention ||
|
|
2739
|
+
(afterUpdateId !== undefined &&
|
|
2740
|
+
(!Number.isSafeInteger(afterUpdateId) || afterUpdateId < 0)))
|
|
2207
2741
|
return undefined;
|
|
2208
|
-
const matches = [...deferredSources]
|
|
2209
|
-
|
|
2742
|
+
const matches = [...deferredSources]
|
|
2743
|
+
.filter(([id, source]) => claims.get(id) === kind &&
|
|
2744
|
+
source.journalBindingKey === journalBindingKey &&
|
|
2745
|
+
(afterUpdateId === undefined || id > afterUpdateId))
|
|
2210
2746
|
.sort(([a], [b]) => a - b);
|
|
2211
|
-
const sources = matches
|
|
2747
|
+
const sources = matches
|
|
2748
|
+
.slice(0, TELEGRAM_HELD_SOURCE_INSPECTION_LIMIT)
|
|
2749
|
+
.map(([updateId, source]) => {
|
|
2212
2750
|
let committed;
|
|
2213
|
-
return {
|
|
2751
|
+
return {
|
|
2752
|
+
original: structuredClone(source.entry),
|
|
2753
|
+
retry(authority) {
|
|
2214
2754
|
const authorized = () => isOwnerCurrent() && authority.isCurrent() && isCurrent();
|
|
2215
2755
|
if (!authorized())
|
|
2216
2756
|
return undefined;
|
|
2217
2757
|
if (committed)
|
|
2218
2758
|
return { ...committed, duplicate: true };
|
|
2219
|
-
const stillProtected = () => authorized() &&
|
|
2759
|
+
const stillProtected = () => authorized() &&
|
|
2760
|
+
(claims.get(updateId) === kind ||
|
|
2761
|
+
claims.get(updateId) === "abandoning") &&
|
|
2220
2762
|
deferredSources.get(updateId) === source;
|
|
2221
2763
|
if (!stillProtected())
|
|
2222
2764
|
return undefined;
|
|
2223
2765
|
committed = abandonSource({ ...authority, updateId, signal, isCurrent: stillProtected }, source);
|
|
2224
2766
|
return committed;
|
|
2225
|
-
}
|
|
2767
|
+
},
|
|
2768
|
+
};
|
|
2226
2769
|
});
|
|
2227
|
-
return isCurrent()
|
|
2228
|
-
|
|
2770
|
+
return isCurrent()
|
|
2771
|
+
? {
|
|
2772
|
+
sources,
|
|
2773
|
+
...(matches.length > TELEGRAM_HELD_SOURCE_INSPECTION_LIMIT
|
|
2774
|
+
? {
|
|
2775
|
+
nextAfterUpdateId: matches[TELEGRAM_HELD_SOURCE_INSPECTION_LIMIT - 1][0],
|
|
2776
|
+
}
|
|
2777
|
+
: {}),
|
|
2778
|
+
}
|
|
2779
|
+
: undefined;
|
|
2780
|
+
};
|
|
2781
|
+
const inspectDeferredOriginal = (input) => {
|
|
2782
|
+
const expectedOwner = owner;
|
|
2783
|
+
const source = deferredSources.get(input.updateId);
|
|
2784
|
+
const current = () => {
|
|
2785
|
+
if (!expectedOwner ||
|
|
2786
|
+
owner !== expectedOwner ||
|
|
2787
|
+
input.signal.aborted ||
|
|
2788
|
+
expectedOwner.controller.signal !== input.signal ||
|
|
2789
|
+
!source ||
|
|
2790
|
+
deferredSources.get(input.updateId) !== source ||
|
|
2791
|
+
claims.get(input.updateId) !== "deferred" ||
|
|
2792
|
+
deps.getJournalBindingKey?.() !== source.journalBindingKey ||
|
|
2793
|
+
deps.isContextCurrent?.(expectedOwner.ctx) === false ||
|
|
2794
|
+
!deps.hasAuthority(expectedOwner.ctx))
|
|
2795
|
+
return false;
|
|
2796
|
+
return isQueueOwnerIdentityCurrent(expectedOwner);
|
|
2797
|
+
};
|
|
2798
|
+
if (!current())
|
|
2799
|
+
return undefined;
|
|
2800
|
+
const snapshot = deps.journal.read();
|
|
2801
|
+
const entry = snapshot.entries.find((value) => value.updateId === input.updateId);
|
|
2802
|
+
if (!isTelegramUpdateJournalLegacyFamilyVersion(snapshot.version) ||
|
|
2803
|
+
!entry ||
|
|
2804
|
+
JSON.stringify(entry) !== JSON.stringify(source.entry) ||
|
|
2805
|
+
!current())
|
|
2806
|
+
return undefined;
|
|
2807
|
+
return {
|
|
2808
|
+
source: {
|
|
2809
|
+
journalBindingKey: source.journalBindingKey,
|
|
2810
|
+
...createTelegramUpdateJournalEntryDigest(entry),
|
|
2811
|
+
},
|
|
2812
|
+
entry,
|
|
2813
|
+
};
|
|
2229
2814
|
};
|
|
2230
2815
|
return {
|
|
2231
2816
|
start(ctx) {
|
|
@@ -2247,6 +2832,8 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
2247
2832
|
controller: createAbortController(),
|
|
2248
2833
|
queueOwnerIdentity,
|
|
2249
2834
|
};
|
|
2835
|
+
livePreparation = undefined;
|
|
2836
|
+
delete state.preparedInputCount;
|
|
2250
2837
|
claims.clear();
|
|
2251
2838
|
deferredSources.clear();
|
|
2252
2839
|
committedQueueReceipts.clear();
|
|
@@ -2260,25 +2847,7 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
2260
2847
|
state.journalEntryCount = 0;
|
|
2261
2848
|
state.journalSerializedBytes = 0;
|
|
2262
2849
|
state.oldestAdmittedAtMs = undefined;
|
|
2263
|
-
state.
|
|
2264
|
-
delete state.abandoningClaimCount;
|
|
2265
|
-
delete state.historicalClaimCount;
|
|
2266
|
-
state.queuedClaimCount = 0;
|
|
2267
|
-
state.foreignQueuedCount = 0;
|
|
2268
|
-
delete state.foreignQueuedOwner;
|
|
2269
|
-
state.retryWaitCount = 0;
|
|
2270
|
-
state.failedCount = 0;
|
|
2271
|
-
state.nextRetryUpdateId = undefined;
|
|
2272
|
-
state.nextRetryAtMs = undefined;
|
|
2273
|
-
state.nextRetryAttemptCount = undefined;
|
|
2274
|
-
state.nextRetryFailureClass = undefined;
|
|
2275
|
-
state.failedUpdateId = undefined;
|
|
2276
|
-
state.failedFailureId = undefined;
|
|
2277
|
-
state.failedAttemptCount = undefined;
|
|
2278
|
-
state.failedClass = undefined;
|
|
2279
|
-
state.failedSummary = undefined;
|
|
2280
|
-
state.terminalFailureAtMs = undefined;
|
|
2281
|
-
state.unsettledExecutionCount = unsettledExecutions.size;
|
|
2850
|
+
clearTelegramUpdateWorkerClaimState(state, unsettledExecutions.size);
|
|
2282
2851
|
state.lastCompletedUpdateId = undefined;
|
|
2283
2852
|
state.lastCompletedAtMs = undefined;
|
|
2284
2853
|
state.lastFailureAtMs = undefined;
|
|
@@ -2286,6 +2855,401 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
2286
2855
|
notifyStateChange();
|
|
2287
2856
|
launchDrain();
|
|
2288
2857
|
},
|
|
2858
|
+
prepareDeferredLiveInput(input) {
|
|
2859
|
+
const expectedOwner = owner, binding = deps.getJournalBindingKey?.(), ids = [...input.sourceUpdateIds];
|
|
2860
|
+
const sources = ids.map((id) => deferredSources.get(id));
|
|
2861
|
+
const ownerCurrent = () => !!expectedOwner &&
|
|
2862
|
+
owner === expectedOwner &&
|
|
2863
|
+
expectedOwner.controller.signal === input.signal &&
|
|
2864
|
+
!input.signal.aborted &&
|
|
2865
|
+
!!binding &&
|
|
2866
|
+
deps.getJournalBindingKey?.() === binding &&
|
|
2867
|
+
deps.hasAuthority(expectedOwner.ctx) &&
|
|
2868
|
+
deps.isContextCurrent?.(expectedOwner.ctx) !== false &&
|
|
2869
|
+
isQueueOwnerIdentityCurrent(expectedOwner) &&
|
|
2870
|
+
input.isCurrent();
|
|
2871
|
+
const sourcesCurrent = () => ownerCurrent() &&
|
|
2872
|
+
sources.every((source, index) => !!source &&
|
|
2873
|
+
deferredSources.get(ids[index]) === source &&
|
|
2874
|
+
source.journalBindingKey === binding &&
|
|
2875
|
+
claims.get(ids[index]) === "deferred" &&
|
|
2876
|
+
isFreshLiveSource(expectedOwner, ids[index]));
|
|
2877
|
+
if (!ownerCurrent() ||
|
|
2878
|
+
!ids.length ||
|
|
2879
|
+
!ids.includes(input.updateId) ||
|
|
2880
|
+
new Set(ids).size !== ids.length ||
|
|
2881
|
+
ids.some((id) => !Number.isSafeInteger(id) || id < 0) ||
|
|
2882
|
+
livePreparation ||
|
|
2883
|
+
deps.executeCustodiedUpdate ||
|
|
2884
|
+
!sourcesCurrent())
|
|
2885
|
+
return undefined;
|
|
2886
|
+
const confirm = () => {
|
|
2887
|
+
if (!sourcesCurrent())
|
|
2888
|
+
return false;
|
|
2889
|
+
try {
|
|
2890
|
+
const snapshot = deps.journal.read();
|
|
2891
|
+
return (isTelegramUpdateJournalLegacyFamilyVersion(snapshot.version) &&
|
|
2892
|
+
sourcesCurrent() &&
|
|
2893
|
+
sources.every((source, index) => snapshot.entries.filter((entry) => entry.updateId === ids[index]).length === 1 &&
|
|
2894
|
+
source?.entry.state === "pending" &&
|
|
2895
|
+
!source.entry.queueOwner &&
|
|
2896
|
+
!source.entry.queueReceiptId &&
|
|
2897
|
+
!source.entry.preApprovalExcluded &&
|
|
2898
|
+
isDeepStrictEqual(snapshot.entries.find((entry) => entry.updateId === ids[index]), source.entry)));
|
|
2899
|
+
}
|
|
2900
|
+
catch (error) {
|
|
2901
|
+
recordRuntimeEvent(error, { phase: "live-deferred-observe" });
|
|
2902
|
+
return false;
|
|
2903
|
+
}
|
|
2904
|
+
};
|
|
2905
|
+
if (!confirm())
|
|
2906
|
+
return undefined;
|
|
2907
|
+
const held = {
|
|
2908
|
+
version: 1,
|
|
2909
|
+
ids: new Set(ids),
|
|
2910
|
+
};
|
|
2911
|
+
livePreparation = held;
|
|
2912
|
+
const current = () => livePreparation === held && sourcesCurrent();
|
|
2913
|
+
const available = () => current() && !held.discardIssued;
|
|
2914
|
+
const finish = () => {
|
|
2915
|
+
livePreparation = undefined;
|
|
2916
|
+
updateClaimCounts();
|
|
2917
|
+
notifyStateChange();
|
|
2918
|
+
};
|
|
2919
|
+
updateClaimCounts();
|
|
2920
|
+
notifyStateChange();
|
|
2921
|
+
return {
|
|
2922
|
+
sourceUpdateIds: Object.freeze(ids),
|
|
2923
|
+
isCurrent: available,
|
|
2924
|
+
isOwnerCurrent: ownerCurrent,
|
|
2925
|
+
confirmSaved: () => available() && confirm(),
|
|
2926
|
+
beginRelease(canRelease) {
|
|
2927
|
+
if (!available() ||
|
|
2928
|
+
!canRelease() ||
|
|
2929
|
+
!confirm() ||
|
|
2930
|
+
!canRelease() ||
|
|
2931
|
+
!available())
|
|
2932
|
+
return false;
|
|
2933
|
+
for (const source of sources)
|
|
2934
|
+
source.liveReleaseIssued = true;
|
|
2935
|
+
finish();
|
|
2936
|
+
return true;
|
|
2937
|
+
},
|
|
2938
|
+
settleTransferred(canSettle) {
|
|
2939
|
+
if (!current() || !canSettle() || !completeExact)
|
|
2940
|
+
return "protected";
|
|
2941
|
+
if (held.discardIssued)
|
|
2942
|
+
return "unknown";
|
|
2943
|
+
if (!confirm() || !canSettle() || !available())
|
|
2944
|
+
return "protected";
|
|
2945
|
+
const expected = sources.map((source) => createTelegramUpdateJournalEntryDigest(source.entry));
|
|
2946
|
+
held.discardIssued = true;
|
|
2947
|
+
try {
|
|
2948
|
+
const result = completeExact(ids, expected, undefined, () => current() && canSettle());
|
|
2949
|
+
if (!current() ||
|
|
2950
|
+
!canSettle() ||
|
|
2951
|
+
result.removedUpdateIds.length !== ids.length ||
|
|
2952
|
+
!ids.every((id) => result.removedUpdateIds.includes(id)))
|
|
2953
|
+
return "unknown";
|
|
2954
|
+
}
|
|
2955
|
+
catch (error) {
|
|
2956
|
+
recordRuntimeEvent(error, {
|
|
2957
|
+
phase: "live-deferred-transfer-settlement",
|
|
2958
|
+
});
|
|
2959
|
+
return "unknown";
|
|
2960
|
+
}
|
|
2961
|
+
for (const id of ids) {
|
|
2962
|
+
claims.delete(id);
|
|
2963
|
+
deferredSources.delete(id);
|
|
2964
|
+
}
|
|
2965
|
+
finish();
|
|
2966
|
+
for (const id of ids) {
|
|
2967
|
+
if (!ownerCurrent() || !canSettle())
|
|
2968
|
+
return "unknown";
|
|
2969
|
+
state.lastCompletedUpdateId = id;
|
|
2970
|
+
state.lastCompletedAtMs = getNowMs();
|
|
2971
|
+
try {
|
|
2972
|
+
deps.onUpdateCompleted?.(id, expectedOwner.ctx, binding);
|
|
2973
|
+
}
|
|
2974
|
+
catch (error) {
|
|
2975
|
+
recordRuntimeEvent(error, {
|
|
2976
|
+
phase: "update-completion-observer",
|
|
2977
|
+
updateId: id,
|
|
2978
|
+
});
|
|
2979
|
+
}
|
|
2980
|
+
}
|
|
2981
|
+
return ownerCurrent() && canSettle() ? "settled" : "unknown";
|
|
2982
|
+
},
|
|
2983
|
+
cancel() {
|
|
2984
|
+
if (!available())
|
|
2985
|
+
return false;
|
|
2986
|
+
finish();
|
|
2987
|
+
return true;
|
|
2988
|
+
},
|
|
2989
|
+
};
|
|
2990
|
+
},
|
|
2991
|
+
prepareLiveInput(ctx, sourceUpdateIds, isCurrent) {
|
|
2992
|
+
const expectedOwner = owner, binding = deps.getJournalBindingKey?.();
|
|
2993
|
+
const ids = [...sourceUpdateIds];
|
|
2994
|
+
const currentOwner = () => !!expectedOwner &&
|
|
2995
|
+
owner === expectedOwner &&
|
|
2996
|
+
expectedOwner.ctx === ctx &&
|
|
2997
|
+
!expectedOwner.controller.signal.aborted &&
|
|
2998
|
+
!!binding &&
|
|
2999
|
+
deps.getJournalBindingKey?.() === binding &&
|
|
3000
|
+
deps.isContextCurrent?.(ctx) !== false &&
|
|
3001
|
+
deps.hasAuthority(ctx) &&
|
|
3002
|
+
isQueueOwnerIdentityCurrent(expectedOwner) &&
|
|
3003
|
+
isCurrent?.() !== false;
|
|
3004
|
+
if (!currentOwner() ||
|
|
3005
|
+
!expectedOwner?.startupUpdateIds ||
|
|
3006
|
+
livePreparation ||
|
|
3007
|
+
deps.executeCustodiedUpdate ||
|
|
3008
|
+
!completeExact ||
|
|
3009
|
+
!ids.length ||
|
|
3010
|
+
new Set(ids).size !== ids.length ||
|
|
3011
|
+
ids.some((id) => !Number.isSafeInteger(id) ||
|
|
3012
|
+
id < 0 ||
|
|
3013
|
+
claims.has(id) ||
|
|
3014
|
+
unsettledExecutionsByUpdateId.has(id) ||
|
|
3015
|
+
(state.phase === "executing" && state.currentUpdateId === id)))
|
|
3016
|
+
return undefined;
|
|
3017
|
+
let before;
|
|
3018
|
+
try {
|
|
3019
|
+
before = deps.journal.read();
|
|
3020
|
+
}
|
|
3021
|
+
catch (error) {
|
|
3022
|
+
recordRuntimeEvent(error, { phase: "live-input-prepare" });
|
|
3023
|
+
return undefined;
|
|
3024
|
+
}
|
|
3025
|
+
if (!currentOwner() ||
|
|
3026
|
+
!isTelegramUpdateJournalLegacyFamilyVersion(before.version) ||
|
|
3027
|
+
before.entries.some((entry) => ids.includes(entry.updateId)))
|
|
3028
|
+
return undefined;
|
|
3029
|
+
const held = {
|
|
3030
|
+
version: before.version,
|
|
3031
|
+
ids: new Set(ids),
|
|
3032
|
+
};
|
|
3033
|
+
let released = false, completionReadiness;
|
|
3034
|
+
livePreparation = held;
|
|
3035
|
+
const current = () => livePreparation === held && !held.released && currentOwner();
|
|
3036
|
+
const readPending = (full = true) => {
|
|
3037
|
+
if (!current())
|
|
3038
|
+
return undefined;
|
|
3039
|
+
try {
|
|
3040
|
+
const snapshot = deps.journal.read();
|
|
3041
|
+
const entries = snapshot.entries.filter((entry) => held.ids.has(entry.updateId));
|
|
3042
|
+
if (!current() ||
|
|
3043
|
+
snapshot.version !== held.version ||
|
|
3044
|
+
(full && entries.length !== ids.length) ||
|
|
3045
|
+
entries.some((entry) => entry.state !== "pending" ||
|
|
3046
|
+
entry.preApprovalExcluded === true ||
|
|
3047
|
+
entry.inputClaim ||
|
|
3048
|
+
entry.routingInput ||
|
|
3049
|
+
entry.failure ||
|
|
3050
|
+
entry.queueOwner ||
|
|
3051
|
+
entry.queueReceiptId ||
|
|
3052
|
+
entry.queueKind ||
|
|
3053
|
+
entry.queueHandoff ||
|
|
3054
|
+
"inputProvenance" in entry ||
|
|
3055
|
+
claims.has(entry.updateId) ||
|
|
3056
|
+
unsettledExecutionsByUpdateId.has(entry.updateId)))
|
|
3057
|
+
return undefined;
|
|
3058
|
+
return entries;
|
|
3059
|
+
}
|
|
3060
|
+
catch (error) {
|
|
3061
|
+
recordRuntimeEvent(error, { phase: "live-input-observe" });
|
|
3062
|
+
return undefined;
|
|
3063
|
+
}
|
|
3064
|
+
};
|
|
3065
|
+
const finish = () => {
|
|
3066
|
+
if (!held.released || !held.status)
|
|
3067
|
+
livePreparation = undefined;
|
|
3068
|
+
updateClaimCounts();
|
|
3069
|
+
notifyStateChange();
|
|
3070
|
+
blocked = false;
|
|
3071
|
+
pendingSignal = true;
|
|
3072
|
+
launchDrain();
|
|
3073
|
+
};
|
|
3074
|
+
updateClaimCounts();
|
|
3075
|
+
notifyStateChange();
|
|
3076
|
+
return {
|
|
3077
|
+
sourceUpdateIds: Object.freeze([...ids]),
|
|
3078
|
+
isCurrent: () => current() && !held.discardIssued,
|
|
3079
|
+
isOwnerCurrent: currentOwner,
|
|
3080
|
+
confirmSaved() {
|
|
3081
|
+
const entries = readPending();
|
|
3082
|
+
if (held.discardIssued ||
|
|
3083
|
+
!entries ||
|
|
3084
|
+
(held.saved && !isDeepStrictEqual(held.saved, entries)))
|
|
3085
|
+
return false;
|
|
3086
|
+
held.saved ??= structuredClone(entries);
|
|
3087
|
+
return true;
|
|
3088
|
+
},
|
|
3089
|
+
prepareSourceCompletion() {
|
|
3090
|
+
if (!current() ||
|
|
3091
|
+
held.discardIssued ||
|
|
3092
|
+
!held.saved ||
|
|
3093
|
+
ids.length !== 1 ||
|
|
3094
|
+
!deps.getQueueOwnerIdentity)
|
|
3095
|
+
return undefined;
|
|
3096
|
+
if (completionReadiness)
|
|
3097
|
+
return completionReadiness.isCurrent()
|
|
3098
|
+
? completionReadiness
|
|
3099
|
+
: undefined;
|
|
3100
|
+
const entries = readPending(), entry = entries?.[0], journal = deps.journal, read = journal.read, remove = journal.removeCompletedExact;
|
|
3101
|
+
if (!entry ||
|
|
3102
|
+
!isDeepStrictEqual(entries, held.saved) ||
|
|
3103
|
+
typeof remove !== "function" ||
|
|
3104
|
+
!completeExact ||
|
|
3105
|
+
!current())
|
|
3106
|
+
return undefined;
|
|
3107
|
+
const snapshot = {
|
|
3108
|
+
source: {
|
|
3109
|
+
journalBindingKey: binding,
|
|
3110
|
+
...createTelegramUpdateJournalEntryDigest(entry),
|
|
3111
|
+
},
|
|
3112
|
+
update: structuredClone(entry.update),
|
|
3113
|
+
};
|
|
3114
|
+
const ownerCurrent = () => !held.discardIssued &&
|
|
3115
|
+
currentOwner() &&
|
|
3116
|
+
deps.journal === journal &&
|
|
3117
|
+
journal.read === read &&
|
|
3118
|
+
journal.removeCompletedExact === remove;
|
|
3119
|
+
let retained;
|
|
3120
|
+
completionReadiness = {
|
|
3121
|
+
get snapshot() {
|
|
3122
|
+
return structuredClone(snapshot);
|
|
3123
|
+
},
|
|
3124
|
+
isCurrent: ownerCurrent,
|
|
3125
|
+
bindCarrier(value) {
|
|
3126
|
+
if (!released ||
|
|
3127
|
+
!ownerCurrent() ||
|
|
3128
|
+
getTelegramUpdateExecutionFence(value)?.signal !==
|
|
3129
|
+
expectedOwner.controller.signal)
|
|
3130
|
+
return undefined;
|
|
3131
|
+
if (retained)
|
|
3132
|
+
return value === retained.carrier &&
|
|
3133
|
+
retained.completion.isCurrent()
|
|
3134
|
+
? retained.completion
|
|
3135
|
+
: undefined;
|
|
3136
|
+
const completion = prepareTelegramDeferredSourceCompletion(value);
|
|
3137
|
+
if (!completion ||
|
|
3138
|
+
!isDeepStrictEqual(completion.source, snapshot.source) ||
|
|
3139
|
+
!ownerCurrent() ||
|
|
3140
|
+
!completion.isCurrent())
|
|
3141
|
+
return undefined;
|
|
3142
|
+
retained = { carrier: value, completion };
|
|
3143
|
+
return completion;
|
|
3144
|
+
},
|
|
3145
|
+
};
|
|
3146
|
+
return ownerCurrent() ? completionReadiness : undefined;
|
|
3147
|
+
},
|
|
3148
|
+
canPrepareStatusConsumption() {
|
|
3149
|
+
return (current() &&
|
|
3150
|
+
!held.discardIssued &&
|
|
3151
|
+
typeof deps.admitPreparedLiveInput === "function" &&
|
|
3152
|
+
!!deps.getQueueOwnerIdentity);
|
|
3153
|
+
},
|
|
3154
|
+
prepareStatusConsumption(input) {
|
|
3155
|
+
const admit = deps.admitPreparedLiveInput, ports = {
|
|
3156
|
+
assertCurrent: input.assertCurrent,
|
|
3157
|
+
bindCarrier: input.bindCarrier,
|
|
3158
|
+
execute: input.execute,
|
|
3159
|
+
};
|
|
3160
|
+
if (!current() ||
|
|
3161
|
+
held.status ||
|
|
3162
|
+
typeof admit !== "function" ||
|
|
3163
|
+
!completionReadiness ||
|
|
3164
|
+
!completionReadiness.isCurrent() ||
|
|
3165
|
+
Object.values(ports).some((port) => typeof port !== "function") ||
|
|
3166
|
+
!isDeepStrictEqual(input.source, completionReadiness.snapshot.source))
|
|
3167
|
+
return false;
|
|
3168
|
+
const readiness = completionReadiness, readyCurrent = readiness.isCurrent;
|
|
3169
|
+
held.status = {
|
|
3170
|
+
current: () => currentOwner() &&
|
|
3171
|
+
!held.discardIssued &&
|
|
3172
|
+
readiness.isCurrent === readyCurrent &&
|
|
3173
|
+
readyCurrent.call(readiness) &&
|
|
3174
|
+
deps.admitPreparedLiveInput === admit &&
|
|
3175
|
+
Object.entries(ports).every(([key, value]) => Reflect.get(input, key) === value),
|
|
3176
|
+
assertCurrent: ports.assertCurrent.bind(input),
|
|
3177
|
+
bindCarrier: ports.bindCarrier.bind(input),
|
|
3178
|
+
execute: ports.execute.bind(input),
|
|
3179
|
+
admit,
|
|
3180
|
+
};
|
|
3181
|
+
return held.status.current();
|
|
3182
|
+
},
|
|
3183
|
+
release(canRelease) {
|
|
3184
|
+
if (!current() ||
|
|
3185
|
+
held.discardIssued ||
|
|
3186
|
+
!held.saved ||
|
|
3187
|
+
!canRelease() ||
|
|
3188
|
+
held.status?.current() === false)
|
|
3189
|
+
return false;
|
|
3190
|
+
const entries = readPending();
|
|
3191
|
+
if (!entries ||
|
|
3192
|
+
!isDeepStrictEqual(entries, held.saved) ||
|
|
3193
|
+
!canRelease() ||
|
|
3194
|
+
!current())
|
|
3195
|
+
return false;
|
|
3196
|
+
released = true;
|
|
3197
|
+
held.released = true;
|
|
3198
|
+
finish();
|
|
3199
|
+
return true;
|
|
3200
|
+
},
|
|
3201
|
+
cancelEmpty() {
|
|
3202
|
+
if (!current() || held.discardIssued || held.saved)
|
|
3203
|
+
return false;
|
|
3204
|
+
try {
|
|
3205
|
+
const snapshot = deps.journal.read();
|
|
3206
|
+
if (!current() ||
|
|
3207
|
+
snapshot.version !== held.version ||
|
|
3208
|
+
snapshot.entries.some((entry) => held.ids.has(entry.updateId)))
|
|
3209
|
+
return false;
|
|
3210
|
+
}
|
|
3211
|
+
catch (error) {
|
|
3212
|
+
recordRuntimeEvent(error, { phase: "live-input-cancel-empty" });
|
|
3213
|
+
return false;
|
|
3214
|
+
}
|
|
3215
|
+
finish();
|
|
3216
|
+
return true;
|
|
3217
|
+
},
|
|
3218
|
+
discardSaved(canDiscard) {
|
|
3219
|
+
if (!current() || !canDiscard() || !completeExact)
|
|
3220
|
+
return "protected";
|
|
3221
|
+
if (held.discardIssued)
|
|
3222
|
+
return "unknown";
|
|
3223
|
+
const entries = readPending(false);
|
|
3224
|
+
if (!entries ||
|
|
3225
|
+
(held.saved && !isDeepStrictEqual(held.saved, entries)) ||
|
|
3226
|
+
!current() ||
|
|
3227
|
+
!canDiscard())
|
|
3228
|
+
return "protected";
|
|
3229
|
+
if (!entries.length) {
|
|
3230
|
+
finish();
|
|
3231
|
+
return "discarded";
|
|
3232
|
+
}
|
|
3233
|
+
const updateIds = entries.map((entry) => entry.updateId);
|
|
3234
|
+
const expected = entries.map((entry) => createTelegramUpdateJournalEntryDigest(entry));
|
|
3235
|
+
held.discardIssued = true;
|
|
3236
|
+
try {
|
|
3237
|
+
const result = completeExact(updateIds, expected, undefined, () => current() && canDiscard());
|
|
3238
|
+
if (!current() ||
|
|
3239
|
+
!canDiscard() ||
|
|
3240
|
+
result.removedUpdateIds.length !== updateIds.length ||
|
|
3241
|
+
!updateIds.every((id) => result.removedUpdateIds.includes(id)))
|
|
3242
|
+
return "unknown";
|
|
3243
|
+
}
|
|
3244
|
+
catch (error) {
|
|
3245
|
+
recordRuntimeEvent(error, { phase: "live-input-discard" });
|
|
3246
|
+
return "unknown";
|
|
3247
|
+
}
|
|
3248
|
+
finish();
|
|
3249
|
+
return "discarded";
|
|
3250
|
+
},
|
|
3251
|
+
};
|
|
3252
|
+
},
|
|
2289
3253
|
signal() {
|
|
2290
3254
|
if (!owner || owner.controller.signal.aborted)
|
|
2291
3255
|
return;
|
|
@@ -2293,55 +3257,294 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
2293
3257
|
pendingSignal = true;
|
|
2294
3258
|
launchDrain();
|
|
2295
3259
|
},
|
|
2296
|
-
armRoutingInput(input) {
|
|
2297
|
-
|
|
3260
|
+
armRoutingInput(input) {
|
|
3261
|
+
return mutateRoutingSource(input, false)?.lifetime;
|
|
3262
|
+
},
|
|
3263
|
+
selectRoutingInput(input) {
|
|
3264
|
+
return mutateRoutingSource(input, true)?.issued === true;
|
|
3265
|
+
},
|
|
2298
3266
|
isRoutingInputCurrent(input) {
|
|
2299
|
-
return owner?.controller.signal === input.signal &&
|
|
2300
|
-
|
|
3267
|
+
return (owner?.controller.signal === input.signal &&
|
|
3268
|
+
!input.signal.aborted &&
|
|
3269
|
+
deferredSources.has(input.updateId) &&
|
|
3270
|
+
claims.get(input.updateId) !== "abandoning");
|
|
2301
3271
|
},
|
|
2302
|
-
|
|
2303
|
-
const expectedOwner = owner;
|
|
2304
|
-
const
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
3272
|
+
prepareDeferredSourceCompletion(input) {
|
|
3273
|
+
const expectedOwner = owner, binding = deps.getJournalBindingKey?.(), journal = deps.journal, read = journal.read, remove = journal.removeCompletedExact;
|
|
3274
|
+
const current = () => !!expectedOwner &&
|
|
3275
|
+
owner === expectedOwner &&
|
|
3276
|
+
expectedOwner.controller.signal === input.signal &&
|
|
3277
|
+
!input.signal.aborted &&
|
|
3278
|
+
!!binding &&
|
|
3279
|
+
deps.getJournalBindingKey?.() === binding &&
|
|
3280
|
+
deps.isContextCurrent?.(expectedOwner.ctx) !== false &&
|
|
3281
|
+
deps.hasAuthority(expectedOwner.ctx) &&
|
|
3282
|
+
isQueueOwnerIdentityCurrent(expectedOwner) &&
|
|
3283
|
+
deps.journal === journal &&
|
|
3284
|
+
journal.read === read &&
|
|
3285
|
+
journal.removeCompletedExact === remove;
|
|
3286
|
+
if (!deps.getQueueOwnerIdentity ||
|
|
3287
|
+
!expectedOwner?.startupUpdateIds ||
|
|
3288
|
+
typeof remove !== "function" ||
|
|
3289
|
+
!completeExact ||
|
|
3290
|
+
!current() ||
|
|
3291
|
+
deps.executeCustodiedUpdate)
|
|
3292
|
+
return undefined;
|
|
3293
|
+
let original;
|
|
3294
|
+
try {
|
|
3295
|
+
original = inspectDeferredOriginal(input);
|
|
3296
|
+
}
|
|
3297
|
+
catch (error) {
|
|
3298
|
+
recordRuntimeEvent(error, {
|
|
3299
|
+
phase: "source-completion-prepare",
|
|
3300
|
+
updateId: input.updateId,
|
|
3301
|
+
});
|
|
3302
|
+
return undefined;
|
|
3303
|
+
}
|
|
3304
|
+
if (!original ||
|
|
3305
|
+
!current() ||
|
|
3306
|
+
!isFreshLiveSource(expectedOwner, input.updateId) ||
|
|
3307
|
+
original.entry.state !== "pending" ||
|
|
3308
|
+
original.entry.preApprovalExcluded ||
|
|
3309
|
+
original.entry.inputClaim ||
|
|
3310
|
+
original.entry.queueOwner ||
|
|
3311
|
+
original.entry.queueKind ||
|
|
3312
|
+
original.entry.queueReceiptId ||
|
|
3313
|
+
original.entry.queueHandoff ||
|
|
3314
|
+
original.entry.failure ||
|
|
3315
|
+
"inputProvenance" in original.entry)
|
|
3316
|
+
return undefined;
|
|
3317
|
+
const source = { ...original.source };
|
|
3318
|
+
return {
|
|
3319
|
+
get source() {
|
|
3320
|
+
return { ...source };
|
|
3321
|
+
},
|
|
3322
|
+
isCurrent: current,
|
|
2312
3323
|
};
|
|
2313
|
-
|
|
3324
|
+
},
|
|
3325
|
+
prepareDeferredQueueAdmission(input) {
|
|
3326
|
+
const expectedOwner = owner, binding = deps.getJournalBindingKey?.();
|
|
3327
|
+
const read = queueAdmissionRead, commit = queueAdmissionCommit, inspect = queueAdmissionInspect;
|
|
3328
|
+
const current = () => !!expectedOwner &&
|
|
3329
|
+
owner === expectedOwner &&
|
|
3330
|
+
expectedOwner.controller.signal === input.signal &&
|
|
3331
|
+
!input.signal.aborted &&
|
|
3332
|
+
!!binding &&
|
|
3333
|
+
deps.getJournalBindingKey?.() === binding &&
|
|
3334
|
+
deps.hasAuthority(expectedOwner.ctx) &&
|
|
3335
|
+
deps.isContextCurrent?.(expectedOwner.ctx) !== false &&
|
|
3336
|
+
isQueueOwnerIdentityCurrent(expectedOwner) &&
|
|
3337
|
+
deps.journal === queueAdmissionJournal &&
|
|
3338
|
+
deps.journal.read === read &&
|
|
3339
|
+
deps.journal.markQueued === commit &&
|
|
3340
|
+
deps.journal.isQueueReceiptCurrent === inspect;
|
|
3341
|
+
if (!deps.getQueueOwnerIdentity ||
|
|
3342
|
+
typeof commit !== "function" ||
|
|
3343
|
+
!inspect ||
|
|
3344
|
+
!current())
|
|
2314
3345
|
return undefined;
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
3346
|
+
let original;
|
|
3347
|
+
try {
|
|
3348
|
+
original = inspectDeferredOriginal(input);
|
|
3349
|
+
}
|
|
3350
|
+
catch (error) {
|
|
3351
|
+
recordRuntimeEvent(error, {
|
|
3352
|
+
phase: "queue-admission-prepare",
|
|
3353
|
+
updateId: input.updateId,
|
|
3354
|
+
});
|
|
2319
3355
|
return undefined;
|
|
2320
|
-
|
|
3356
|
+
}
|
|
3357
|
+
if (!original ||
|
|
3358
|
+
!current() ||
|
|
3359
|
+
!isFreshLiveSource(expectedOwner, input.updateId) ||
|
|
3360
|
+
original.entry.state !== "pending" ||
|
|
3361
|
+
original.entry.queueOwner ||
|
|
3362
|
+
original.entry.queueReceiptId ||
|
|
3363
|
+
original.entry.preApprovalExcluded)
|
|
3364
|
+
return undefined;
|
|
3365
|
+
const source = { ...original.source }, update = structuredClone(original.entry.update);
|
|
3366
|
+
const complete = queueAdmissionJournal.completeQueued, completeScoped = queueAdmissionJournal.completeQueuedExact;
|
|
3367
|
+
const readCompletion = queueAdmissionJournal.inspectSourceCompletion;
|
|
3368
|
+
const completionCurrent = () => current() &&
|
|
3369
|
+
queueAdmissionJournal.completeQueued === complete &&
|
|
3370
|
+
completeScoped === queueCompletionPort &&
|
|
3371
|
+
queueAdmissionJournal.completeQueuedExact === completeScoped &&
|
|
3372
|
+
readCompletion === completionReaderPort &&
|
|
3373
|
+
queueAdmissionJournal.inspectSourceCompletion === readCompletion;
|
|
3374
|
+
const inspectedReceipts = new WeakMap();
|
|
3375
|
+
return {
|
|
3376
|
+
isCurrent: current,
|
|
3377
|
+
inspectReceipt(receipt) {
|
|
3378
|
+
if (!current() ||
|
|
3379
|
+
receipt.journalBindingKey !== binding ||
|
|
3380
|
+
receipt.queueKind !== "prompt" ||
|
|
3381
|
+
!isDeepStrictEqual(receipt.sourceUpdateIds, [input.updateId]))
|
|
3382
|
+
return undefined;
|
|
3383
|
+
try {
|
|
3384
|
+
// The warm owner requires its actual publication ACK, not merely queued bytes or the report hint.
|
|
3385
|
+
const committed = getCurrentQueueReceipt(receipt);
|
|
3386
|
+
if (!committed ||
|
|
3387
|
+
!current() ||
|
|
3388
|
+
!isTelegramUpdateJournalQueueOwnerProcess(committed.queueOwner, expectedOwner.queueOwnerIdentity))
|
|
3389
|
+
return undefined;
|
|
3390
|
+
const entry = findExactQueuedPromptEntry(read.call(queueAdmissionJournal), receipt.receiptId, input.updateId, committed.queueOwner, update);
|
|
3391
|
+
if (!entry ||
|
|
3392
|
+
!current() ||
|
|
3393
|
+
!getCurrentQueueReceipt(receipt) ||
|
|
3394
|
+
!current())
|
|
3395
|
+
return undefined;
|
|
3396
|
+
const scope = committed.sourceCompletions?.find((scope) => scope.updateId === input.updateId);
|
|
3397
|
+
if (scope?.sourceSha256 ===
|
|
3398
|
+
createTelegramUpdateJournalEntryDigest(entry).sourceSha256) {
|
|
3399
|
+
inspectedReceipts.set(receipt, {
|
|
3400
|
+
receipt: normalizeQueueReceipt(committed.receipt),
|
|
3401
|
+
completion: {
|
|
3402
|
+
updateId: scope.updateId,
|
|
3403
|
+
sourceSha256: scope.sourceSha256,
|
|
3404
|
+
completionSha256: scope.completionSha256,
|
|
3405
|
+
},
|
|
3406
|
+
});
|
|
3407
|
+
}
|
|
3408
|
+
return {
|
|
3409
|
+
source: { ...source },
|
|
3410
|
+
receipt: {
|
|
3411
|
+
queueKind: "prompt",
|
|
3412
|
+
receiptId: committed.receipt.receiptId,
|
|
3413
|
+
sourceUpdateIds: [...committed.receipt.sourceUpdateIds],
|
|
3414
|
+
queueOwner: { ...committed.queueOwner },
|
|
3415
|
+
},
|
|
3416
|
+
};
|
|
3417
|
+
}
|
|
3418
|
+
catch (error) {
|
|
3419
|
+
recordRuntimeEvent(error, {
|
|
3420
|
+
phase: "queue-admission-observe",
|
|
3421
|
+
updateId: input.updateId,
|
|
3422
|
+
});
|
|
3423
|
+
return undefined;
|
|
3424
|
+
}
|
|
3425
|
+
},
|
|
3426
|
+
prepareCompletionScope(receipt, queueOwner, completionSha256) {
|
|
3427
|
+
if (!completionCurrent() ||
|
|
3428
|
+
!queueCompletionPort ||
|
|
3429
|
+
!completionReaderPort ||
|
|
3430
|
+
!/^[a-f0-9]{64}$/u.test(completionSha256) ||
|
|
3431
|
+
receipt.journalBindingKey !== binding ||
|
|
3432
|
+
receipt.queueKind !== "prompt" ||
|
|
3433
|
+
!isDeepStrictEqual(receipt.sourceUpdateIds, [input.updateId]) ||
|
|
3434
|
+
!isTelegramUpdateJournalQueueOwnerProcess(queueOwner, expectedOwner.queueOwnerIdentity))
|
|
3435
|
+
return undefined;
|
|
3436
|
+
try {
|
|
3437
|
+
if (!current())
|
|
3438
|
+
return undefined;
|
|
3439
|
+
const entry = findExactQueuedPromptEntry(read.call(queueAdmissionJournal), receipt.receiptId, input.updateId, queueOwner, update);
|
|
3440
|
+
if (!entry ||
|
|
3441
|
+
inspect.call(queueAdmissionJournal, normalizeQueueReceipt(receipt), { ...queueOwner }) !== true ||
|
|
3442
|
+
!completionCurrent())
|
|
3443
|
+
return undefined;
|
|
3444
|
+
const completion = {
|
|
3445
|
+
...createTelegramUpdateJournalEntryDigest(entry),
|
|
3446
|
+
completionSha256,
|
|
3447
|
+
};
|
|
3448
|
+
inspectedReceipts.set(receipt, {
|
|
3449
|
+
receipt: normalizeQueueReceipt(receipt),
|
|
3450
|
+
completion: { ...completion },
|
|
3451
|
+
});
|
|
3452
|
+
return { ...completion, journalBindingKey: binding };
|
|
3453
|
+
}
|
|
3454
|
+
catch (error) {
|
|
3455
|
+
recordRuntimeEvent(error, {
|
|
3456
|
+
phase: "queue-completion-scope",
|
|
3457
|
+
updateId: input.updateId,
|
|
3458
|
+
});
|
|
3459
|
+
return undefined;
|
|
3460
|
+
}
|
|
3461
|
+
},
|
|
3462
|
+
inspectCompletion(receipt) {
|
|
3463
|
+
const inspected = inspectedReceipts.get(receipt);
|
|
3464
|
+
// The native ACK belongs to this exact object and whole receipt, not an equivalent reconstructed query.
|
|
3465
|
+
if (!completionCurrent() ||
|
|
3466
|
+
!readCompletion ||
|
|
3467
|
+
!inspected ||
|
|
3468
|
+
!areTelegramQueueAdmissionReceiptsEqual(inspected.receipt, receipt) ||
|
|
3469
|
+
!isQueueReceiptCompletionAcknowledged(receipt) ||
|
|
3470
|
+
!completionCurrent())
|
|
3471
|
+
return undefined;
|
|
3472
|
+
try {
|
|
3473
|
+
const acknowledged = readCompletion.call(queueAdmissionJournal, {
|
|
3474
|
+
...inspected.completion,
|
|
3475
|
+
});
|
|
3476
|
+
return completionCurrent() &&
|
|
3477
|
+
isDeepStrictEqual(acknowledged, inspected.completion)
|
|
3478
|
+
? { ...source }
|
|
3479
|
+
: undefined;
|
|
3480
|
+
}
|
|
3481
|
+
catch (error) {
|
|
3482
|
+
recordRuntimeEvent(error, {
|
|
3483
|
+
phase: "queue-completion-observe",
|
|
3484
|
+
updateId: input.updateId,
|
|
3485
|
+
});
|
|
3486
|
+
return undefined;
|
|
3487
|
+
}
|
|
3488
|
+
},
|
|
3489
|
+
};
|
|
3490
|
+
},
|
|
3491
|
+
inspectDeferredSource(input) {
|
|
3492
|
+
return inspectDeferredOriginal(input)?.source;
|
|
3493
|
+
},
|
|
3494
|
+
inspectDeferredSourceSnapshot(input) {
|
|
3495
|
+
const original = inspectDeferredOriginal(input);
|
|
3496
|
+
return (original && {
|
|
3497
|
+
source: original.source,
|
|
3498
|
+
update: structuredClone(original.entry.update),
|
|
3499
|
+
});
|
|
2321
3500
|
},
|
|
2322
3501
|
isHistoricalSource(input) {
|
|
2323
3502
|
const source = deferredSources.get(input.updateId);
|
|
2324
|
-
const isCurrent = () => owner?.controller.signal === input.signal &&
|
|
2325
|
-
|
|
3503
|
+
const isCurrent = () => owner?.controller.signal === input.signal &&
|
|
3504
|
+
!input.signal.aborted &&
|
|
3505
|
+
source?.historical === true &&
|
|
3506
|
+
deferredSources.get(input.updateId) === source;
|
|
2326
3507
|
if (!isCurrent())
|
|
2327
3508
|
return false;
|
|
2328
3509
|
// Domain eligibility observes original evidence, never mutable handler projections.
|
|
2329
|
-
return (!input.matchesOriginal ||
|
|
3510
|
+
return ((!input.matchesOriginal ||
|
|
3511
|
+
input.matchesOriginal(structuredClone(source.entry)) === true) &&
|
|
3512
|
+
isCurrent());
|
|
3513
|
+
},
|
|
3514
|
+
getRoutingInput(input) {
|
|
3515
|
+
const lifetime = deferredSources.get(input.updateId)?.entry.routingInput;
|
|
3516
|
+
return owner?.controller.signal === input.signal &&
|
|
3517
|
+
!input.signal.aborted &&
|
|
3518
|
+
lifetime
|
|
3519
|
+
? structuredClone(lifetime)
|
|
3520
|
+
: undefined;
|
|
2330
3521
|
},
|
|
2331
3522
|
supportsDeferredAbandonment(input) {
|
|
2332
3523
|
const expectedOwner = owner;
|
|
2333
3524
|
const source = deferredSources.get(input.updateId);
|
|
2334
|
-
if (!expectedOwner ||
|
|
2335
|
-
|
|
2336
|
-
|
|
3525
|
+
if (!expectedOwner ||
|
|
3526
|
+
input.signal.aborted ||
|
|
3527
|
+
expectedOwner.controller.signal !== input.signal ||
|
|
3528
|
+
!source ||
|
|
3529
|
+
source.historical ||
|
|
3530
|
+
input.journalBindingKey !== source.journalBindingKey ||
|
|
3531
|
+
!deps.journal.abandonPending ||
|
|
3532
|
+
!deps.journal.inspectPendingRetention ||
|
|
2337
3533
|
claims.get(input.updateId) === "queued")
|
|
2338
3534
|
return false;
|
|
2339
|
-
return deps.isContextCurrent?.(expectedOwner.ctx) !== false &&
|
|
2340
|
-
deps.getJournalBindingKey?.() === source.journalBindingKey &&
|
|
3535
|
+
return (deps.isContextCurrent?.(expectedOwner.ctx) !== false &&
|
|
3536
|
+
deps.getJournalBindingKey?.() === source.journalBindingKey &&
|
|
3537
|
+
deps.hasAuthority(expectedOwner.ctx));
|
|
3538
|
+
},
|
|
3539
|
+
abandonDeferred(input) {
|
|
3540
|
+
return abandonSource(input);
|
|
3541
|
+
},
|
|
3542
|
+
inspectAbandoning(input) {
|
|
3543
|
+
return inspectRecovery(input, "abandoning");
|
|
3544
|
+
},
|
|
3545
|
+
inspectHistorical(input) {
|
|
3546
|
+
return inspectRecovery(input, "historical");
|
|
2341
3547
|
},
|
|
2342
|
-
abandonDeferred(input) { return abandonSource(input); },
|
|
2343
|
-
inspectAbandoning(input) { return inspectRecovery(input, "abandoning"); },
|
|
2344
|
-
inspectHistorical(input) { return inspectRecovery(input, "historical"); },
|
|
2345
3548
|
async settleDeferred(input) {
|
|
2346
3549
|
const expectedOwner = owner;
|
|
2347
3550
|
if (!expectedOwner ||
|
|
@@ -2359,7 +3562,11 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
2359
3562
|
return;
|
|
2360
3563
|
}
|
|
2361
3564
|
const claim = claims.get(input.updateId);
|
|
2362
|
-
if (
|
|
3565
|
+
if (livePreparation?.ids.has(input.updateId))
|
|
3566
|
+
return;
|
|
3567
|
+
if (claim === "abandoning" ||
|
|
3568
|
+
claim === "historical" ||
|
|
3569
|
+
claim === "retained" ||
|
|
2363
3570
|
(!claim && routingJournal?.inspectExpiry(input.updateId)))
|
|
2364
3571
|
return;
|
|
2365
3572
|
if (input.outcome.kind === "complete") {
|
|
@@ -2376,10 +3583,27 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
2376
3583
|
}
|
|
2377
3584
|
if (completion.kind !== "complete")
|
|
2378
3585
|
return;
|
|
2379
|
-
const
|
|
2380
|
-
|
|
2381
|
-
|
|
3586
|
+
const source = completion.expectedSource && {
|
|
3587
|
+
...completion.expectedSource,
|
|
3588
|
+
};
|
|
3589
|
+
const journalBindingKey = deps.getJournalBindingKey?.();
|
|
3590
|
+
const isCurrent = () => owner === expectedOwner &&
|
|
3591
|
+
!input.signal.aborted &&
|
|
3592
|
+
deps.isContextCurrent?.(expectedOwner.ctx) !== false &&
|
|
3593
|
+
deps.hasAuthority(expectedOwner.ctx) &&
|
|
3594
|
+
isQueueOwnerIdentityCurrent(expectedOwner) &&
|
|
3595
|
+
deps.getJournalBindingKey?.() === journalBindingKey;
|
|
3596
|
+
const result = commitCompletedBatch(expectedOwner, [
|
|
3597
|
+
{
|
|
3598
|
+
updateId: input.updateId,
|
|
3599
|
+
...(source ? { expectedSource: source } : {}),
|
|
3600
|
+
},
|
|
3601
|
+
], source ? isCurrent : undefined);
|
|
3602
|
+
if (!result) {
|
|
2382
3603
|
transition("idle", input.updateId);
|
|
3604
|
+
if (source && isCurrent())
|
|
3605
|
+
return { source, isCurrent };
|
|
3606
|
+
}
|
|
2383
3607
|
return;
|
|
2384
3608
|
}
|
|
2385
3609
|
if (input.outcome.kind === "deferred") {
|
|
@@ -2394,7 +3618,14 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
2394
3618
|
}
|
|
2395
3619
|
let outcome;
|
|
2396
3620
|
try {
|
|
2397
|
-
outcome = validateTelegramUpdateAdmissionOutcome(input.outcome, input.updateId, new Set([
|
|
3621
|
+
outcome = validateTelegramUpdateAdmissionOutcome(input.outcome, input.updateId, new Set([
|
|
3622
|
+
input.updateId,
|
|
3623
|
+
...[...claims]
|
|
3624
|
+
.filter(([, claim]) => claim !== "abandoning" &&
|
|
3625
|
+
claim !== "historical" &&
|
|
3626
|
+
claim !== "retained")
|
|
3627
|
+
.map(([id]) => id),
|
|
3628
|
+
]));
|
|
2398
3629
|
}
|
|
2399
3630
|
catch (error) {
|
|
2400
3631
|
blockWithFailure("invalid-outcome", "late-invalid-outcome", error, input.updateId);
|
|
@@ -2422,7 +3653,9 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
2422
3653
|
},
|
|
2423
3654
|
async settleCustodied(input) {
|
|
2424
3655
|
const expectedOwner = owner;
|
|
2425
|
-
if (!expectedOwner ||
|
|
3656
|
+
if (!expectedOwner ||
|
|
3657
|
+
expectedOwner.controller.signal !== input.signal ||
|
|
3658
|
+
input.signal.aborted)
|
|
2426
3659
|
return;
|
|
2427
3660
|
if (checkAuthority(expectedOwner, input.updateId))
|
|
2428
3661
|
return;
|
|
@@ -2446,7 +3679,8 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
2446
3679
|
}
|
|
2447
3680
|
const receipt = normalizeQueueReceipt(input.result.queueReceipt);
|
|
2448
3681
|
const existing = committedQueueReceipts.get(receipt.receiptId);
|
|
2449
|
-
if (existing &&
|
|
3682
|
+
if (existing &&
|
|
3683
|
+
areTelegramQueueAdmissionReceiptsEqual(existing.receipt, receipt) &&
|
|
2450
3684
|
areTelegramUpdateJournalQueueOwnersEqual(existing.queueOwner, input.result.queueReceipt.queueOwner))
|
|
2451
3685
|
return;
|
|
2452
3686
|
if (existing) {
|
|
@@ -2454,7 +3688,7 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
2454
3688
|
return;
|
|
2455
3689
|
}
|
|
2456
3690
|
if (!receipt.sourceUpdateIds.includes(input.updateId) ||
|
|
2457
|
-
receipt.sourceUpdateIds.some(updateId => claims.get(updateId) !== "deferred")) {
|
|
3691
|
+
receipt.sourceUpdateIds.some((updateId) => claims.get(updateId) !== "deferred")) {
|
|
2458
3692
|
blockWithFailure("invalid-outcome", "late-custody-unclaimed", new TelegramUpdateAdmissionOutcomeError(`Telegram update ${input.updateId} reported custodied queue without exact deferred claims.`), input.updateId);
|
|
2459
3693
|
return;
|
|
2460
3694
|
}
|
|
@@ -2473,21 +3707,29 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
2473
3707
|
}
|
|
2474
3708
|
transition("queued", input.updateId);
|
|
2475
3709
|
},
|
|
2476
|
-
isQueueReceiptCommitted: receipt => getCurrentQueueReceipt(receipt) !== undefined,
|
|
3710
|
+
isQueueReceiptCommitted: (receipt) => getCurrentQueueReceipt(receipt) !== undefined,
|
|
2477
3711
|
getQueueReceiptOwner(receipt) {
|
|
2478
3712
|
const committed = getCurrentQueueReceipt(receipt);
|
|
2479
3713
|
return committed ? { ...committed.queueOwner } : undefined;
|
|
2480
3714
|
},
|
|
2481
3715
|
getQueueReceiptSettlementOwner(receipt, ctx, reason) {
|
|
2482
3716
|
const expectedOwner = owner, committed = committedQueueReceipts.get(receipt.receiptId);
|
|
2483
|
-
if (!expectedOwner ||
|
|
2484
|
-
|
|
2485
|
-
!
|
|
2486
|
-
(
|
|
3717
|
+
if (!expectedOwner ||
|
|
3718
|
+
expectedOwner.controller.signal.aborted ||
|
|
3719
|
+
!(deps.isContextCurrent?.(ctx) ?? expectedOwner.ctx === ctx) ||
|
|
3720
|
+
!isQueueOwnerIdentityCurrent(expectedOwner) ||
|
|
3721
|
+
!committed ||
|
|
3722
|
+
!areTelegramQueueAdmissionReceiptsEqual(committed.receipt, normalizeQueueReceipt(receipt)) ||
|
|
3723
|
+
(deps.getJournalBindingKey &&
|
|
3724
|
+
committed.receipt.journalBindingKey !==
|
|
3725
|
+
deps.getJournalBindingKey()) ||
|
|
2487
3726
|
!isTelegramUpdateJournalQueueOwnerProcess(committed.queueOwner, expectedOwner.queueOwnerIdentity))
|
|
2488
3727
|
return undefined;
|
|
2489
3728
|
const attempt = scopedQueueCompletionAttempts.get(receipt.receiptId);
|
|
2490
|
-
if (attempt
|
|
3729
|
+
if (attempt
|
|
3730
|
+
? attempt.owner !== expectedOwner ||
|
|
3731
|
+
attempt.reason !== reason ||
|
|
3732
|
+
attempt.journalBindingKey !== deps.getJournalBindingKey?.()
|
|
2491
3733
|
: !getCurrentQueueReceipt(receipt))
|
|
2492
3734
|
return undefined;
|
|
2493
3735
|
return { ...committed.queueOwner };
|
|
@@ -2500,7 +3742,9 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
2500
3742
|
return false;
|
|
2501
3743
|
}
|
|
2502
3744
|
// An ACK permits only local completion reconciliation, never execution readiness.
|
|
2503
|
-
const receipts = input.sourceCompletions === undefined
|
|
3745
|
+
const receipts = input.sourceCompletions === undefined
|
|
3746
|
+
? input.receipts.filter((receipt) => !isQueueReceiptCompletionAcknowledged(receipt))
|
|
3747
|
+
: input.receipts;
|
|
2504
3748
|
if (receipts.length === 0)
|
|
2505
3749
|
return input.sourceCompletions === undefined;
|
|
2506
3750
|
const normalizedReceipts = receipts.map(normalizeQueueReceipt);
|
|
@@ -2528,83 +3772,138 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
2528
3772
|
}
|
|
2529
3773
|
}
|
|
2530
3774
|
const completionBinding = deps.getJournalBindingKey?.();
|
|
2531
|
-
const ordinaryObserverBound = normalizedReceipts.every(receipt => committedQueueReceipts.get(receipt.receiptId).receipt
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
3775
|
+
const ordinaryObserverBound = normalizedReceipts.every((receipt) => committedQueueReceipts.get(receipt.receiptId).receipt
|
|
3776
|
+
.journalBindingKey === completionBinding);
|
|
3777
|
+
const ordinaryCompletionCurrent = () => ordinaryObserverBound &&
|
|
3778
|
+
owner === expectedOwner &&
|
|
3779
|
+
!expectedOwner.controller.signal.aborted &&
|
|
3780
|
+
(deps.isContextCurrent?.(input.ctx) ??
|
|
3781
|
+
expectedOwner.ctx === input.ctx) &&
|
|
3782
|
+
isQueueOwnerIdentityCurrent(expectedOwner) &&
|
|
3783
|
+
deps.getJournalBindingKey?.() === completionBinding;
|
|
2535
3784
|
let removedUpdateIds;
|
|
2536
3785
|
let scopedCompletionCurrent;
|
|
2537
3786
|
try {
|
|
2538
|
-
const prepared = normalizedReceipts.flatMap(receipt => committedQueueReceipts.get(receipt.receiptId).sourceCompletions ??
|
|
2539
|
-
|
|
2540
|
-
|
|
3787
|
+
const prepared = normalizedReceipts.flatMap((receipt) => committedQueueReceipts.get(receipt.receiptId).sourceCompletions ??
|
|
3788
|
+
[]);
|
|
3789
|
+
const sourceCompletions = input.sourceCompletions === undefined
|
|
3790
|
+
? prepared.length
|
|
3791
|
+
? prepared
|
|
3792
|
+
: undefined
|
|
3793
|
+
: input.sourceCompletions;
|
|
3794
|
+
if (input.sourceCompletions !== undefined &&
|
|
3795
|
+
prepared.some((expected) => !input.sourceCompletions.some((value) => isDeepStrictEqual(value, expected)))) {
|
|
2541
3796
|
throw new Error("Telegram queued completion conflicts with its prepared source scopes.");
|
|
2542
3797
|
}
|
|
2543
|
-
if (input.sourceCompletions === undefined &&
|
|
2544
|
-
|
|
2545
|
-
|
|
3798
|
+
if (input.sourceCompletions === undefined &&
|
|
3799
|
+
prepared.length &&
|
|
3800
|
+
receipts.some((receipt) => !requiredScopedQueueReceipts.has(receipt.receiptId))) {
|
|
3801
|
+
const scoped = receipts.filter((receipt) => requiredScopedQueueReceipts.has(receipt.receiptId));
|
|
3802
|
+
const ordinary = receipts.filter((receipt) => !requiredScopedQueueReceipts.has(receipt.receiptId));
|
|
2546
3803
|
if (!scoped.length || !ordinary.length)
|
|
2547
3804
|
throw new Error("Telegram mixed queue completion has incomplete scoped authority.");
|
|
2548
3805
|
const binding = deps.getJournalBindingKey?.();
|
|
2549
|
-
const current = () => owner === expectedOwner &&
|
|
2550
|
-
|
|
2551
|
-
|
|
3806
|
+
const current = () => owner === expectedOwner &&
|
|
3807
|
+
!expectedOwner.controller.signal.aborted &&
|
|
3808
|
+
(deps.isContextCurrent?.(input.ctx) ??
|
|
3809
|
+
expectedOwner.ctx === input.ctx) &&
|
|
3810
|
+
isQueueOwnerIdentityCurrent(expectedOwner) &&
|
|
3811
|
+
!!binding &&
|
|
3812
|
+
deps.getJournalBindingKey?.() === binding;
|
|
2552
3813
|
// Independent whole receipts need independent ACKs. Ordinary siblings must not acquire invented scopes.
|
|
2553
|
-
if (!current() ||
|
|
3814
|
+
if (!current() ||
|
|
3815
|
+
!completeQueueReceipts({
|
|
3816
|
+
receipts: scoped,
|
|
3817
|
+
ctx: input.ctx,
|
|
3818
|
+
reason: input.reason,
|
|
3819
|
+
}) ||
|
|
3820
|
+
!current())
|
|
2554
3821
|
return false;
|
|
2555
|
-
return completeQueueReceipts({
|
|
3822
|
+
return completeQueueReceipts({
|
|
3823
|
+
receipts: ordinary,
|
|
3824
|
+
ctx: input.ctx,
|
|
3825
|
+
reason: input.reason,
|
|
3826
|
+
});
|
|
2556
3827
|
}
|
|
2557
3828
|
if (sourceCompletions === undefined) {
|
|
2558
|
-
if (queuedCompletions.some(receipt => requiredScopedQueueReceipts.has(receipt.receiptId))) {
|
|
3829
|
+
if (queuedCompletions.some((receipt) => requiredScopedQueueReceipts.has(receipt.receiptId))) {
|
|
2559
3830
|
throw new Error("Telegram scoped queue completion cannot downgrade required source scopes.");
|
|
2560
3831
|
}
|
|
2561
|
-
removedUpdateIds =
|
|
3832
|
+
removedUpdateIds =
|
|
3833
|
+
deps.journal.completeQueued(queuedCompletions).removedUpdateIds;
|
|
2562
3834
|
}
|
|
2563
3835
|
else {
|
|
2564
3836
|
for (const receipt of queuedCompletions)
|
|
2565
3837
|
requiredScopedQueueReceipts.add(receipt.receiptId);
|
|
2566
3838
|
queuedCompletions.sort((a, b) => a.receiptId < b.receiptId ? -1 : a.receiptId > b.receiptId ? 1 : 0);
|
|
2567
3839
|
const journalBindingKey = deps.getJournalBindingKey?.();
|
|
2568
|
-
const current = () => owner === expectedOwner &&
|
|
2569
|
-
|
|
3840
|
+
const current = () => owner === expectedOwner &&
|
|
3841
|
+
!expectedOwner.controller.signal.aborted &&
|
|
3842
|
+
(deps.isContextCurrent?.(input.ctx) ??
|
|
3843
|
+
expectedOwner.ctx === input.ctx) &&
|
|
3844
|
+
isQueueOwnerIdentityCurrent(expectedOwner) &&
|
|
2570
3845
|
deps.getJournalBindingKey?.() === journalBindingKey;
|
|
2571
|
-
if (!journalBindingKey ||
|
|
2572
|
-
!
|
|
3846
|
+
if (!journalBindingKey ||
|
|
3847
|
+
!current() ||
|
|
3848
|
+
!completeQueuedExact ||
|
|
3849
|
+
!inspectCompletion ||
|
|
3850
|
+
!Array.isArray(sourceCompletions) ||
|
|
3851
|
+
(input.sourceCompletions !== undefined &&
|
|
3852
|
+
sourceCompletions.length !== sourceUpdateIds.size)) {
|
|
2573
3853
|
throw new Error("Telegram scoped queue completion requires full current source authority.");
|
|
2574
3854
|
}
|
|
2575
3855
|
scopedCompletionCurrent = current;
|
|
2576
|
-
const completions = normalizeTelegramQueueSourceCompletions(sourceCompletions, sourceUpdateIds, journalBindingKey, input.sourceCompletions !== undefined)
|
|
2577
|
-
|
|
2578
|
-
if (queuedCompletions.some(receipt => !completions.some(scope => receipt.sourceUpdateIds.includes(scope.updateId)))) {
|
|
3856
|
+
const completions = normalizeTelegramQueueSourceCompletions(sourceCompletions, sourceUpdateIds, journalBindingKey, input.sourceCompletions !== undefined).map(({ journalBindingKey: _binding, ...completion }) => completion);
|
|
3857
|
+
if (queuedCompletions.some((receipt) => !completions.some((scope) => receipt.sourceUpdateIds.includes(scope.updateId)))) {
|
|
2579
3858
|
throw new Error("Telegram scoped queue completion requires a proven origin for every whole receipt.");
|
|
2580
3859
|
}
|
|
2581
|
-
const previous = queuedCompletions.map(receipt => scopedQueueCompletionAttempts.get(receipt.receiptId));
|
|
2582
|
-
const retained = previous.find(attempt => attempt !== undefined);
|
|
2583
|
-
if (retained &&
|
|
2584
|
-
|
|
2585
|
-
|
|
3860
|
+
const previous = queuedCompletions.map((receipt) => scopedQueueCompletionAttempts.get(receipt.receiptId));
|
|
3861
|
+
const retained = previous.find((attempt) => attempt !== undefined);
|
|
3862
|
+
if (retained &&
|
|
3863
|
+
(previous.some((attempt) => attempt !== retained) ||
|
|
3864
|
+
retained.owner !== expectedOwner ||
|
|
3865
|
+
retained.journalBindingKey !== journalBindingKey ||
|
|
3866
|
+
retained.reason !== input.reason ||
|
|
3867
|
+
!isDeepStrictEqual(retained.receipts, queuedCompletions) ||
|
|
3868
|
+
!isDeepStrictEqual(retained.completions, completions))) {
|
|
2586
3869
|
throw new Error("Telegram scoped queue completion has conflicting issued authority.");
|
|
2587
3870
|
}
|
|
2588
3871
|
if (!retained) {
|
|
2589
|
-
const attempt = {
|
|
2590
|
-
|
|
2591
|
-
|
|
3872
|
+
const attempt = {
|
|
3873
|
+
owner: expectedOwner,
|
|
3874
|
+
journalBindingKey,
|
|
3875
|
+
reason: input.reason,
|
|
3876
|
+
receipts: queuedCompletions.map((receipt) => ({
|
|
3877
|
+
...receipt,
|
|
3878
|
+
sourceUpdateIds: [...receipt.sourceUpdateIds],
|
|
3879
|
+
queueOwner: { ...receipt.queueOwner },
|
|
3880
|
+
})),
|
|
3881
|
+
completions: completions.map((completion) => ({ ...completion })),
|
|
3882
|
+
};
|
|
2592
3883
|
if (!current())
|
|
2593
3884
|
throw new Error("Telegram scoped queue completion authority changed before disposal.");
|
|
2594
3885
|
// Mark before issuance: an exception may follow a committed rename, and cannot license another disposal.
|
|
2595
3886
|
for (const receipt of queuedCompletions)
|
|
2596
3887
|
scopedQueueCompletionAttempts.set(receipt.receiptId, attempt);
|
|
2597
|
-
const result = completeQueuedExact(queuedCompletions.map(
|
|
2598
|
-
|
|
3888
|
+
const result = completeQueuedExact(queuedCompletions.map((receipt) => ({
|
|
3889
|
+
...receipt,
|
|
3890
|
+
sourceUpdateIds: [...receipt.sourceUpdateIds],
|
|
3891
|
+
queueOwner: { ...receipt.queueOwner },
|
|
3892
|
+
})), completions.map((completion) => ({ ...completion })));
|
|
3893
|
+
if (!current() ||
|
|
3894
|
+
!isDeepStrictEqual(result.sourceCompletions, completions)) {
|
|
2599
3895
|
throw new Error("Telegram scoped queue completion ACK was not confirmed.");
|
|
2600
3896
|
}
|
|
2601
3897
|
const removed = new Set(result.removedUpdateIds);
|
|
2602
|
-
if (removed.size !== sourceUpdateIds.size ||
|
|
3898
|
+
if (removed.size !== sourceUpdateIds.size ||
|
|
3899
|
+
[...sourceUpdateIds].some((id) => !removed.has(id))) {
|
|
2603
3900
|
throw new Error("Telegram scoped queue completion did not remove every source.");
|
|
2604
3901
|
}
|
|
2605
3902
|
}
|
|
2606
3903
|
for (const completion of completions) {
|
|
2607
|
-
if (!current() ||
|
|
3904
|
+
if (!current() ||
|
|
3905
|
+
!isDeepStrictEqual(inspectCompletion({ ...completion }), completion) ||
|
|
3906
|
+
!current()) {
|
|
2608
3907
|
throw new Error("Telegram scoped queue completion ACK was not retained under current authority.");
|
|
2609
3908
|
}
|
|
2610
3909
|
}
|
|
@@ -2647,7 +3946,10 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
2647
3946
|
observeQueuedCompletion?.(normalizeQueueReceipt(receipt), input.ctx);
|
|
2648
3947
|
}
|
|
2649
3948
|
catch (error) {
|
|
2650
|
-
recordRuntimeEvent(error, {
|
|
3949
|
+
recordRuntimeEvent(error, {
|
|
3950
|
+
phase: "queue-receipt-completion-observer",
|
|
3951
|
+
receiptId: receipt.receiptId,
|
|
3952
|
+
});
|
|
2651
3953
|
}
|
|
2652
3954
|
}
|
|
2653
3955
|
// Ordinary queued disposal is source completion too; notify only after a whole positive ACK and fresh origin authority.
|
|
@@ -2659,7 +3961,10 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
2659
3961
|
deps.onUpdateCompleted?.(updateId, input.ctx, completionBinding);
|
|
2660
3962
|
}
|
|
2661
3963
|
catch (error) {
|
|
2662
|
-
recordRuntimeEvent(error, {
|
|
3964
|
+
recordRuntimeEvent(error, {
|
|
3965
|
+
phase: "queue-source-completion-observer",
|
|
3966
|
+
updateId,
|
|
3967
|
+
});
|
|
2663
3968
|
}
|
|
2664
3969
|
}
|
|
2665
3970
|
if (!scopedCompletionCurrent || scopedCompletionCurrent()) {
|
|
@@ -2680,6 +3985,8 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
2680
3985
|
if (owner !== expectedOwner)
|
|
2681
3986
|
return;
|
|
2682
3987
|
owner = undefined;
|
|
3988
|
+
livePreparation = undefined;
|
|
3989
|
+
delete state.preparedInputCount;
|
|
2683
3990
|
drainPromise = undefined;
|
|
2684
3991
|
claims.clear();
|
|
2685
3992
|
deferredSources.clear();
|
|
@@ -2689,30 +3996,12 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
2689
3996
|
state.phaseStartedAtMs = getNowMs();
|
|
2690
3997
|
state.currentUpdateId = undefined;
|
|
2691
3998
|
state.blockedReason = undefined;
|
|
2692
|
-
state.
|
|
2693
|
-
delete state.abandoningClaimCount;
|
|
2694
|
-
delete state.historicalClaimCount;
|
|
2695
|
-
state.queuedClaimCount = 0;
|
|
2696
|
-
state.foreignQueuedCount = 0;
|
|
2697
|
-
delete state.foreignQueuedOwner;
|
|
2698
|
-
state.retryWaitCount = 0;
|
|
2699
|
-
state.failedCount = 0;
|
|
2700
|
-
state.nextRetryUpdateId = undefined;
|
|
2701
|
-
state.nextRetryAtMs = undefined;
|
|
2702
|
-
state.nextRetryAttemptCount = undefined;
|
|
2703
|
-
state.nextRetryFailureClass = undefined;
|
|
2704
|
-
state.failedUpdateId = undefined;
|
|
2705
|
-
state.failedFailureId = undefined;
|
|
2706
|
-
state.failedAttemptCount = undefined;
|
|
2707
|
-
state.failedClass = undefined;
|
|
2708
|
-
state.failedSummary = undefined;
|
|
2709
|
-
state.terminalFailureAtMs = undefined;
|
|
2710
|
-
state.unsettledExecutionCount = unsettledExecutions.size;
|
|
3999
|
+
clearTelegramUpdateWorkerClaimState(state, unsettledExecutions.size);
|
|
2711
4000
|
notifyStateChange();
|
|
2712
4001
|
},
|
|
2713
4002
|
async waitForDrain() {
|
|
2714
4003
|
await (drainPromise ?? Promise.resolve());
|
|
2715
|
-
await Promise.allSettled([...pendingQueuePublications.values()].map(value => value.task));
|
|
4004
|
+
await Promise.allSettled([...pendingQueuePublications.values()].map((value) => value.task));
|
|
2716
4005
|
},
|
|
2717
4006
|
getState() {
|
|
2718
4007
|
return getTelegramUpdateWorkerStateSnapshot(state);
|
|
@@ -2805,19 +4094,6 @@ function getOrCreateUpdateHandlerRegistry() {
|
|
|
2805
4094
|
export function getTelegramUpdateHandlerRegistry() {
|
|
2806
4095
|
return getOrCreateUpdateHandlerRegistry();
|
|
2807
4096
|
}
|
|
2808
|
-
/**
|
|
2809
|
-
* Wrap a default polling `handleUpdate` with the public update handler registry.
|
|
2810
|
-
*/
|
|
2811
|
-
export function createTelegramUpdateHandle(deps) {
|
|
2812
|
-
const registry = deps.registry ?? getOrCreateUpdateHandlerRegistry();
|
|
2813
|
-
const { defaultHandle } = deps;
|
|
2814
|
-
return async (update, ctx) => {
|
|
2815
|
-
const verdict = await registry.dispatch(update);
|
|
2816
|
-
if (verdict === "consume")
|
|
2817
|
-
return;
|
|
2818
|
-
await defaultHandle(update, ctx);
|
|
2819
|
-
};
|
|
2820
|
-
}
|
|
2821
4097
|
function mergeTelegramReportedAdmissionOutcome(current, next, updateId) {
|
|
2822
4098
|
if (!current || current.kind === "deferred")
|
|
2823
4099
|
return next;
|
|
@@ -2829,16 +4105,20 @@ function mergeTelegramReportedAdmissionOutcome(current, next, updateId) {
|
|
|
2829
4105
|
if (!next.expectedSource)
|
|
2830
4106
|
return current;
|
|
2831
4107
|
if (current.expectedSource.updateId === next.expectedSource.updateId &&
|
|
2832
|
-
current.expectedSource.journalBindingKey ===
|
|
4108
|
+
current.expectedSource.journalBindingKey ===
|
|
4109
|
+
next.expectedSource.journalBindingKey &&
|
|
2833
4110
|
current.expectedSource.sourceSha256 === next.expectedSource.sourceSha256) {
|
|
2834
4111
|
if (!current.expectedSource.completionSha256)
|
|
2835
4112
|
return next;
|
|
2836
|
-
if (!next.expectedSource.completionSha256 ||
|
|
4113
|
+
if (!next.expectedSource.completionSha256 ||
|
|
4114
|
+
current.expectedSource.completionSha256 ===
|
|
4115
|
+
next.expectedSource.completionSha256)
|
|
2837
4116
|
return current;
|
|
2838
4117
|
}
|
|
2839
4118
|
throw new TelegramUpdateAdmissionOutcomeError(`Telegram update ${updateId} reported conflicting completion sources.`);
|
|
2840
4119
|
}
|
|
2841
|
-
if (current.kind === "queued" &&
|
|
4120
|
+
if (current.kind === "queued" &&
|
|
4121
|
+
next.kind === "queued" &&
|
|
2842
4122
|
areTelegramQueueAdmissionReceiptsEqual(current, next))
|
|
2843
4123
|
return current;
|
|
2844
4124
|
throw new TelegramUpdateAdmissionOutcomeError(`Telegram update ${updateId} reported conflicting queue outcomes.`);
|
|
@@ -2850,17 +4130,23 @@ export function createTelegramInputCustodyWorkerJournalPort(store) {
|
|
|
2850
4130
|
return {
|
|
2851
4131
|
read: () => store.read(),
|
|
2852
4132
|
isQueueReceiptCurrent(receipt, owner) {
|
|
2853
|
-
const entries = store
|
|
2854
|
-
|
|
4133
|
+
const entries = store
|
|
4134
|
+
.read()
|
|
4135
|
+
.entries.filter((entry) => entry.queueReceiptId === receipt.receiptId);
|
|
4136
|
+
return (receipt.sourceUpdateIds.length > 0 &&
|
|
4137
|
+
entries.length === receipt.sourceUpdateIds.length &&
|
|
2855
4138
|
entries.every((entry, index) => entry.updateId === receipt.sourceUpdateIds[index] &&
|
|
2856
|
-
entry.state === "queued" &&
|
|
2857
|
-
entry.
|
|
4139
|
+
entry.state === "queued" &&
|
|
4140
|
+
entry.queueKind === receipt.queueKind &&
|
|
4141
|
+
!entry.queueHandoff &&
|
|
4142
|
+
entry.queueOwner !== undefined &&
|
|
4143
|
+
areTelegramUpdateJournalQueueOwnersEqual(entry.queueOwner, owner)));
|
|
2858
4144
|
},
|
|
2859
4145
|
markQueued: legacyMutation,
|
|
2860
4146
|
markExecutionFailure: legacyMutation,
|
|
2861
4147
|
// The common drain batches excluded IDs here; the store rejects all non-vetoed input atomically.
|
|
2862
|
-
removeCompleted: updateIds => store.removeExcluded(updateIds),
|
|
2863
|
-
completeQueued: receipts => store.completeQueued(receipts),
|
|
4148
|
+
removeCompleted: (updateIds) => store.removeExcluded(updateIds),
|
|
4149
|
+
completeQueued: (receipts) => store.completeQueued(receipts),
|
|
2864
4150
|
inputCustody: {
|
|
2865
4151
|
acquireInput: store.acquireInput,
|
|
2866
4152
|
startInput: store.startInput,
|
|
@@ -2873,7 +4159,7 @@ export function createTelegramInputCustodyLegacyDispositionRuntime(deps) {
|
|
|
2873
4159
|
const withBinding = (recoveryKey, operation) => {
|
|
2874
4160
|
if (!recoveryKey)
|
|
2875
4161
|
throw new TelegramUpdateAdmissionOutcomeError("Telegram legacy custody disposition binding is unavailable.");
|
|
2876
|
-
return deps.withBindingReference(recoveryKey, binding => {
|
|
4162
|
+
return deps.withBindingReference(recoveryKey, (binding) => {
|
|
2877
4163
|
if (binding.recoveryKey !== recoveryKey)
|
|
2878
4164
|
throw new TelegramUpdateAdmissionOutcomeError("Telegram legacy custody disposition binding is unavailable.");
|
|
2879
4165
|
return operation(binding.journal);
|
|
@@ -2881,10 +4167,10 @@ export function createTelegramInputCustodyLegacyDispositionRuntime(deps) {
|
|
|
2881
4167
|
};
|
|
2882
4168
|
return {
|
|
2883
4169
|
list(recoveryKey) {
|
|
2884
|
-
return withBinding(recoveryKey, journal => journal.listLegacyCustodyCandidates());
|
|
4170
|
+
return withBinding(recoveryKey, (journal) => journal.listLegacyCustodyCandidates());
|
|
2885
4171
|
},
|
|
2886
4172
|
apply(recoveryKey, authority) {
|
|
2887
|
-
return withBinding(recoveryKey, journal => journal.applyLegacyCustodyDisposition(authority));
|
|
4173
|
+
return withBinding(recoveryKey, (journal) => journal.applyLegacyCustodyDisposition(authority));
|
|
2888
4174
|
},
|
|
2889
4175
|
};
|
|
2890
4176
|
}
|
|
@@ -2893,44 +4179,62 @@ export function createTelegramInputCustodyHandoffClient(deps) {
|
|
|
2893
4179
|
async transfer(input) {
|
|
2894
4180
|
const reconciled = deps.resolveAcceptedReference?.({
|
|
2895
4181
|
sourceUpdateId: input.receipt.updateId,
|
|
2896
|
-
recipientBindingKey: input.recipientBindingKey
|
|
4182
|
+
recipientBindingKey: input.recipientBindingKey,
|
|
2897
4183
|
});
|
|
2898
4184
|
if (reconciled)
|
|
2899
4185
|
return { ...reconciled, duplicate: true };
|
|
2900
|
-
const offered = deps.journal.offerInputHandoff({
|
|
2901
|
-
|
|
4186
|
+
const offered = deps.journal.offerInputHandoff({
|
|
4187
|
+
receipt: input.receipt,
|
|
4188
|
+
recipientOwner: input.recipientOwner,
|
|
4189
|
+
handoffToken: input.handoffToken,
|
|
4190
|
+
});
|
|
2902
4191
|
const envelope = {
|
|
2903
|
-
kind: "leader.offerInputCustodyHandoff",
|
|
4192
|
+
kind: "leader.offerInputCustodyHandoff",
|
|
4193
|
+
requestId: input.requestId,
|
|
2904
4194
|
recipientInstanceId: input.recipientInstanceId,
|
|
2905
4195
|
recipientRegistrationGeneration: input.recipientRegistrationGeneration,
|
|
2906
4196
|
recipientBindingKey: input.recipientBindingKey,
|
|
2907
4197
|
sourceRecoveryKey: offered.source.journalBindingKey,
|
|
2908
|
-
source: offered.source,
|
|
2909
|
-
|
|
4198
|
+
source: offered.source,
|
|
4199
|
+
handoffId: offered.handoff.handoffId,
|
|
4200
|
+
sentAtMs: input.sentAtMs,
|
|
4201
|
+
...(input.auth ? { auth: input.auth } : {}),
|
|
2910
4202
|
};
|
|
2911
4203
|
const response = await deps.sendEnvelope(envelope);
|
|
2912
|
-
if (response?.kind !== "bus.ack" ||
|
|
2913
|
-
|
|
4204
|
+
if (response?.kind !== "bus.ack" ||
|
|
4205
|
+
response.requestId !== input.requestId ||
|
|
4206
|
+
!response.ok ||
|
|
4207
|
+
!response.result ||
|
|
4208
|
+
typeof response.result !== "object" ||
|
|
2914
4209
|
Array.isArray(response.result))
|
|
2915
4210
|
throw new Error("Telegram input custody handoff acknowledgement is missing or rejected.");
|
|
2916
4211
|
const result = response.result;
|
|
2917
4212
|
const source = result.source;
|
|
2918
4213
|
if (result.sourceRecoveryKey !== offered.source.journalBindingKey ||
|
|
2919
|
-
!source ||
|
|
2920
|
-
source
|
|
4214
|
+
!source ||
|
|
4215
|
+
typeof source !== "object" ||
|
|
4216
|
+
Array.isArray(source) ||
|
|
4217
|
+
source.updateId !==
|
|
4218
|
+
offered.source.updateId ||
|
|
2921
4219
|
!source.owner ||
|
|
2922
4220
|
typeof source.owner !== "object" ||
|
|
2923
4221
|
Array.isArray(source.owner) ||
|
|
2924
4222
|
typeof source.owner.acquisitionId !== "string" ||
|
|
2925
|
-
source.owner
|
|
4223
|
+
source.owner
|
|
4224
|
+
.handoffId !== offered.handoff.handoffId ||
|
|
2926
4225
|
typeof result.duplicate !== "boolean")
|
|
2927
4226
|
throw new Error("Telegram input custody handoff acknowledgement returned mismatched authority.");
|
|
2928
|
-
return {
|
|
2929
|
-
|
|
4227
|
+
return {
|
|
4228
|
+
sourceRecoveryKey: result.sourceRecoveryKey,
|
|
4229
|
+
source: {
|
|
4230
|
+
updateId: source.updateId,
|
|
4231
|
+
owner: {
|
|
2930
4232
|
acquisitionId: source.owner.acquisitionId,
|
|
2931
|
-
handoffId: source.owner.handoffId
|
|
2932
|
-
}
|
|
2933
|
-
|
|
4233
|
+
handoffId: source.owner.handoffId,
|
|
4234
|
+
},
|
|
4235
|
+
},
|
|
4236
|
+
duplicate: result.duplicate,
|
|
4237
|
+
};
|
|
2934
4238
|
},
|
|
2935
4239
|
};
|
|
2936
4240
|
}
|
|
@@ -2938,56 +4242,88 @@ export function createTelegramInputCustodyHandoffAcceptanceRuntime(deps) {
|
|
|
2938
4242
|
return {
|
|
2939
4243
|
accept(input, ctx) {
|
|
2940
4244
|
const binding = deps.resolveBinding(input.sourceRecoveryKey);
|
|
2941
|
-
if (!binding ||
|
|
4245
|
+
if (!binding ||
|
|
4246
|
+
binding.recoveryKey !== input.sourceRecoveryKey ||
|
|
2942
4247
|
binding.recipientBindingKey !== input.recipientBindingKey ||
|
|
2943
4248
|
input.source.journalBindingKey !== input.sourceRecoveryKey)
|
|
2944
4249
|
throw new Error("Telegram input custody handoff binding is unavailable or changed.");
|
|
2945
|
-
const accepted = binding.journal.acceptInputHandoff({
|
|
2946
|
-
|
|
4250
|
+
const accepted = binding.journal.acceptInputHandoff({
|
|
4251
|
+
source: input.source,
|
|
4252
|
+
recipientOwner: binding.recipientOwner,
|
|
4253
|
+
handoffId: input.handoffId,
|
|
4254
|
+
});
|
|
2947
4255
|
const acceptedHandoffId = accepted.receipt.owner.handoffId;
|
|
2948
4256
|
if (!acceptedHandoffId || acceptedHandoffId !== input.handoffId)
|
|
2949
4257
|
throw new Error("Telegram input custody handoff acceptance returned mismatched authority.");
|
|
2950
4258
|
binding.signalWorker(ctx);
|
|
2951
|
-
return {
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
4259
|
+
return {
|
|
4260
|
+
sourceRecoveryKey: binding.recoveryKey,
|
|
4261
|
+
source: {
|
|
4262
|
+
updateId: accepted.receipt.updateId,
|
|
4263
|
+
owner: {
|
|
4264
|
+
acquisitionId: accepted.receipt.owner.acquisitionId,
|
|
4265
|
+
handoffId: acceptedHandoffId,
|
|
4266
|
+
},
|
|
4267
|
+
},
|
|
4268
|
+
duplicate: accepted.duplicate,
|
|
4269
|
+
};
|
|
2955
4270
|
},
|
|
2956
4271
|
};
|
|
2957
4272
|
}
|
|
2958
4273
|
export function createTelegramInputCustodyForwardReferenceResolver(deps) {
|
|
2959
|
-
return input => {
|
|
4274
|
+
return (input) => {
|
|
2960
4275
|
if (input.recipientBindingKey !== deps.recipientBindingKey)
|
|
2961
4276
|
return undefined;
|
|
2962
|
-
const entry = deps.journal
|
|
4277
|
+
const entry = deps.journal
|
|
4278
|
+
.read()
|
|
4279
|
+
.entries.find((candidate) => candidate.updateId === input.sourceUpdateId);
|
|
2963
4280
|
const claim = entry?.inputClaim;
|
|
2964
|
-
if (!entry ||
|
|
2965
|
-
|
|
4281
|
+
if (!entry ||
|
|
4282
|
+
entry.state !== "pending" ||
|
|
4283
|
+
entry.preApprovalExcluded !== false ||
|
|
4284
|
+
!claim ||
|
|
4285
|
+
claim.phase !== "ready" ||
|
|
4286
|
+
claim.handoff ||
|
|
2966
4287
|
claim.recipientBindingKey !== deps.recipientBindingKey ||
|
|
2967
4288
|
!claim.owner.handoffId ||
|
|
2968
4289
|
claim.owner.sessionGeneration !== deps.recipientOwner.sessionGeneration ||
|
|
2969
4290
|
!isTelegramUpdateJournalQueueOwnerProcess(claim.owner, deps.recipientOwner))
|
|
2970
4291
|
return undefined;
|
|
2971
|
-
return {
|
|
2972
|
-
|
|
2973
|
-
|
|
4292
|
+
return {
|
|
4293
|
+
sourceRecoveryKey: deps.recoveryKey,
|
|
4294
|
+
source: {
|
|
4295
|
+
updateId: entry.updateId,
|
|
4296
|
+
owner: {
|
|
4297
|
+
acquisitionId: claim.owner.acquisitionId,
|
|
4298
|
+
handoffId: claim.owner.handoffId,
|
|
4299
|
+
},
|
|
4300
|
+
},
|
|
4301
|
+
};
|
|
2974
4302
|
};
|
|
2975
4303
|
}
|
|
2976
4304
|
export function createTelegramInputCustodySourceReferenceWakeRuntime(deps) {
|
|
2977
4305
|
return {
|
|
2978
4306
|
wakeSource(input, ctx) {
|
|
2979
4307
|
const binding = deps.resolveBinding(input.sourceRecoveryKey);
|
|
2980
|
-
if (!binding ||
|
|
4308
|
+
if (!binding ||
|
|
4309
|
+
binding.recoveryKey !== input.sourceRecoveryKey ||
|
|
2981
4310
|
binding.recipientBindingKey !== input.recipientBindingKey)
|
|
2982
4311
|
throw new Error("Telegram follower source-reference binding is unavailable or changed.");
|
|
2983
|
-
const entry = binding.journal
|
|
4312
|
+
const entry = binding.journal
|
|
4313
|
+
.read()
|
|
4314
|
+
.entries.find((candidate) => candidate.updateId === input.sourceUpdateId);
|
|
2984
4315
|
const claim = entry?.inputClaim;
|
|
2985
|
-
if (!entry ||
|
|
2986
|
-
|
|
4316
|
+
if (!entry ||
|
|
4317
|
+
entry.state !== "pending" ||
|
|
4318
|
+
entry.preApprovalExcluded !== false ||
|
|
4319
|
+
!claim ||
|
|
4320
|
+
claim.phase !== "ready" ||
|
|
4321
|
+
claim.handoff ||
|
|
2987
4322
|
claim.recipientBindingKey !== input.recipientBindingKey ||
|
|
2988
4323
|
claim.owner.acquisitionId !== input.sourceClaim.acquisitionId ||
|
|
2989
4324
|
claim.owner.handoffId !== input.sourceClaim.handoffId ||
|
|
2990
|
-
claim.owner.sessionGeneration !==
|
|
4325
|
+
claim.owner.sessionGeneration !==
|
|
4326
|
+
binding.recipientOwner.sessionGeneration ||
|
|
2991
4327
|
!isTelegramUpdateJournalQueueOwnerProcess(claim.owner, binding.recipientOwner))
|
|
2992
4328
|
throw new Error("Telegram follower source-reference claim is unavailable or changed.");
|
|
2993
4329
|
binding.signalWorker(ctx);
|
|
@@ -2996,10 +4332,10 @@ export function createTelegramInputCustodySourceReferenceWakeRuntime(deps) {
|
|
|
2996
4332
|
}
|
|
2997
4333
|
export function createTelegramInputCustodyBusBindingRuntime(deps) {
|
|
2998
4334
|
const acceptance = createTelegramInputCustodyHandoffAcceptanceRuntime({
|
|
2999
|
-
resolveBinding: deps.resolveBinding
|
|
4335
|
+
resolveBinding: deps.resolveBinding,
|
|
3000
4336
|
});
|
|
3001
4337
|
const wake = createTelegramInputCustodySourceReferenceWakeRuntime({
|
|
3002
|
-
resolveBinding: deps.resolveBinding
|
|
4338
|
+
resolveBinding: deps.resolveBinding,
|
|
3003
4339
|
});
|
|
3004
4340
|
return {
|
|
3005
4341
|
acceptHandoff: acceptance.accept,
|
|
@@ -3029,7 +4365,7 @@ export function evaluateTelegramInputCustodyActivationReadiness(input) {
|
|
|
3029
4365
|
return { enabled: false, blocker: "migration-incomplete" };
|
|
3030
4366
|
if (input.sourceStatus !== "absent" && input.sourceStatus !== "v3")
|
|
3031
4367
|
return { enabled: false, blocker: "source-unready" };
|
|
3032
|
-
if (input.peerReadiness.some(readiness => readiness !== "ready"))
|
|
4368
|
+
if (input.peerReadiness.some((readiness) => readiness !== "ready"))
|
|
3033
4369
|
return { enabled: false, blocker: "peer-capability-mismatch" };
|
|
3034
4370
|
return { enabled: true };
|
|
3035
4371
|
}
|
|
@@ -3049,9 +4385,17 @@ export function createTelegramInputCustodyReadinessEvidenceStore(deps) {
|
|
|
3049
4385
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
3050
4386
|
throw new Error("Telegram custody readiness evidence is malformed.");
|
|
3051
4387
|
const snapshot = value;
|
|
3052
|
-
if (snapshot.version !== 1 ||
|
|
3053
|
-
|
|
3054
|
-
|
|
4388
|
+
if (snapshot.version !== 1 ||
|
|
4389
|
+
!Number.isSafeInteger(snapshot.revision) ||
|
|
4390
|
+
snapshot.revision < 0 ||
|
|
4391
|
+
Object.keys(snapshot).some((key) => ![
|
|
4392
|
+
"version",
|
|
4393
|
+
"revision",
|
|
4394
|
+
"writerExclusion",
|
|
4395
|
+
"migration",
|
|
4396
|
+
"startupExclusion",
|
|
4397
|
+
"migrationCompletion",
|
|
4398
|
+
].includes(key)))
|
|
3055
4399
|
throw new Error("Telegram custody readiness evidence is malformed.");
|
|
3056
4400
|
const validate = (evidence, kind) => {
|
|
3057
4401
|
if (evidence === undefined)
|
|
@@ -3059,61 +4403,104 @@ export function createTelegramInputCustodyReadinessEvidenceStore(deps) {
|
|
|
3059
4403
|
if (!evidence || typeof evidence !== "object" || Array.isArray(evidence))
|
|
3060
4404
|
throw new Error("Telegram custody readiness evidence is malformed.");
|
|
3061
4405
|
const record = evidence;
|
|
3062
|
-
const statuses = kind === "writer"
|
|
4406
|
+
const statuses = kind === "writer"
|
|
4407
|
+
? ["excluded", "present", "unknown"]
|
|
3063
4408
|
: ["complete", "incomplete", "unknown"];
|
|
3064
|
-
const writerKeys = [
|
|
3065
|
-
|
|
3066
|
-
"
|
|
4409
|
+
const writerKeys = [
|
|
4410
|
+
"startupAuthorityId",
|
|
4411
|
+
"closureOperationId",
|
|
4412
|
+
"writerInventorySha256",
|
|
4413
|
+
];
|
|
4414
|
+
const migrationKeys = [
|
|
4415
|
+
"migrationAuthorityId",
|
|
4416
|
+
"startupAuthorityId",
|
|
4417
|
+
"closureOperationId",
|
|
4418
|
+
"migrationInventorySha256",
|
|
4419
|
+
"resultingSourceFamily",
|
|
4420
|
+
];
|
|
3067
4421
|
const linkedKeys = kind === "writer" ? writerKeys : migrationKeys;
|
|
3068
|
-
const suppliedLinkedKeys = linkedKeys.filter(key => record[key] !== undefined);
|
|
3069
|
-
const linkedMalformed = suppliedLinkedKeys.length === linkedKeys.length &&
|
|
3070
|
-
typeof record.
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
typeof record.
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
4422
|
+
const suppliedLinkedKeys = linkedKeys.filter((key) => record[key] !== undefined);
|
|
4423
|
+
const linkedMalformed = suppliedLinkedKeys.length === linkedKeys.length &&
|
|
4424
|
+
(typeof record.startupAuthorityId !== "string" ||
|
|
4425
|
+
!record.startupAuthorityId ||
|
|
4426
|
+
typeof record.closureOperationId !== "string" ||
|
|
4427
|
+
!record.closureOperationId ||
|
|
4428
|
+
(kind === "writer"
|
|
4429
|
+
? typeof record.writerInventorySha256 !== "string" ||
|
|
4430
|
+
!/^[a-f0-9]{64}$/u.test(record.writerInventorySha256)
|
|
4431
|
+
: typeof record.migrationAuthorityId !== "string" ||
|
|
4432
|
+
!record.migrationAuthorityId ||
|
|
4433
|
+
typeof record.migrationInventorySha256 !== "string" ||
|
|
4434
|
+
!/^[a-f0-9]{64}$/u.test(record.migrationInventorySha256) ||
|
|
4435
|
+
(record.resultingSourceFamily !== "absent" &&
|
|
4436
|
+
record.resultingSourceFamily !== "v3")));
|
|
4437
|
+
if (record.version !== 1 ||
|
|
4438
|
+
typeof record.profileKey !== "string" ||
|
|
4439
|
+
!record.profileKey ||
|
|
4440
|
+
typeof record.recoveryKey !== "string" ||
|
|
4441
|
+
!record.recoveryKey ||
|
|
4442
|
+
!statuses.includes(record.status) ||
|
|
4443
|
+
Object.keys(record).some((key) => ![
|
|
4444
|
+
"version",
|
|
4445
|
+
"profileKey",
|
|
4446
|
+
"recoveryKey",
|
|
4447
|
+
"status",
|
|
4448
|
+
...linkedKeys,
|
|
4449
|
+
].includes(key)) ||
|
|
4450
|
+
(suppliedLinkedKeys.length !== 0 &&
|
|
4451
|
+
suppliedLinkedKeys.length !== linkedKeys.length) ||
|
|
3082
4452
|
linkedMalformed)
|
|
3083
4453
|
throw new Error("Telegram custody readiness evidence is malformed.");
|
|
3084
|
-
return {
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
4454
|
+
return {
|
|
4455
|
+
version: 1,
|
|
4456
|
+
profileKey: record.profileKey,
|
|
4457
|
+
recoveryKey: record.recoveryKey,
|
|
4458
|
+
status: record.status,
|
|
4459
|
+
...(suppliedLinkedKeys.length === linkedKeys.length
|
|
4460
|
+
? kind === "writer"
|
|
4461
|
+
? {
|
|
4462
|
+
startupAuthorityId: record.startupAuthorityId,
|
|
4463
|
+
closureOperationId: record.closureOperationId,
|
|
4464
|
+
writerInventorySha256: record.writerInventorySha256,
|
|
4465
|
+
}
|
|
4466
|
+
: {
|
|
4467
|
+
migrationAuthorityId: record.migrationAuthorityId,
|
|
4468
|
+
startupAuthorityId: record.startupAuthorityId,
|
|
4469
|
+
closureOperationId: record.closureOperationId,
|
|
4470
|
+
migrationInventorySha256: record.migrationInventorySha256,
|
|
4471
|
+
resultingSourceFamily: record.resultingSourceFamily,
|
|
4472
|
+
}
|
|
4473
|
+
: {}),
|
|
4474
|
+
};
|
|
3097
4475
|
};
|
|
3098
4476
|
const writerExclusion = validate(snapshot.writerExclusion, "writer");
|
|
3099
4477
|
const migration = validate(snapshot.migration, "migration");
|
|
3100
4478
|
let startupExclusion;
|
|
3101
4479
|
if (snapshot.startupExclusion !== undefined) {
|
|
3102
4480
|
const candidate = snapshot.startupExclusion;
|
|
3103
|
-
if (!candidate ||
|
|
4481
|
+
if (!candidate ||
|
|
4482
|
+
typeof candidate.profileKey !== "string" ||
|
|
3104
4483
|
typeof candidate.recoveryKey !== "string")
|
|
3105
4484
|
throw new Error("Telegram custody readiness evidence is malformed.");
|
|
3106
|
-
startupExclusion = normalizeTelegramInputCustodyStartupExclusionAuthority(candidate, {
|
|
4485
|
+
startupExclusion = normalizeTelegramInputCustodyStartupExclusionAuthority(candidate, {
|
|
4486
|
+
profileKey: candidate.profileKey,
|
|
4487
|
+
recoveryKey: candidate.recoveryKey,
|
|
4488
|
+
});
|
|
3107
4489
|
if (!startupExclusion)
|
|
3108
4490
|
throw new Error("Telegram custody readiness evidence is malformed.");
|
|
3109
4491
|
}
|
|
3110
4492
|
let migrationCompletion;
|
|
3111
4493
|
if (snapshot.migrationCompletion !== undefined) {
|
|
3112
4494
|
const candidate = snapshot.migrationCompletion;
|
|
3113
|
-
if (!candidate ||
|
|
4495
|
+
if (!candidate ||
|
|
4496
|
+
typeof candidate.profileKey !== "string" ||
|
|
3114
4497
|
typeof candidate.recoveryKey !== "string")
|
|
3115
4498
|
throw new Error("Telegram custody readiness evidence is malformed.");
|
|
3116
|
-
migrationCompletion =
|
|
4499
|
+
migrationCompletion =
|
|
4500
|
+
normalizeTelegramInputCustodyMigrationCompletionAuthority(candidate, {
|
|
4501
|
+
profileKey: candidate.profileKey,
|
|
4502
|
+
recoveryKey: candidate.recoveryKey,
|
|
4503
|
+
});
|
|
3117
4504
|
if (!migrationCompletion)
|
|
3118
4505
|
throw new Error("Telegram custody readiness evidence is malformed.");
|
|
3119
4506
|
}
|
|
@@ -3126,48 +4513,72 @@ export function createTelegramInputCustodyReadinessEvidenceStore(deps) {
|
|
|
3126
4513
|
identities.push(startupExclusion);
|
|
3127
4514
|
if (migrationCompletion)
|
|
3128
4515
|
identities.push(migrationCompletion);
|
|
3129
|
-
if (identities.some(evidence => evidence.profileKey !== identities[0]?.profileKey ||
|
|
4516
|
+
if (identities.some((evidence) => evidence.profileKey !== identities[0]?.profileKey ||
|
|
3130
4517
|
evidence.recoveryKey !== identities[0]?.recoveryKey))
|
|
3131
4518
|
throw new Error("Telegram custody readiness evidence has conflicting identities.");
|
|
3132
|
-
if (writerExclusion?.startupAuthorityId &&
|
|
4519
|
+
if (writerExclusion?.startupAuthorityId &&
|
|
4520
|
+
startupExclusion &&
|
|
3133
4521
|
(writerExclusion.startupAuthorityId !== startupExclusion.authorityId ||
|
|
3134
|
-
writerExclusion.closureOperationId !==
|
|
3135
|
-
|
|
4522
|
+
writerExclusion.closureOperationId !==
|
|
4523
|
+
startupExclusion.closureOperationId ||
|
|
4524
|
+
writerExclusion.writerInventorySha256 !==
|
|
4525
|
+
startupExclusion.writerInventorySha256))
|
|
3136
4526
|
throw new Error("Telegram custody readiness evidence has conflicting authorities.");
|
|
3137
|
-
if (migration?.migrationAuthorityId &&
|
|
4527
|
+
if (migration?.migrationAuthorityId &&
|
|
4528
|
+
migrationCompletion &&
|
|
3138
4529
|
(migration.migrationAuthorityId !== migrationCompletion.authorityId ||
|
|
3139
|
-
migration.startupAuthorityId !==
|
|
3140
|
-
|
|
3141
|
-
migration.
|
|
3142
|
-
|
|
4530
|
+
migration.startupAuthorityId !==
|
|
4531
|
+
migrationCompletion.startupAuthorityId ||
|
|
4532
|
+
migration.closureOperationId !==
|
|
4533
|
+
migrationCompletion.closureOperationId ||
|
|
4534
|
+
migration.migrationInventorySha256 !==
|
|
4535
|
+
migrationCompletion.migrationInventorySha256 ||
|
|
4536
|
+
migration.resultingSourceFamily !==
|
|
4537
|
+
migrationCompletion.resultingSourceFamily))
|
|
3143
4538
|
throw new Error("Telegram custody readiness evidence has conflicting migration authorities.");
|
|
3144
|
-
if (migrationCompletion &&
|
|
3145
|
-
|
|
3146
|
-
|
|
4539
|
+
if (migrationCompletion &&
|
|
4540
|
+
startupExclusion &&
|
|
4541
|
+
(migrationCompletion.startupAuthorityId !==
|
|
4542
|
+
startupExclusion.authorityId ||
|
|
4543
|
+
migrationCompletion.closureOperationId !==
|
|
4544
|
+
startupExclusion.closureOperationId))
|
|
3147
4545
|
throw new Error("Telegram custody readiness evidence has conflicting cutover authorities.");
|
|
3148
|
-
return {
|
|
4546
|
+
return {
|
|
4547
|
+
version: 1,
|
|
4548
|
+
revision: snapshot.revision,
|
|
3149
4549
|
...(writerExclusion ? { writerExclusion } : {}),
|
|
3150
4550
|
...(migration ? { migration } : {}),
|
|
3151
4551
|
...(startupExclusion ? { startupExclusion } : {}),
|
|
3152
|
-
...(migrationCompletion ? { migrationCompletion } : {})
|
|
4552
|
+
...(migrationCompletion ? { migrationCompletion } : {}),
|
|
4553
|
+
};
|
|
3153
4554
|
};
|
|
3154
4555
|
const read = () => decode(deps.readRetained());
|
|
3155
4556
|
const publish = (input) => deps.withSerialization(() => {
|
|
3156
4557
|
const current = read();
|
|
3157
4558
|
if (current.revision !== input.expectedRevision)
|
|
3158
4559
|
throw new Error("Telegram custody readiness evidence revision changed.");
|
|
3159
|
-
const next = {
|
|
3160
|
-
...
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
4560
|
+
const next = {
|
|
4561
|
+
...current,
|
|
4562
|
+
revision: current.revision + 1,
|
|
4563
|
+
...(input.kind === "writer-exclusion"
|
|
4564
|
+
? { writerExclusion: input.evidence }
|
|
4565
|
+
: input.kind === "migration"
|
|
4566
|
+
? { migration: input.evidence }
|
|
4567
|
+
: input.kind === "startup-exclusion"
|
|
4568
|
+
? { startupExclusion: input.evidence }
|
|
4569
|
+
: { migrationCompletion: input.evidence }),
|
|
4570
|
+
};
|
|
3164
4571
|
const serialized = `${JSON.stringify(next)}\n`;
|
|
3165
4572
|
const validated = decode(serialized);
|
|
3166
4573
|
const normalizedEvidence = input.kind === "writer-exclusion"
|
|
3167
|
-
? validated.writerExclusion
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
4574
|
+
? validated.writerExclusion
|
|
4575
|
+
: input.kind === "migration"
|
|
4576
|
+
? validated.migration
|
|
4577
|
+
: input.kind === "startup-exclusion"
|
|
4578
|
+
? validated.startupExclusion
|
|
4579
|
+
: validated.migrationCompletion;
|
|
4580
|
+
if (!normalizedEvidence ||
|
|
4581
|
+
!deps.authorizePublication(input.kind, normalizedEvidence))
|
|
3171
4582
|
throw new Error("Telegram custody readiness evidence publication is unauthorized.");
|
|
3172
4583
|
deps.publishRetained(serialized);
|
|
3173
4584
|
return validated;
|
|
@@ -3178,23 +4589,45 @@ export function normalizeTelegramInputCustodyStartupExclusionAuthority(value, ex
|
|
|
3178
4589
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
3179
4590
|
return undefined;
|
|
3180
4591
|
const record = value;
|
|
3181
|
-
const keys = [
|
|
3182
|
-
"
|
|
3183
|
-
|
|
4592
|
+
const keys = [
|
|
4593
|
+
"version",
|
|
4594
|
+
"status",
|
|
4595
|
+
"authorityId",
|
|
4596
|
+
"closureOperationId",
|
|
4597
|
+
"profileKey",
|
|
4598
|
+
"recoveryKey",
|
|
4599
|
+
"writerInventorySha256",
|
|
4600
|
+
"allowedWriterProtocol",
|
|
4601
|
+
"authorizedAtMs",
|
|
4602
|
+
];
|
|
4603
|
+
if (Object.keys(record).some((key) => !keys.includes(key)) ||
|
|
4604
|
+
record.version !== 1 ||
|
|
3184
4605
|
(record.status !== "enforced" && record.status !== "revoked") ||
|
|
3185
|
-
record.profileKey !== expected.profileKey ||
|
|
3186
|
-
|
|
3187
|
-
typeof record.
|
|
4606
|
+
record.profileKey !== expected.profileKey ||
|
|
4607
|
+
record.recoveryKey !== expected.recoveryKey ||
|
|
4608
|
+
typeof record.authorityId !== "string" ||
|
|
4609
|
+
!record.authorityId ||
|
|
4610
|
+
record.authorityId.length > 512 ||
|
|
4611
|
+
typeof record.closureOperationId !== "string" ||
|
|
4612
|
+
!record.closureOperationId ||
|
|
3188
4613
|
record.closureOperationId.length > 512 ||
|
|
3189
4614
|
typeof record.writerInventorySha256 !== "string" ||
|
|
3190
4615
|
!/^[a-f0-9]{64}$/u.test(record.writerInventorySha256) ||
|
|
3191
4616
|
record.allowedWriterProtocol !== "custody-v3" ||
|
|
3192
|
-
!Number.isSafeInteger(record.authorizedAtMs) ||
|
|
4617
|
+
!Number.isSafeInteger(record.authorizedAtMs) ||
|
|
4618
|
+
record.authorizedAtMs < 0)
|
|
3193
4619
|
return undefined;
|
|
3194
|
-
return {
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
4620
|
+
return {
|
|
4621
|
+
version: 1,
|
|
4622
|
+
status: record.status,
|
|
4623
|
+
authorityId: record.authorityId,
|
|
4624
|
+
closureOperationId: record.closureOperationId,
|
|
4625
|
+
profileKey: expected.profileKey,
|
|
4626
|
+
recoveryKey: expected.recoveryKey,
|
|
4627
|
+
writerInventorySha256: record.writerInventorySha256,
|
|
4628
|
+
allowedWriterProtocol: "custody-v3",
|
|
4629
|
+
authorizedAtMs: record.authorizedAtMs,
|
|
4630
|
+
};
|
|
3198
4631
|
}
|
|
3199
4632
|
export function evaluateTelegramInputCustodyWriterExclusionEvidence(input) {
|
|
3200
4633
|
const identity = { ...input.expected };
|
|
@@ -3203,14 +4636,17 @@ export function evaluateTelegramInputCustodyWriterExclusionEvidence(input) {
|
|
|
3203
4636
|
input.inventory.profileKey !== input.expected.profileKey ||
|
|
3204
4637
|
input.inventory.recoveryKey !== input.expected.recoveryKey ||
|
|
3205
4638
|
!/^[a-f0-9]{64}$/u.test(input.inventory.writerInventorySha256) ||
|
|
3206
|
-
!authority ||
|
|
4639
|
+
!authority ||
|
|
4640
|
+
authority.status !== "enforced" ||
|
|
3207
4641
|
authority.profileKey !== input.expected.profileKey ||
|
|
3208
4642
|
authority.recoveryKey !== input.expected.recoveryKey ||
|
|
3209
4643
|
authority.writerInventorySha256 !== input.inventory.writerInventorySha256)
|
|
3210
4644
|
return { version: 1, status: "unknown", ...identity };
|
|
3211
|
-
const authorityBinding = {
|
|
4645
|
+
const authorityBinding = {
|
|
4646
|
+
startupAuthorityId: authority.authorityId,
|
|
3212
4647
|
closureOperationId: authority.closureOperationId,
|
|
3213
|
-
writerInventorySha256: authority.writerInventorySha256
|
|
4648
|
+
writerInventorySha256: authority.writerInventorySha256,
|
|
4649
|
+
};
|
|
3214
4650
|
let unknown = false;
|
|
3215
4651
|
for (const writer of input.inventory.writers) {
|
|
3216
4652
|
let liveness;
|
|
@@ -3221,49 +4657,82 @@ export function evaluateTelegramInputCustodyWriterExclusionEvidence(input) {
|
|
|
3221
4657
|
liveness = "unverifiable";
|
|
3222
4658
|
}
|
|
3223
4659
|
if (liveness === "alive")
|
|
3224
|
-
return {
|
|
3225
|
-
|
|
4660
|
+
return {
|
|
4661
|
+
version: 1,
|
|
4662
|
+
status: "present",
|
|
4663
|
+
...identity,
|
|
4664
|
+
...authorityBinding,
|
|
4665
|
+
};
|
|
3226
4666
|
if (liveness !== "dead")
|
|
3227
4667
|
unknown = true;
|
|
3228
4668
|
}
|
|
3229
|
-
return {
|
|
3230
|
-
|
|
4669
|
+
return {
|
|
4670
|
+
version: 1,
|
|
4671
|
+
status: unknown ? "unknown" : "excluded",
|
|
4672
|
+
...identity,
|
|
4673
|
+
...authorityBinding,
|
|
4674
|
+
};
|
|
3231
4675
|
}
|
|
3232
4676
|
export function normalizeTelegramInputCustodyMigrationCompletionAuthority(value, expected) {
|
|
3233
4677
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
3234
4678
|
return undefined;
|
|
3235
4679
|
const record = value;
|
|
3236
|
-
const keys = [
|
|
3237
|
-
"
|
|
3238
|
-
"
|
|
3239
|
-
|
|
4680
|
+
const keys = [
|
|
4681
|
+
"version",
|
|
4682
|
+
"status",
|
|
4683
|
+
"authorityId",
|
|
4684
|
+
"startupAuthorityId",
|
|
4685
|
+
"closureOperationId",
|
|
4686
|
+
"profileKey",
|
|
4687
|
+
"recoveryKey",
|
|
4688
|
+
"migrationInventorySha256",
|
|
4689
|
+
"resultingSourceFamily",
|
|
4690
|
+
"authorizedAtMs",
|
|
4691
|
+
];
|
|
4692
|
+
if (Object.keys(record).some((key) => !keys.includes(key)) ||
|
|
4693
|
+
record.version !== 1 ||
|
|
3240
4694
|
(record.status !== "authorized" && record.status !== "revoked") ||
|
|
3241
|
-
record.profileKey !== expected.profileKey ||
|
|
3242
|
-
|
|
3243
|
-
typeof record.
|
|
4695
|
+
record.profileKey !== expected.profileKey ||
|
|
4696
|
+
record.recoveryKey !== expected.recoveryKey ||
|
|
4697
|
+
typeof record.authorityId !== "string" ||
|
|
4698
|
+
!record.authorityId ||
|
|
4699
|
+
record.authorityId.length > 512 ||
|
|
4700
|
+
typeof record.startupAuthorityId !== "string" ||
|
|
4701
|
+
!record.startupAuthorityId ||
|
|
3244
4702
|
record.startupAuthorityId.length > 512 ||
|
|
3245
|
-
typeof record.closureOperationId !== "string" ||
|
|
4703
|
+
typeof record.closureOperationId !== "string" ||
|
|
4704
|
+
!record.closureOperationId ||
|
|
3246
4705
|
record.closureOperationId.length > 512 ||
|
|
3247
4706
|
typeof record.migrationInventorySha256 !== "string" ||
|
|
3248
4707
|
!/^[a-f0-9]{64}$/u.test(record.migrationInventorySha256) ||
|
|
3249
|
-
(record.resultingSourceFamily !== "absent" &&
|
|
3250
|
-
|
|
4708
|
+
(record.resultingSourceFamily !== "absent" &&
|
|
4709
|
+
record.resultingSourceFamily !== "v3") ||
|
|
4710
|
+
!Number.isSafeInteger(record.authorizedAtMs) ||
|
|
4711
|
+
record.authorizedAtMs < 0)
|
|
3251
4712
|
return undefined;
|
|
3252
|
-
return {
|
|
4713
|
+
return {
|
|
4714
|
+
version: 1,
|
|
4715
|
+
status: record.status,
|
|
4716
|
+
authorityId: record.authorityId,
|
|
3253
4717
|
startupAuthorityId: record.startupAuthorityId,
|
|
3254
|
-
closureOperationId: record.closureOperationId,
|
|
4718
|
+
closureOperationId: record.closureOperationId,
|
|
4719
|
+
profileKey: expected.profileKey,
|
|
3255
4720
|
recoveryKey: expected.recoveryKey,
|
|
3256
4721
|
migrationInventorySha256: record.migrationInventorySha256,
|
|
3257
4722
|
resultingSourceFamily: record.resultingSourceFamily,
|
|
3258
|
-
authorizedAtMs: record.authorizedAtMs
|
|
4723
|
+
authorizedAtMs: record.authorizedAtMs,
|
|
4724
|
+
};
|
|
3259
4725
|
}
|
|
3260
4726
|
export function executeTelegramInputCustodyWriterCutover(input) {
|
|
3261
|
-
const matchesAuthority = (candidate) => Boolean(candidate &&
|
|
4727
|
+
const matchesAuthority = (candidate) => Boolean(candidate &&
|
|
4728
|
+
candidate.status === "enforced" &&
|
|
4729
|
+
candidate.version === 1 &&
|
|
3262
4730
|
candidate.profileKey === input.expected.profileKey &&
|
|
3263
4731
|
candidate.recoveryKey === input.expected.recoveryKey &&
|
|
3264
4732
|
candidate.authorityId === input.startupAuthority.authorityId &&
|
|
3265
4733
|
candidate.closureOperationId === input.closure.operationId &&
|
|
3266
|
-
candidate.writerInventorySha256 ===
|
|
4734
|
+
candidate.writerInventorySha256 ===
|
|
4735
|
+
input.startupAuthority.writerInventorySha256 &&
|
|
3267
4736
|
candidate.allowedWriterProtocol === "custody-v3" &&
|
|
3268
4737
|
candidate.authorizedAtMs === input.startupAuthority.authorizedAtMs);
|
|
3269
4738
|
if (input.closure.profileKey !== input.expected.profileKey ||
|
|
@@ -3271,8 +4740,10 @@ export function executeTelegramInputCustodyWriterCutover(input) {
|
|
|
3271
4740
|
!matchesAuthority(input.evidenceStore.read().startupExclusion))
|
|
3272
4741
|
return { kind: "blocked", blocker: "startup-authority" };
|
|
3273
4742
|
const evidence = evaluateTelegramInputCustodyWriterExclusionEvidence({
|
|
3274
|
-
expected: input.expected,
|
|
3275
|
-
|
|
4743
|
+
expected: input.expected,
|
|
4744
|
+
inventory: input.inventory,
|
|
4745
|
+
startupAuthority: input.startupAuthority,
|
|
4746
|
+
getProcessLiveness: input.getProcessLiveness,
|
|
3276
4747
|
});
|
|
3277
4748
|
if (evidence.status !== "excluded")
|
|
3278
4749
|
return { kind: "blocked", blocker: "writer-inventory", evidence };
|
|
@@ -3291,30 +4762,43 @@ export function executeTelegramInputCustodyWriterCutover(input) {
|
|
|
3291
4762
|
if (!matchesAuthority(current.startupExclusion))
|
|
3292
4763
|
return { kind: "blocked", blocker: "startup-authority", evidence };
|
|
3293
4764
|
const existing = current.writerExclusion;
|
|
3294
|
-
if (existing?.status === "excluded" &&
|
|
4765
|
+
if (existing?.status === "excluded" &&
|
|
4766
|
+
existing.profileKey === evidence.profileKey &&
|
|
3295
4767
|
existing.recoveryKey === evidence.recoveryKey &&
|
|
3296
4768
|
existing.startupAuthorityId === evidence.startupAuthorityId &&
|
|
3297
4769
|
existing.closureOperationId === evidence.closureOperationId &&
|
|
3298
4770
|
existing.writerInventorySha256 === evidence.writerInventorySha256)
|
|
3299
|
-
return {
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
4771
|
+
return {
|
|
4772
|
+
kind: "completed",
|
|
4773
|
+
mode: installed.mode,
|
|
4774
|
+
evidence: existing,
|
|
4775
|
+
resumed: installed.resumed,
|
|
4776
|
+
};
|
|
4777
|
+
input.evidenceStore.publish({
|
|
4778
|
+
expectedRevision: current.revision,
|
|
4779
|
+
kind: "writer-exclusion",
|
|
4780
|
+
evidence,
|
|
4781
|
+
});
|
|
3303
4782
|
return { kind: "completed", mode: installed.mode, evidence, resumed: false };
|
|
3304
4783
|
}
|
|
3305
4784
|
function matchesTelegramInputCustodyActivationEvidence(evidence, expected) {
|
|
3306
|
-
return Boolean(evidence &&
|
|
4785
|
+
return Boolean(evidence &&
|
|
4786
|
+
evidence.profileKey === expected.profileKey &&
|
|
3307
4787
|
evidence.recoveryKey === expected.recoveryKey);
|
|
3308
4788
|
}
|
|
3309
4789
|
export function executeTelegramInputCustodyMigrationCompletion(input) {
|
|
3310
|
-
const matchesAuthority = (candidate) => Boolean(candidate &&
|
|
4790
|
+
const matchesAuthority = (candidate) => Boolean(candidate &&
|
|
4791
|
+
candidate.version === 1 &&
|
|
4792
|
+
candidate.status === "authorized" &&
|
|
3311
4793
|
candidate.profileKey === input.expected.profileKey &&
|
|
3312
4794
|
candidate.recoveryKey === input.expected.recoveryKey &&
|
|
3313
4795
|
candidate.authorityId === input.authority.authorityId &&
|
|
3314
4796
|
candidate.startupAuthorityId === input.authority.startupAuthorityId &&
|
|
3315
4797
|
candidate.closureOperationId === input.authority.closureOperationId &&
|
|
3316
|
-
candidate.migrationInventorySha256 ===
|
|
3317
|
-
|
|
4798
|
+
candidate.migrationInventorySha256 ===
|
|
4799
|
+
input.authority.migrationInventorySha256 &&
|
|
4800
|
+
candidate.resultingSourceFamily ===
|
|
4801
|
+
input.authority.resultingSourceFamily &&
|
|
3318
4802
|
candidate.authorizedAtMs === input.authority.authorizedAtMs);
|
|
3319
4803
|
const current = input.evidenceStore.read();
|
|
3320
4804
|
if (!matchesAuthority(current.migrationCompletion))
|
|
@@ -3330,15 +4814,19 @@ export function executeTelegramInputCustodyMigrationCompletion(input) {
|
|
|
3330
4814
|
}
|
|
3331
4815
|
if (source !== input.authority.resultingSourceFamily)
|
|
3332
4816
|
return { kind: "blocked", blocker: "source-drift" };
|
|
3333
|
-
const evidence = {
|
|
3334
|
-
|
|
4817
|
+
const evidence = {
|
|
4818
|
+
version: 1,
|
|
4819
|
+
status: "complete",
|
|
4820
|
+
...input.expected,
|
|
3335
4821
|
migrationAuthorityId: input.authority.authorityId,
|
|
3336
4822
|
startupAuthorityId: input.authority.startupAuthorityId,
|
|
3337
4823
|
closureOperationId: input.authority.closureOperationId,
|
|
3338
4824
|
migrationInventorySha256: input.authority.migrationInventorySha256,
|
|
3339
|
-
resultingSourceFamily: input.authority.resultingSourceFamily
|
|
4825
|
+
resultingSourceFamily: input.authority.resultingSourceFamily,
|
|
4826
|
+
};
|
|
3340
4827
|
const existing = current.migration;
|
|
3341
|
-
if (existing?.status === "complete" &&
|
|
4828
|
+
if (existing?.status === "complete" &&
|
|
4829
|
+
existing.profileKey === evidence.profileKey &&
|
|
3342
4830
|
existing.recoveryKey === evidence.recoveryKey &&
|
|
3343
4831
|
existing.migrationAuthorityId === evidence.migrationAuthorityId &&
|
|
3344
4832
|
existing.startupAuthorityId === evidence.startupAuthorityId &&
|
|
@@ -3346,7 +4834,11 @@ export function executeTelegramInputCustodyMigrationCompletion(input) {
|
|
|
3346
4834
|
existing.migrationInventorySha256 === evidence.migrationInventorySha256 &&
|
|
3347
4835
|
existing.resultingSourceFamily === evidence.resultingSourceFamily)
|
|
3348
4836
|
return { kind: "completed", evidence: existing, resumed: true };
|
|
3349
|
-
input.evidenceStore.publish({
|
|
4837
|
+
input.evidenceStore.publish({
|
|
4838
|
+
expectedRevision: current.revision,
|
|
4839
|
+
kind: "migration",
|
|
4840
|
+
evidence,
|
|
4841
|
+
});
|
|
3350
4842
|
return { kind: "completed", evidence, resumed: false };
|
|
3351
4843
|
}
|
|
3352
4844
|
export function createTelegramInputCustodyProvenReadinessResolver(deps) {
|
|
@@ -3363,26 +4855,30 @@ export function createTelegramInputCustodyProvenReadinessResolver(deps) {
|
|
|
3363
4855
|
const expected = deps.expectedIdentity();
|
|
3364
4856
|
const authority = deps.readMigrationCompletionAuthority();
|
|
3365
4857
|
return matchesTelegramInputCustodyActivationEvidence(authority, expected) &&
|
|
3366
|
-
authority?.status === "authorized" &&
|
|
3367
|
-
|
|
4858
|
+
authority?.status === "authorized" &&
|
|
4859
|
+
authority.resultingSourceFamily === source
|
|
4860
|
+
? source
|
|
4861
|
+
: "ambiguous";
|
|
3368
4862
|
},
|
|
3369
4863
|
areLegacyWritersExcluded() {
|
|
3370
4864
|
const expected = deps.expectedIdentity();
|
|
3371
4865
|
const evidence = deps.readWriterExclusionEvidence();
|
|
3372
4866
|
const authority = deps.readStartupExclusionAuthority();
|
|
3373
4867
|
const mode = deps.readWriterProtocolMode();
|
|
3374
|
-
return matchesTelegramInputCustodyActivationEvidence(evidence, expected) &&
|
|
4868
|
+
return (matchesTelegramInputCustodyActivationEvidence(evidence, expected) &&
|
|
3375
4869
|
matchesTelegramInputCustodyActivationEvidence(authority, expected) &&
|
|
3376
4870
|
matchesTelegramInputCustodyActivationEvidence(mode, expected) &&
|
|
3377
|
-
evidence?.version === 1 &&
|
|
3378
|
-
|
|
4871
|
+
evidence?.version === 1 &&
|
|
4872
|
+
evidence.status === "excluded" &&
|
|
4873
|
+
authority?.version === 1 &&
|
|
4874
|
+
authority.status === "enforced" &&
|
|
3379
4875
|
mode?.protocol === "custody-v3" &&
|
|
3380
4876
|
evidence.startupAuthorityId === authority.authorityId &&
|
|
3381
4877
|
evidence.startupAuthorityId === mode.startupAuthorityId &&
|
|
3382
4878
|
evidence.closureOperationId === authority.closureOperationId &&
|
|
3383
4879
|
evidence.closureOperationId === mode.closureOperationId &&
|
|
3384
4880
|
evidence.writerInventorySha256 === authority.writerInventorySha256 &&
|
|
3385
|
-
evidence.writerInventorySha256 === mode.writerInventorySha256;
|
|
4881
|
+
evidence.writerInventorySha256 === mode.writerInventorySha256);
|
|
3386
4882
|
},
|
|
3387
4883
|
isHistoricalMigrationComplete() {
|
|
3388
4884
|
const expected = deps.expectedIdentity();
|
|
@@ -3397,10 +4893,12 @@ export function createTelegramInputCustodyProvenReadinessResolver(deps) {
|
|
|
3397
4893
|
catch {
|
|
3398
4894
|
source = "ambiguous";
|
|
3399
4895
|
}
|
|
3400
|
-
return matchesTelegramInputCustodyActivationEvidence(evidence, expected) &&
|
|
4896
|
+
return (matchesTelegramInputCustodyActivationEvidence(evidence, expected) &&
|
|
3401
4897
|
matchesTelegramInputCustodyActivationEvidence(authority, expected) &&
|
|
3402
|
-
evidence?.version === 1 &&
|
|
3403
|
-
|
|
4898
|
+
evidence?.version === 1 &&
|
|
4899
|
+
evidence.status === "complete" &&
|
|
4900
|
+
authority?.version === 1 &&
|
|
4901
|
+
authority.status === "authorized" &&
|
|
3404
4902
|
authority.resultingSourceFamily === source &&
|
|
3405
4903
|
evidence.migrationAuthorityId === authority.authorityId &&
|
|
3406
4904
|
evidence.startupAuthorityId === authority.startupAuthorityId &&
|
|
@@ -3408,8 +4906,9 @@ export function createTelegramInputCustodyProvenReadinessResolver(deps) {
|
|
|
3408
4906
|
evidence.startupAuthorityId === mode?.startupAuthorityId &&
|
|
3409
4907
|
evidence.closureOperationId === authority.closureOperationId &&
|
|
3410
4908
|
evidence.closureOperationId === mode?.closureOperationId &&
|
|
3411
|
-
evidence.migrationInventorySha256 ===
|
|
3412
|
-
|
|
4909
|
+
evidence.migrationInventorySha256 ===
|
|
4910
|
+
authority.migrationInventorySha256 &&
|
|
4911
|
+
evidence.resultingSourceFamily === authority.resultingSourceFamily);
|
|
3413
4912
|
},
|
|
3414
4913
|
listPeerReadiness: deps.listPeerReadiness,
|
|
3415
4914
|
});
|
|
@@ -3423,14 +4922,18 @@ export function createTelegramInputCustodyActivationReadinessResolver(deps) {
|
|
|
3423
4922
|
try {
|
|
3424
4923
|
legacyWritersExcluded = deps.areLegacyWritersExcluded();
|
|
3425
4924
|
}
|
|
3426
|
-
catch {
|
|
4925
|
+
catch {
|
|
4926
|
+
/* blocked */
|
|
4927
|
+
}
|
|
3427
4928
|
if (!legacyWritersExcluded)
|
|
3428
4929
|
return { enabled: false, blocker: "legacy-writers-present" };
|
|
3429
4930
|
let historicalMigrationComplete = false;
|
|
3430
4931
|
try {
|
|
3431
4932
|
historicalMigrationComplete = deps.isHistoricalMigrationComplete();
|
|
3432
4933
|
}
|
|
3433
|
-
catch {
|
|
4934
|
+
catch {
|
|
4935
|
+
/* blocked */
|
|
4936
|
+
}
|
|
3434
4937
|
if (!historicalMigrationComplete)
|
|
3435
4938
|
return { enabled: false, blocker: "migration-incomplete" };
|
|
3436
4939
|
let sourceStatus;
|
|
@@ -3449,8 +4952,13 @@ export function createTelegramInputCustodyActivationReadinessResolver(deps) {
|
|
|
3449
4952
|
catch {
|
|
3450
4953
|
peerReadiness = ["unknown"];
|
|
3451
4954
|
}
|
|
3452
|
-
return evaluateTelegramInputCustodyActivationReadiness({
|
|
3453
|
-
|
|
4955
|
+
return evaluateTelegramInputCustodyActivationReadiness({
|
|
4956
|
+
requested,
|
|
4957
|
+
sourceStatus,
|
|
4958
|
+
legacyWritersExcluded,
|
|
4959
|
+
historicalMigrationComplete,
|
|
4960
|
+
peerReadiness,
|
|
4961
|
+
});
|
|
3454
4962
|
};
|
|
3455
4963
|
}
|
|
3456
4964
|
export function createTelegramInputCustodyLifecycleBindingResolver(deps) {
|
|
@@ -3461,21 +4969,33 @@ export function createTelegramInputCustodyLifecycleBindingResolver(deps) {
|
|
|
3461
4969
|
return undefined;
|
|
3462
4970
|
const source = deps.resolveInputJournal();
|
|
3463
4971
|
const recipientBindingKey = deps.getRecipientBindingKey()?.trim();
|
|
3464
|
-
if (!source ||
|
|
4972
|
+
if (!source ||
|
|
4973
|
+
!source.runtimeKey ||
|
|
4974
|
+
!source.recoveryKey ||
|
|
4975
|
+
!recipientBindingKey)
|
|
3465
4976
|
return undefined;
|
|
3466
4977
|
let port = ports.get(source.journal);
|
|
3467
4978
|
if (!port) {
|
|
3468
4979
|
port = createTelegramInputCustodyWorkerJournalPort(source.journal);
|
|
3469
4980
|
ports.set(source.journal, port);
|
|
3470
4981
|
}
|
|
3471
|
-
return {
|
|
4982
|
+
return {
|
|
4983
|
+
runtimeKey: JSON.stringify({
|
|
4984
|
+
source: source.runtimeKey,
|
|
4985
|
+
recipientBindingKey,
|
|
4986
|
+
}),
|
|
3472
4987
|
recoveryKey: source.recoveryKey,
|
|
3473
|
-
recipientBindingKey,
|
|
4988
|
+
recipientBindingKey,
|
|
4989
|
+
journal: {
|
|
4990
|
+
...port,
|
|
4991
|
+
appendBatch: source.journal.appendBatch,
|
|
3474
4992
|
discardQueued: source.journal.discardQueued,
|
|
3475
4993
|
recoverDeadQueueOwner: source.journal.recoverDeadQueueOwner,
|
|
3476
4994
|
offerQueuedHandoff: source.journal.offerQueuedHandoff,
|
|
3477
4995
|
acceptQueuedHandoff: source.journal.acceptQueuedHandoff,
|
|
3478
|
-
cancelQueuedHandoff: source.journal.cancelQueuedHandoff
|
|
4996
|
+
cancelQueuedHandoff: source.journal.cancelQueuedHandoff,
|
|
4997
|
+
},
|
|
4998
|
+
};
|
|
3479
4999
|
};
|
|
3480
5000
|
}
|
|
3481
5001
|
export function createTelegramCustodiedExecutionSession(input) {
|
|
@@ -3485,10 +5005,12 @@ export function createTelegramCustodiedExecutionSession(input) {
|
|
|
3485
5005
|
const receipt = receipts.get(updateId);
|
|
3486
5006
|
if (!receipt) {
|
|
3487
5007
|
const duplicate = settledQueues.get(updateId);
|
|
3488
|
-
if (duplicate &&
|
|
5008
|
+
if (duplicate &&
|
|
5009
|
+
outcome.kind === "queued" &&
|
|
3489
5010
|
duplicate.queueReceipt.queueKind === outcome.queueKind &&
|
|
3490
5011
|
duplicate.queueReceipt.receiptId === outcome.receiptId &&
|
|
3491
|
-
duplicate.queueReceipt.sourceUpdateIds.length ===
|
|
5012
|
+
duplicate.queueReceipt.sourceUpdateIds.length ===
|
|
5013
|
+
outcome.sourceUpdateIds.length &&
|
|
3492
5014
|
duplicate.queueReceipt.sourceUpdateIds.every((id, index) => id === outcome.sourceUpdateIds[index])) {
|
|
3493
5015
|
settledQueues.delete(updateId);
|
|
3494
5016
|
return duplicate;
|
|
@@ -3500,12 +5022,18 @@ export function createTelegramCustodiedExecutionSession(input) {
|
|
|
3500
5022
|
if (outcome.kind === "queued") {
|
|
3501
5023
|
if (!outcome.sourceUpdateIds.includes(updateId))
|
|
3502
5024
|
throw new TelegramUpdateAdmissionOutcomeError("Telegram grouped queue custody omitted the settling input.");
|
|
3503
|
-
const grouped = outcome.sourceUpdateIds.map(sourceUpdateId => receipts.get(sourceUpdateId));
|
|
3504
|
-
if (grouped.some(candidate => !candidate))
|
|
5025
|
+
const grouped = outcome.sourceUpdateIds.map((sourceUpdateId) => receipts.get(sourceUpdateId));
|
|
5026
|
+
if (grouped.some((candidate) => !candidate))
|
|
3505
5027
|
throw new TelegramUpdateAdmissionOutcomeError("Telegram grouped queue custody omitted an exact running source receipt.");
|
|
3506
|
-
const queued = input.journal.queueInputs({
|
|
3507
|
-
|
|
3508
|
-
|
|
5028
|
+
const queued = input.journal.queueInputs({
|
|
5029
|
+
queueKind: outcome.queueKind,
|
|
5030
|
+
receiptId: outcome.receiptId,
|
|
5031
|
+
receipts: grouped,
|
|
5032
|
+
});
|
|
5033
|
+
const result = {
|
|
5034
|
+
status: "queued",
|
|
5035
|
+
queueReceipt: queued.queueReceipt,
|
|
5036
|
+
};
|
|
3509
5037
|
for (const sourceUpdateId of outcome.sourceUpdateIds) {
|
|
3510
5038
|
receipts.delete(sourceUpdateId);
|
|
3511
5039
|
if (sourceUpdateId !== updateId)
|
|
@@ -3520,8 +5048,11 @@ export function createTelegramCustodiedExecutionSession(input) {
|
|
|
3520
5048
|
return {
|
|
3521
5049
|
settle,
|
|
3522
5050
|
async execute(update, handler) {
|
|
3523
|
-
const acquired = input.journal.acquireInput({
|
|
3524
|
-
|
|
5051
|
+
const acquired = input.journal.acquireInput({
|
|
5052
|
+
updateId: update.update_id,
|
|
5053
|
+
recipientBindingKey: input.recipientBindingKey,
|
|
5054
|
+
executionUpdate: update,
|
|
5055
|
+
});
|
|
3525
5056
|
const started = input.journal.startInput(acquired.receipt);
|
|
3526
5057
|
if (!started.started)
|
|
3527
5058
|
return { status: "outcome-unknown", receipt: acquired.receipt };
|
|
@@ -3549,6 +5080,10 @@ export function createTelegramUpdateAdmissionHandle(deps) {
|
|
|
3549
5080
|
if (deps.onLateOutcome && !deps.onLateOutcomeError) {
|
|
3550
5081
|
throw new Error("Telegram late admission outcomes require a diagnostic error sink.");
|
|
3551
5082
|
}
|
|
5083
|
+
return createTelegramUpdateSourceAdmissionHandle(deps);
|
|
5084
|
+
}
|
|
5085
|
+
/** Same native source/fence/late-ACK owner, with an internal binding-only callback instead of registry/ordinary dispatch. */
|
|
5086
|
+
function createTelegramUpdateSourceAdmissionHandle(deps, capture) {
|
|
3552
5087
|
const registry = deps.registry ?? getOrCreateUpdateHandlerRegistry();
|
|
3553
5088
|
let nextExecutionGeneration = 0;
|
|
3554
5089
|
return async (update, ctx, signal) => {
|
|
@@ -3565,8 +5100,15 @@ export function createTelegramUpdateAdmissionHandle(deps) {
|
|
|
3565
5100
|
generation,
|
|
3566
5101
|
updateId: update.update_id,
|
|
3567
5102
|
signal,
|
|
3568
|
-
isCurrent: () => !signal.aborted &&
|
|
3569
|
-
|
|
5103
|
+
isCurrent: () => !signal.aborted &&
|
|
5104
|
+
!suspended &&
|
|
5105
|
+
((!routingLifetime && !routingClockUnknown) ||
|
|
5106
|
+
immediate ||
|
|
5107
|
+
settlementReported ||
|
|
5108
|
+
deps.isRoutingInputCurrent?.({
|
|
5109
|
+
updateId: update.update_id,
|
|
5110
|
+
signal,
|
|
5111
|
+
}) !== false),
|
|
3570
5112
|
assertCurrent() {
|
|
3571
5113
|
if (!execution.isCurrent()) {
|
|
3572
5114
|
throw signal.reason ?? new DOMException("Aborted", "AbortError");
|
|
@@ -3574,18 +5116,25 @@ export function createTelegramUpdateAdmissionHandle(deps) {
|
|
|
3574
5116
|
},
|
|
3575
5117
|
};
|
|
3576
5118
|
execution.assertCurrent();
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
5119
|
+
if (!capture) {
|
|
5120
|
+
const verdict = await registry.dispatch(update, execution);
|
|
5121
|
+
execution.assertCurrent();
|
|
5122
|
+
if (verdict === "consume")
|
|
5123
|
+
return { kind: "complete" };
|
|
5124
|
+
}
|
|
3581
5125
|
let immediate = true;
|
|
3582
5126
|
let settlementReported = false;
|
|
3583
5127
|
let outcome;
|
|
5128
|
+
let completionAttempted = false;
|
|
5129
|
+
let completedSource;
|
|
3584
5130
|
const boundUpdate = bindTelegramUpdateExecutionFence(bindTelegramUpdateAdmissionSource(update, (next) => {
|
|
3585
5131
|
if (!execution.isCurrent())
|
|
3586
5132
|
return;
|
|
3587
5133
|
if (next.kind === "deferred" && next.routingReview) {
|
|
3588
|
-
if (!immediate ||
|
|
5134
|
+
if (!immediate ||
|
|
5135
|
+
settlementReported ||
|
|
5136
|
+
routingClaims > 0 ||
|
|
5137
|
+
!deps.isHistoricalSource?.({ updateId: update.update_id, signal })) {
|
|
3589
5138
|
throw new TelegramUpdateAdmissionOutcomeError("Historical review cannot override accepted or selected work.");
|
|
3590
5139
|
}
|
|
3591
5140
|
suspended = true;
|
|
@@ -3599,15 +5148,34 @@ export function createTelegramUpdateAdmissionHandle(deps) {
|
|
|
3599
5148
|
if (!deps.onLateOutcome) {
|
|
3600
5149
|
throw new TelegramUpdateAdmissionOutcomeError(`Telegram update ${update.update_id} reported a late outcome without an owner.`);
|
|
3601
5150
|
}
|
|
5151
|
+
const expectedSource = next.kind === "complete" &&
|
|
5152
|
+
next.expectedSource &&
|
|
5153
|
+
!completionAttempted
|
|
5154
|
+
? { ...next.expectedSource }
|
|
5155
|
+
: undefined;
|
|
5156
|
+
if (expectedSource)
|
|
5157
|
+
completionAttempted = true;
|
|
5158
|
+
const reported = expectedSource
|
|
5159
|
+
? { ...next, expectedSource: { ...expectedSource } }
|
|
5160
|
+
: next;
|
|
3602
5161
|
void Promise.resolve()
|
|
3603
|
-
.then(() => {
|
|
5162
|
+
.then(async () => {
|
|
3604
5163
|
if (!execution.isCurrent())
|
|
3605
5164
|
return;
|
|
3606
|
-
|
|
5165
|
+
const completion = await deps.onLateOutcome(reported, {
|
|
3607
5166
|
updateId: update.update_id,
|
|
3608
5167
|
ctx,
|
|
3609
5168
|
signal,
|
|
3610
5169
|
});
|
|
5170
|
+
if (expectedSource &&
|
|
5171
|
+
completion &&
|
|
5172
|
+
execution.isCurrent() &&
|
|
5173
|
+
completion.isCurrent() &&
|
|
5174
|
+
isDeepStrictEqual(completion.source, expectedSource))
|
|
5175
|
+
completedSource = {
|
|
5176
|
+
...completion,
|
|
5177
|
+
source: { ...expectedSource },
|
|
5178
|
+
};
|
|
3611
5179
|
})
|
|
3612
5180
|
.catch((error) => {
|
|
3613
5181
|
try {
|
|
@@ -3617,9 +5185,16 @@ export function createTelegramUpdateAdmissionHandle(deps) {
|
|
|
3617
5185
|
// Diagnostic sinks must not create an unhandled late Promise.
|
|
3618
5186
|
}
|
|
3619
5187
|
});
|
|
3620
|
-
}, {
|
|
3621
|
-
|
|
3622
|
-
|
|
5188
|
+
}, {
|
|
5189
|
+
abandon: (authority) => {
|
|
5190
|
+
const isCurrent = () => !signal.aborted &&
|
|
5191
|
+
routingClaims === 0 &&
|
|
5192
|
+
!settlementReported &&
|
|
5193
|
+
authority.isCurrent();
|
|
5194
|
+
if (immediate ||
|
|
5195
|
+
outcome?.kind !== "deferred" ||
|
|
5196
|
+
!deps.abandonDeferred ||
|
|
5197
|
+
!isCurrent())
|
|
3623
5198
|
return undefined;
|
|
3624
5199
|
if (abandoned)
|
|
3625
5200
|
return { ...abandoned, duplicate: true };
|
|
@@ -3627,23 +5202,91 @@ export function createTelegramUpdateAdmissionHandle(deps) {
|
|
|
3627
5202
|
suspended = true;
|
|
3628
5203
|
// Failed/unknown publication keeps dispatch suspended for exact cancellation retry.
|
|
3629
5204
|
// An ineligible request that attempted no publication remains inert.
|
|
3630
|
-
const result = deps.abandonDeferred({
|
|
5205
|
+
const result = deps.abandonDeferred({
|
|
5206
|
+
...authority,
|
|
5207
|
+
updateId: update.update_id,
|
|
5208
|
+
signal,
|
|
5209
|
+
isCurrent,
|
|
5210
|
+
});
|
|
3631
5211
|
if (result)
|
|
3632
5212
|
abandoned = result;
|
|
3633
5213
|
else
|
|
3634
5214
|
suspended = wasSuspended;
|
|
3635
5215
|
return result;
|
|
3636
|
-
},
|
|
3637
|
-
|
|
3638
|
-
|
|
5216
|
+
},
|
|
5217
|
+
inspectAbandoning: (request) => execution.isCurrent()
|
|
5218
|
+
? deps.inspectAbandoning?.({ ...request, signal })
|
|
5219
|
+
: undefined,
|
|
5220
|
+
inspectHistorical: (request) => execution.isCurrent()
|
|
5221
|
+
? deps.inspectHistorical?.({ ...request, signal })
|
|
5222
|
+
: undefined,
|
|
3639
5223
|
inspectSource: () => execution.isCurrent() && !immediate && !settlementReported
|
|
3640
|
-
? deps.inspectDeferredSource?.({
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
5224
|
+
? deps.inspectDeferredSource?.({
|
|
5225
|
+
updateId: update.update_id,
|
|
5226
|
+
signal,
|
|
5227
|
+
})
|
|
5228
|
+
: undefined,
|
|
5229
|
+
inspectSourceSnapshot: () => execution.isCurrent() && !immediate && !settlementReported
|
|
5230
|
+
? deps.inspectDeferredSourceSnapshot?.({
|
|
5231
|
+
updateId: update.update_id,
|
|
5232
|
+
signal,
|
|
5233
|
+
})
|
|
5234
|
+
: undefined,
|
|
5235
|
+
inspectCompletion: () => execution.isCurrent() && completedSource?.isCurrent()
|
|
5236
|
+
? { ...completedSource.source }
|
|
5237
|
+
: undefined,
|
|
5238
|
+
isSourceUnsettled: () => execution.isCurrent() &&
|
|
5239
|
+
!immediate &&
|
|
5240
|
+
!settlementReported &&
|
|
5241
|
+
!suspended,
|
|
5242
|
+
prepareQueueAdmission: () => execution.isCurrent() &&
|
|
5243
|
+
!immediate &&
|
|
5244
|
+
!settlementReported &&
|
|
5245
|
+
!suspended
|
|
5246
|
+
? deps.prepareDeferredQueueAdmission?.({
|
|
5247
|
+
updateId: update.update_id,
|
|
5248
|
+
signal,
|
|
5249
|
+
})
|
|
5250
|
+
: undefined,
|
|
5251
|
+
prepareSourceCompletion: () => execution.isCurrent() &&
|
|
5252
|
+
!immediate &&
|
|
5253
|
+
!settlementReported &&
|
|
5254
|
+
!suspended
|
|
5255
|
+
? deps.prepareDeferredSourceCompletion?.({
|
|
5256
|
+
updateId: update.update_id,
|
|
5257
|
+
signal,
|
|
5258
|
+
})
|
|
5259
|
+
: undefined,
|
|
5260
|
+
prepareLiveInput: (sourceUpdateIds, isCurrent) => execution.isCurrent() &&
|
|
5261
|
+
!immediate &&
|
|
5262
|
+
!settlementReported &&
|
|
5263
|
+
!suspended
|
|
5264
|
+
? deps.prepareDeferredLiveInput?.({
|
|
5265
|
+
updateId: update.update_id,
|
|
5266
|
+
sourceUpdateIds,
|
|
5267
|
+
signal,
|
|
5268
|
+
isCurrent: () => execution.isCurrent() && isCurrent(),
|
|
5269
|
+
})
|
|
5270
|
+
: undefined,
|
|
5271
|
+
isHistorical: (matchesOriginal) => !signal.aborted &&
|
|
5272
|
+
deps.isHistoricalSource?.({
|
|
5273
|
+
updateId: update.update_id,
|
|
5274
|
+
signal,
|
|
5275
|
+
...(matchesOriginal ? { matchesOriginal } : {}),
|
|
5276
|
+
}) === true,
|
|
5277
|
+
isHistoricalReviewHeld: () => !signal.aborted &&
|
|
5278
|
+
suspended &&
|
|
5279
|
+
outcome?.kind === "deferred" &&
|
|
5280
|
+
outcome.routingReview === true,
|
|
5281
|
+
supportsAbandonment: (journalBindingKey) => !signal.aborted &&
|
|
5282
|
+
!settlementReported &&
|
|
5283
|
+
deps.supportsDeferredAbandonment?.({
|
|
5284
|
+
updateId: update.update_id,
|
|
5285
|
+
signal,
|
|
5286
|
+
journalBindingKey,
|
|
5287
|
+
}) === true,
|
|
5288
|
+
getRoutingInput: () => deps.getRoutingInput?.({ updateId: update.update_id, signal }),
|
|
5289
|
+
armRoutingInput: (operatorUserId, sourceUpdateIds, chooser) => {
|
|
3647
5290
|
execution.assertCurrent();
|
|
3648
5291
|
if (settlementReported || routingClaims || suspended)
|
|
3649
5292
|
return undefined;
|
|
@@ -3653,7 +5296,13 @@ export function createTelegramUpdateAdmissionHandle(deps) {
|
|
|
3653
5296
|
if (routingSourceIds && !isDeepStrictEqual(ids, routingSourceIds))
|
|
3654
5297
|
throw new TelegramUpdateAdmissionOutcomeError("Telegram routing input source membership changed.");
|
|
3655
5298
|
try {
|
|
3656
|
-
routingLifetime = deps.armRoutingInput?.({
|
|
5299
|
+
routingLifetime = deps.armRoutingInput?.({
|
|
5300
|
+
updateId: update.update_id,
|
|
5301
|
+
signal,
|
|
5302
|
+
operatorUserId,
|
|
5303
|
+
sourceUpdateIds: ids,
|
|
5304
|
+
...(chooser ? { chooser } : {}),
|
|
5305
|
+
});
|
|
3657
5306
|
}
|
|
3658
5307
|
catch (error) {
|
|
3659
5308
|
routingClockUnknown = true;
|
|
@@ -3673,8 +5322,13 @@ export function createTelegramUpdateAdmissionHandle(deps) {
|
|
|
3673
5322
|
if (!isDeepStrictEqual(sourceUpdateIds ?? [update.update_id], routingSourceIds))
|
|
3674
5323
|
throw new TelegramUpdateAdmissionOutcomeError("Telegram routing input selection changed its source membership.");
|
|
3675
5324
|
routingSelectionAttempted = true;
|
|
3676
|
-
routingSelectionConfirmed =
|
|
3677
|
-
|
|
5325
|
+
routingSelectionConfirmed =
|
|
5326
|
+
deps.selectRoutingInput?.({
|
|
5327
|
+
updateId: update.update_id,
|
|
5328
|
+
signal,
|
|
5329
|
+
operatorUserId: routingLifetime.operatorUserId,
|
|
5330
|
+
sourceUpdateIds: routingSourceIds,
|
|
5331
|
+
}) === true;
|
|
3678
5332
|
if (!routingSelectionConfirmed)
|
|
3679
5333
|
throw new TelegramUpdateAdmissionOutcomeError("Telegram routing input choice is expired or unavailable.");
|
|
3680
5334
|
}
|
|
@@ -3686,11 +5340,19 @@ export function createTelegramUpdateAdmissionHandle(deps) {
|
|
|
3686
5340
|
released = true;
|
|
3687
5341
|
routingClaims -= 1;
|
|
3688
5342
|
};
|
|
3689
|
-
}
|
|
5343
|
+
},
|
|
5344
|
+
}), execution);
|
|
3690
5345
|
try {
|
|
3691
5346
|
execution.assertCurrent();
|
|
3692
|
-
|
|
3693
|
-
|
|
5347
|
+
if (capture) {
|
|
5348
|
+
capture(boundUpdate);
|
|
5349
|
+
reportTelegramUpdateDeferred(boundUpdate.message);
|
|
5350
|
+
}
|
|
5351
|
+
else
|
|
5352
|
+
await deps.defaultHandle(boundUpdate, ctx, execution);
|
|
5353
|
+
if (outcome?.kind !== "deferred" ||
|
|
5354
|
+
!outcome.routingReview ||
|
|
5355
|
+
signal.aborted)
|
|
3694
5356
|
execution.assertCurrent();
|
|
3695
5357
|
}
|
|
3696
5358
|
finally {
|
|
@@ -3700,47 +5362,63 @@ export function createTelegramUpdateAdmissionHandle(deps) {
|
|
|
3700
5362
|
};
|
|
3701
5363
|
}
|
|
3702
5364
|
export function createTelegramCustodiedUpdateAdmissionHandle(deps) {
|
|
3703
|
-
const session = createTelegramCustodiedExecutionSession({
|
|
3704
|
-
|
|
5365
|
+
const session = createTelegramCustodiedExecutionSession({
|
|
5366
|
+
journal: deps.journal,
|
|
5367
|
+
recipientBindingKey: deps.recipientBindingKey,
|
|
5368
|
+
});
|
|
3705
5369
|
const admission = createTelegramUpdateAdmissionHandle({
|
|
3706
5370
|
defaultHandle: deps.defaultHandle,
|
|
3707
5371
|
registry: deps.registry,
|
|
3708
5372
|
async onLateOutcome(outcome, details) {
|
|
3709
5373
|
const result = await session.settle(details.updateId, outcome);
|
|
3710
|
-
deps.onCustodiedLateSettlement?.(result, {
|
|
5374
|
+
deps.onCustodiedLateSettlement?.(result, {
|
|
5375
|
+
updateId: details.updateId,
|
|
5376
|
+
signal: details.signal,
|
|
5377
|
+
});
|
|
3711
5378
|
},
|
|
3712
5379
|
onLateOutcomeError: deps.onLateOutcomeError,
|
|
3713
5380
|
});
|
|
3714
|
-
return (update, ctx, signal) => session.execute(update, started => admission(started, ctx, signal));
|
|
5381
|
+
return (update, ctx, signal) => session.execute(update, (started) => admission(started, ctx, signal));
|
|
3715
5382
|
}
|
|
3716
5383
|
// A dispatched prompt can remain in memory until agent_start. Remember only acknowledged
|
|
3717
5384
|
// removal of these exact receipt objects for later local discard, never for dispatch/replay.
|
|
3718
5385
|
const acknowledgedQueueReceiptCompletions = new WeakMap();
|
|
3719
5386
|
const isQueueReceiptCompletionAcknowledged = (receipt) => {
|
|
3720
5387
|
const acknowledged = acknowledgedQueueReceiptCompletions.get(receipt);
|
|
3721
|
-
return acknowledged !== undefined &&
|
|
5388
|
+
return (acknowledged !== undefined &&
|
|
5389
|
+
areTelegramQueueAdmissionReceiptsEqual(acknowledged, receipt));
|
|
3722
5390
|
};
|
|
3723
5391
|
export function createTelegramQueueAdmissionSettlementMuxRuntime(runtimes) {
|
|
3724
5392
|
// Plan the whole request before mutation; unready is not an acknowledgement of completion.
|
|
3725
5393
|
const complete = (items, ctx, kind) => {
|
|
3726
5394
|
const plan = new Map();
|
|
3727
|
-
const reason = kind === "prompt"
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
5395
|
+
const reason = kind === "prompt"
|
|
5396
|
+
? "prompt-handoff"
|
|
5397
|
+
: kind === "control"
|
|
5398
|
+
? "control-settlement"
|
|
5399
|
+
: "discard";
|
|
5400
|
+
const settlementOwner = (runtime, receipt) => runtime.getQueueReceiptSettlementOwner
|
|
5401
|
+
? runtime.getQueueReceiptSettlementOwner(receipt, ctx, reason)
|
|
5402
|
+
: runtime.isItemReady({ admissionReceipts: [receipt] })
|
|
5403
|
+
? runtime.getQueueReceiptOwner(receipt)
|
|
5404
|
+
: undefined;
|
|
5405
|
+
for (const receipt of items.flatMap((item) => item.admissionReceipts ?? [])) {
|
|
3731
5406
|
if (isQueueReceiptCompletionAcknowledged(receipt))
|
|
3732
5407
|
continue;
|
|
3733
|
-
const candidates = runtimes.filter(runtime => runtime.getQueueReceiptSettlementOwner
|
|
5408
|
+
const candidates = runtimes.filter((runtime) => runtime.getQueueReceiptSettlementOwner
|
|
5409
|
+
? !!settlementOwner(runtime, receipt)
|
|
3734
5410
|
: runtime.isItemReady({ admissionReceipts: [receipt] }));
|
|
3735
5411
|
const selected = candidates[0];
|
|
3736
5412
|
if (!selected)
|
|
3737
5413
|
return false;
|
|
3738
5414
|
if (candidates.length > 1) {
|
|
3739
5415
|
const owner = settlementOwner(selected, receipt);
|
|
3740
|
-
if (!owner ||
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
5416
|
+
if (!owner ||
|
|
5417
|
+
candidates.some((runtime) => {
|
|
5418
|
+
const candidate = settlementOwner(runtime, receipt);
|
|
5419
|
+
return (!candidate ||
|
|
5420
|
+
!areTelegramUpdateJournalQueueOwnersEqual(owner, candidate));
|
|
5421
|
+
}))
|
|
3744
5422
|
return false;
|
|
3745
5423
|
}
|
|
3746
5424
|
const receipts = plan.get(selected) ?? [];
|
|
@@ -3749,8 +5427,10 @@ export function createTelegramQueueAdmissionSettlementMuxRuntime(runtimes) {
|
|
|
3749
5427
|
}
|
|
3750
5428
|
for (const [runtime, receipts] of plan) {
|
|
3751
5429
|
const item = { admissionReceipts: receipts };
|
|
3752
|
-
const completed = kind === "discard"
|
|
3753
|
-
|
|
5430
|
+
const completed = kind === "discard"
|
|
5431
|
+
? runtime.onItemsDiscarded([item], ctx)
|
|
5432
|
+
: kind === "prompt"
|
|
5433
|
+
? runtime.onPromptHandedOff(item, ctx)
|
|
3754
5434
|
: runtime.onControlSettled(item, ctx);
|
|
3755
5435
|
if (completed !== true)
|
|
3756
5436
|
return false;
|
|
@@ -3787,12 +5467,17 @@ export function createTelegramQueueAdmissionSettlementRuntime(worker) {
|
|
|
3787
5467
|
receipts.push(receipt);
|
|
3788
5468
|
}
|
|
3789
5469
|
}
|
|
3790
|
-
return receipts.length === 0 ||
|
|
5470
|
+
return (receipts.length === 0 ||
|
|
5471
|
+
worker.completeQueueReceipts({ receipts, ctx, reason }));
|
|
3791
5472
|
};
|
|
3792
5473
|
return {
|
|
3793
5474
|
isItemReady: (item) => (item.admissionReceipts ?? []).every(worker.isQueueReceiptCommitted),
|
|
3794
5475
|
getQueueReceiptOwner: worker.getQueueReceiptOwner,
|
|
3795
|
-
...(worker.getQueueReceiptSettlementOwner
|
|
5476
|
+
...(worker.getQueueReceiptSettlementOwner
|
|
5477
|
+
? {
|
|
5478
|
+
getQueueReceiptSettlementOwner: worker.getQueueReceiptSettlementOwner,
|
|
5479
|
+
}
|
|
5480
|
+
: {}),
|
|
3796
5481
|
onPromptHandedOff: (item, ctx) => complete([item], ctx, "prompt-handoff"),
|
|
3797
5482
|
onControlSettled: (item, ctx) => complete([item], ctx, "control-settlement"),
|
|
3798
5483
|
onItemsDiscarded: (items, ctx) => complete(items, ctx, "discard"),
|
|
@@ -3966,8 +5651,7 @@ export function createTelegramQueueHandoffReconciliationRuntimeAssembly(deps) {
|
|
|
3966
5651
|
createRecipientJournalBindingKey(recipient) {
|
|
3967
5652
|
if (!recipient.profileKey || !recipient.sessionId)
|
|
3968
5653
|
return undefined;
|
|
3969
|
-
return deps.createRecipientJournalResolver(recipient.profileKey, recipient.sessionId)()
|
|
3970
|
-
?.recoveryKey;
|
|
5654
|
+
return deps.createRecipientJournalResolver(recipient.profileKey, recipient.sessionId)()?.recoveryKey;
|
|
3971
5655
|
},
|
|
3972
5656
|
getQueuedItems: deps.queueStore.getQueuedItems,
|
|
3973
5657
|
getReceiptOwner(receipt) {
|
|
@@ -4021,7 +5705,9 @@ export function createTelegramQueueHandoffReconciler(deps) {
|
|
|
4021
5705
|
(followerRegistered && deps.canHandoffWithLeader?.() === false)) {
|
|
4022
5706
|
return;
|
|
4023
5707
|
}
|
|
4024
|
-
const followers = deps
|
|
5708
|
+
const followers = deps
|
|
5709
|
+
.listFollowers()
|
|
5710
|
+
.filter((follower) => follower.instanceId !== deps.donorInstanceId);
|
|
4025
5711
|
if (followers.length === 0)
|
|
4026
5712
|
return;
|
|
4027
5713
|
for (const item of [...deps.getQueuedItems()]) {
|
|
@@ -4067,7 +5753,12 @@ export function createTelegramQueueHandoffReconciler(deps) {
|
|
|
4067
5753
|
stageRemote: async ({ payload: sourcePayload }) => {
|
|
4068
5754
|
const payload = {
|
|
4069
5755
|
...sourcePayload,
|
|
4070
|
-
admissionReceipts: [
|
|
5756
|
+
admissionReceipts: [
|
|
5757
|
+
{
|
|
5758
|
+
...sourcePayload.admissionReceipts[0],
|
|
5759
|
+
journalBindingKey: recipientJournalBindingKey,
|
|
5760
|
+
},
|
|
5761
|
+
],
|
|
4071
5762
|
};
|
|
4072
5763
|
if (followerRegistered) {
|
|
4073
5764
|
return deps.stageThroughFollower({
|
|
@@ -4103,7 +5794,7 @@ export function createTelegramQueueHandoffReconciler(deps) {
|
|
|
4103
5794
|
typeof response.result !== "object" ||
|
|
4104
5795
|
!queueOwner) {
|
|
4105
5796
|
throw new Error(response.kind === "bus.ack"
|
|
4106
|
-
? response.message ?? "Telegram queue handoff was rejected."
|
|
5797
|
+
? (response.message ?? "Telegram queue handoff was rejected.")
|
|
4107
5798
|
: "Telegram queue handoff returned no acknowledgement.");
|
|
4108
5799
|
}
|
|
4109
5800
|
return {
|
|
@@ -4164,7 +5855,7 @@ export function createTelegramUpdateAdmissionRuntimeBinding(deps) {
|
|
|
4164
5855
|
let follower;
|
|
4165
5856
|
let settlement;
|
|
4166
5857
|
let inputCustodyBus;
|
|
4167
|
-
const getActive = () => deps.isFollowerRegistered() ? follower : leader;
|
|
5858
|
+
const getActive = () => (deps.isFollowerRegistered() ? follower : leader);
|
|
4168
5859
|
return {
|
|
4169
5860
|
bind(input) {
|
|
4170
5861
|
leader = input.leader;
|
|
@@ -4226,12 +5917,14 @@ export function createTelegramUpdateAdmissionLifecycleRuntime(deps) {
|
|
|
4226
5917
|
let foreignQueueOwnerLiveness;
|
|
4227
5918
|
let releaseSourceReference;
|
|
4228
5919
|
let sourceReferenceBinding;
|
|
5920
|
+
let activeContext;
|
|
4229
5921
|
const withRetainedSourceReference = (operation) => {
|
|
4230
5922
|
if (!journal || !worker)
|
|
4231
5923
|
throw new Error("Telegram update admission worker is not active.");
|
|
4232
5924
|
// Synchronous retained-source access needs a reference, not renewed execution authority.
|
|
4233
5925
|
const release = !releaseSourceReference && sourceReferenceBinding
|
|
4234
|
-
? deps.acquireSourceReference?.(sourceReferenceBinding)
|
|
5926
|
+
? deps.acquireSourceReference?.(sourceReferenceBinding)
|
|
5927
|
+
: undefined;
|
|
4235
5928
|
try {
|
|
4236
5929
|
return operation(journal);
|
|
4237
5930
|
}
|
|
@@ -4239,8 +5932,9 @@ export function createTelegramUpdateAdmissionLifecycleRuntime(deps) {
|
|
|
4239
5932
|
release?.();
|
|
4240
5933
|
}
|
|
4241
5934
|
};
|
|
4242
|
-
const readObservedJournal = () => withRetainedSourceReference(source => source.read());
|
|
5935
|
+
const readObservedJournal = () => withRetainedSourceReference((source) => source.read());
|
|
4243
5936
|
const stopCurrent = async (forget) => {
|
|
5937
|
+
activeContext = undefined;
|
|
4244
5938
|
await worker?.stop();
|
|
4245
5939
|
const release = releaseSourceReference;
|
|
4246
5940
|
releaseSourceReference = undefined;
|
|
@@ -4248,7 +5942,9 @@ export function createTelegramUpdateAdmissionLifecycleRuntime(deps) {
|
|
|
4248
5942
|
release?.();
|
|
4249
5943
|
}
|
|
4250
5944
|
catch (error) {
|
|
4251
|
-
deps.recordRuntimeEvent?.("inbound-worker", error, {
|
|
5945
|
+
deps.recordRuntimeEvent?.("inbound-worker", error, {
|
|
5946
|
+
phase: "source-reference-release",
|
|
5947
|
+
});
|
|
4252
5948
|
}
|
|
4253
5949
|
if (!forget)
|
|
4254
5950
|
return;
|
|
@@ -4267,12 +5963,15 @@ export function createTelegramUpdateAdmissionLifecycleRuntime(deps) {
|
|
|
4267
5963
|
await stopCurrent(true);
|
|
4268
5964
|
return;
|
|
4269
5965
|
}
|
|
4270
|
-
if (replace ||
|
|
5966
|
+
if (replace ||
|
|
5967
|
+
!worker ||
|
|
5968
|
+
activeRuntimeKey !== binding.runtimeKey ||
|
|
4271
5969
|
workerInputCustody !== binding.journal.inputCustody) {
|
|
4272
5970
|
const { runtimeKey, recoveryKey } = binding;
|
|
4273
5971
|
await stopCurrent(true);
|
|
4274
5972
|
binding = deps.resolveBinding();
|
|
4275
|
-
if (!binding ||
|
|
5973
|
+
if (!binding ||
|
|
5974
|
+
binding.runtimeKey !== runtimeKey ||
|
|
4276
5975
|
binding.recoveryKey !== recoveryKey) {
|
|
4277
5976
|
throw new Error("Telegram update source binding changed during startup.");
|
|
4278
5977
|
}
|
|
@@ -4371,6 +6070,7 @@ export function createTelegramUpdateAdmissionLifecycleRuntime(deps) {
|
|
|
4371
6070
|
}
|
|
4372
6071
|
}
|
|
4373
6072
|
worker.start(ctx);
|
|
6073
|
+
activeContext = ctx;
|
|
4374
6074
|
};
|
|
4375
6075
|
const runExclusive = (task) => {
|
|
4376
6076
|
const next = operation.then(task, task);
|
|
@@ -4428,7 +6128,7 @@ export function createTelegramUpdateAdmissionLifecycleRuntime(deps) {
|
|
|
4428
6128
|
if (!journal || !worker || !journal.cancelQueuedHandoff) {
|
|
4429
6129
|
throw new Error("Telegram update journal queue handoff cancellation is not available.");
|
|
4430
6130
|
}
|
|
4431
|
-
const result = withRetainedSourceReference(source => source.cancelQueuedHandoff(input));
|
|
6131
|
+
const result = withRetainedSourceReference((source) => source.cancelQueuedHandoff(input));
|
|
4432
6132
|
worker.signal();
|
|
4433
6133
|
return result;
|
|
4434
6134
|
},
|
|
@@ -4475,6 +6175,31 @@ export function createTelegramUpdateAdmissionLifecycleRuntime(deps) {
|
|
|
4475
6175
|
isTelegramReactionDependencyForQueueItem(entry.update, item)));
|
|
4476
6176
|
},
|
|
4477
6177
|
signal: () => worker?.signal(),
|
|
6178
|
+
prepareLiveInput(ctx, sourceUpdateIds, isCurrent) {
|
|
6179
|
+
const expectedWorker = worker, expectedBinding = sourceReferenceBinding, reference = releaseSourceReference;
|
|
6180
|
+
if (!expectedWorker?.prepareLiveInput ||
|
|
6181
|
+
!expectedBinding ||
|
|
6182
|
+
(deps.acquireSourceReference && !reference))
|
|
6183
|
+
return undefined;
|
|
6184
|
+
const current = () => {
|
|
6185
|
+
if (activeContext !== ctx ||
|
|
6186
|
+
worker !== expectedWorker ||
|
|
6187
|
+
sourceReferenceBinding !== expectedBinding ||
|
|
6188
|
+
releaseSourceReference !== reference ||
|
|
6189
|
+
expectedWorker.getState().phase === "stopped" ||
|
|
6190
|
+
isCurrent?.() === false)
|
|
6191
|
+
return false;
|
|
6192
|
+
const binding = deps.resolveBinding();
|
|
6193
|
+
return (binding?.runtimeKey === expectedBinding.runtimeKey &&
|
|
6194
|
+
binding.recoveryKey === expectedBinding.recoveryKey &&
|
|
6195
|
+
binding.journal.inputCustody ===
|
|
6196
|
+
expectedBinding.journal.inputCustody &&
|
|
6197
|
+
binding.hasAuthority?.() !== false);
|
|
6198
|
+
};
|
|
6199
|
+
if (!current())
|
|
6200
|
+
return undefined;
|
|
6201
|
+
return expectedWorker.prepareLiveInput(ctx, sourceUpdateIds, current);
|
|
6202
|
+
},
|
|
4478
6203
|
getState: () => {
|
|
4479
6204
|
const state = worker?.getState();
|
|
4480
6205
|
if (!state)
|
|
@@ -4496,20 +6221,25 @@ export function createTelegramUpdateAdmissionLifecycleRuntime(deps) {
|
|
|
4496
6221
|
/** Compose source-bound routing and late grouped settlement under one worker. */
|
|
4497
6222
|
export function createTelegramUpdateAdmissionWorkerRuntime(deps) {
|
|
4498
6223
|
let worker;
|
|
4499
|
-
const
|
|
6224
|
+
const admissionDeps = {
|
|
4500
6225
|
defaultHandle: deps.defaultHandle,
|
|
4501
6226
|
registry: deps.registry,
|
|
4502
|
-
abandonDeferred: input => worker?.abandonDeferred?.(input),
|
|
4503
|
-
armRoutingInput: input => worker?.armRoutingInput?.(input),
|
|
4504
|
-
selectRoutingInput: input => worker?.selectRoutingInput?.(input) === true,
|
|
4505
|
-
isRoutingInputCurrent: input => worker?.isRoutingInputCurrent?.(input) !== false,
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
|
|
6227
|
+
abandonDeferred: (input) => worker?.abandonDeferred?.(input),
|
|
6228
|
+
armRoutingInput: (input) => worker?.armRoutingInput?.(input),
|
|
6229
|
+
selectRoutingInput: (input) => worker?.selectRoutingInput?.(input) === true,
|
|
6230
|
+
isRoutingInputCurrent: (input) => worker?.isRoutingInputCurrent?.(input) !== false,
|
|
6231
|
+
getRoutingInput: (input) => worker?.getRoutingInput?.(input),
|
|
6232
|
+
supportsDeferredAbandonment: (input) => worker?.supportsDeferredAbandonment?.(input) === true,
|
|
6233
|
+
inspectAbandoning: (input) => worker?.inspectAbandoning?.(input),
|
|
6234
|
+
inspectHistorical: (input) => worker?.inspectHistorical?.(input),
|
|
6235
|
+
inspectDeferredSource: (input) => worker?.inspectDeferredSource?.(input),
|
|
6236
|
+
inspectDeferredSourceSnapshot: (input) => worker?.inspectDeferredSourceSnapshot?.(input),
|
|
6237
|
+
prepareDeferredLiveInput: (input) => worker?.prepareDeferredLiveInput?.(input),
|
|
6238
|
+
prepareDeferredQueueAdmission: (input) => worker?.prepareDeferredQueueAdmission?.(input),
|
|
6239
|
+
prepareDeferredSourceCompletion: (input) => worker?.prepareDeferredSourceCompletion?.(input),
|
|
6240
|
+
isHistoricalSource: (input) => worker?.isHistoricalSource?.(input) === true,
|
|
4511
6241
|
onLateOutcome(outcome, details) {
|
|
4512
|
-
worker?.settleDeferred({
|
|
6242
|
+
return worker?.settleDeferred({
|
|
4513
6243
|
updateId: details.updateId,
|
|
4514
6244
|
outcome,
|
|
4515
6245
|
signal: details.signal,
|
|
@@ -4521,24 +6251,54 @@ export function createTelegramUpdateAdmissionWorkerRuntime(deps) {
|
|
|
4521
6251
|
updateId,
|
|
4522
6252
|
});
|
|
4523
6253
|
},
|
|
4524
|
-
}
|
|
4525
|
-
const
|
|
6254
|
+
};
|
|
6255
|
+
const executeUpdate = createTelegramUpdateAdmissionHandle(admissionDeps);
|
|
6256
|
+
const admitPreparedLiveInput = async (update, ctx, signal) => {
|
|
6257
|
+
let carrier;
|
|
6258
|
+
const admit = createTelegramUpdateSourceAdmissionHandle(admissionDeps, (value) => {
|
|
6259
|
+
if (!value.message || typeof value.message !== "object")
|
|
6260
|
+
throw new Error("Prepared live input needs one native message carrier.");
|
|
6261
|
+
carrier = value.message;
|
|
6262
|
+
});
|
|
6263
|
+
const outcome = await admit(deps.prepareUpdateForExecution?.(update) ??
|
|
6264
|
+
update, ctx, signal);
|
|
6265
|
+
return { outcome, carrier };
|
|
6266
|
+
};
|
|
6267
|
+
const custodyBindingKey = deps.inputCustody
|
|
6268
|
+
? deps.getRecipientBindingKey?.()
|
|
6269
|
+
: undefined;
|
|
4526
6270
|
if (deps.inputCustody && !custodyBindingKey)
|
|
4527
6271
|
throw new Error("Telegram custodied admission worker requires one recipient binding key.");
|
|
4528
6272
|
const executeCustodiedUpdate = deps.inputCustody && custodyBindingKey
|
|
4529
6273
|
? createTelegramCustodiedUpdateAdmissionHandle({
|
|
4530
|
-
journal: deps.inputCustody,
|
|
4531
|
-
|
|
6274
|
+
journal: deps.inputCustody,
|
|
6275
|
+
recipientBindingKey: custodyBindingKey,
|
|
6276
|
+
defaultHandle: deps.defaultHandle,
|
|
6277
|
+
registry: deps.registry,
|
|
4532
6278
|
onLateOutcomeError(error, updateId) {
|
|
4533
|
-
deps.recordRuntimeEvent?.("inbound-worker", error, {
|
|
6279
|
+
deps.recordRuntimeEvent?.("inbound-worker", error, {
|
|
6280
|
+
phase: "late-custodied-admission-handler",
|
|
6281
|
+
updateId,
|
|
6282
|
+
});
|
|
4534
6283
|
},
|
|
4535
6284
|
onCustodiedLateSettlement(result, details) {
|
|
4536
|
-
worker?.settleCustodied({
|
|
6285
|
+
worker?.settleCustodied({
|
|
6286
|
+
updateId: details.updateId,
|
|
6287
|
+
result,
|
|
6288
|
+
signal: details.signal,
|
|
6289
|
+
});
|
|
4537
6290
|
},
|
|
4538
|
-
})
|
|
6291
|
+
})
|
|
6292
|
+
: undefined;
|
|
4539
6293
|
worker = createTelegramUpdateWorkerRuntime({
|
|
4540
6294
|
...deps,
|
|
4541
|
-
|
|
6295
|
+
admitPreparedLiveInput,
|
|
6296
|
+
...(executeCustodiedUpdate
|
|
6297
|
+
? {
|
|
6298
|
+
executeCustodiedUpdate: async (update, ctx, signal) => executeCustodiedUpdate(deps.prepareUpdateForExecution?.(update) ??
|
|
6299
|
+
update, ctx, signal),
|
|
6300
|
+
}
|
|
6301
|
+
: {}),
|
|
4542
6302
|
async executeUpdate(update, ctx, signal) {
|
|
4543
6303
|
const typedUpdate = update;
|
|
4544
6304
|
try {
|
|
@@ -4558,13 +6318,19 @@ export function createTelegramUpdateAdmissionWorkerRuntime(deps) {
|
|
|
4558
6318
|
export function createTelegramUpdateAdmissionLifecycleAssembly(deps) {
|
|
4559
6319
|
const leader = createTelegramUpdateAdmissionLifecycleRuntime({
|
|
4560
6320
|
resolveBinding: deps.leader.resolveBinding,
|
|
4561
|
-
...(deps.acquireSourceReference
|
|
6321
|
+
...(deps.acquireSourceReference
|
|
6322
|
+
? {
|
|
6323
|
+
acquireSourceReference: (binding) => deps.acquireSourceReference("leader", binding),
|
|
6324
|
+
}
|
|
6325
|
+
: {}),
|
|
4562
6326
|
getQueueOwnerIdentity: deps.worker.getQueueOwnerIdentity,
|
|
4563
6327
|
createWorker(journal, binding) {
|
|
4564
6328
|
return createTelegramUpdateAdmissionWorkerRuntime({
|
|
4565
6329
|
...deps.worker,
|
|
4566
6330
|
journal,
|
|
4567
|
-
...(binding.journal.inputCustody
|
|
6331
|
+
...(binding.journal.inputCustody
|
|
6332
|
+
? { inputCustody: binding.journal.inputCustody }
|
|
6333
|
+
: {}),
|
|
4568
6334
|
getJournalBindingKey: () => binding.recoveryKey,
|
|
4569
6335
|
getRecipientBindingKey: () => binding.recipientBindingKey,
|
|
4570
6336
|
hasAuthority: deps.leader.hasAuthority,
|
|
@@ -4574,8 +6340,14 @@ export function createTelegramUpdateAdmissionLifecycleAssembly(deps) {
|
|
|
4574
6340
|
});
|
|
4575
6341
|
const follower = createTelegramUpdateAdmissionLifecycleRuntime({
|
|
4576
6342
|
getQueueOwnerIdentity: deps.worker.getQueueOwnerIdentity,
|
|
4577
|
-
...(deps.acquireSourceReference
|
|
4578
|
-
|
|
6343
|
+
...(deps.acquireSourceReference
|
|
6344
|
+
? {
|
|
6345
|
+
acquireSourceReference: (binding) => deps.acquireSourceReference("follower", binding),
|
|
6346
|
+
}
|
|
6347
|
+
: {}),
|
|
6348
|
+
...(deps.follower.prepareBinding
|
|
6349
|
+
? { prepareBinding: deps.follower.prepareBinding }
|
|
6350
|
+
: {}),
|
|
4579
6351
|
resolveBinding() {
|
|
4580
6352
|
const generation = deps.follower.getGeneration();
|
|
4581
6353
|
if (!deps.follower.isRegistered() || !generation)
|
|
@@ -4596,7 +6368,9 @@ export function createTelegramUpdateAdmissionLifecycleAssembly(deps) {
|
|
|
4596
6368
|
return createTelegramUpdateAdmissionWorkerRuntime({
|
|
4597
6369
|
...deps.worker,
|
|
4598
6370
|
journal,
|
|
4599
|
-
...(binding.journal.inputCustody
|
|
6371
|
+
...(binding.journal.inputCustody
|
|
6372
|
+
? { inputCustody: binding.journal.inputCustody }
|
|
6373
|
+
: {}),
|
|
4600
6374
|
getJournalBindingKey: () => binding.recoveryKey,
|
|
4601
6375
|
getRecipientBindingKey: () => binding.recipientBindingKey,
|
|
4602
6376
|
hasAuthority: () => binding.hasAuthority?.() ?? false,
|
|
@@ -4610,8 +6384,11 @@ export function createTelegramUpdateAdmissionLifecycleAssembly(deps) {
|
|
|
4610
6384
|
},
|
|
4611
6385
|
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
4612
6386
|
});
|
|
4613
|
-
deps.runtimeBinding.bind({
|
|
4614
|
-
|
|
6387
|
+
deps.runtimeBinding.bind({
|
|
6388
|
+
leader,
|
|
6389
|
+
follower,
|
|
6390
|
+
...(deps.inputCustodyBus ? { inputCustodyBus: deps.inputCustodyBus } : {}),
|
|
6391
|
+
});
|
|
4615
6392
|
return { leader, follower };
|
|
4616
6393
|
}
|
|
4617
6394
|
/** Own queue-owner projection and shared leader/follower worker composition. */
|