@llblab/pi-kit 0.29.1 → 0.30.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/BACKLOG.md +3 -1
- package/CHANGELOG.md +10 -0
- package/README.md +4 -4
- package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +12 -0
- package/node_modules/@llblab/pi-state-flow/README.md +2 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/context.d.ts +2 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/context.js +17 -8
- package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +7 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.d.ts +2 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.js +3 -2
- package/node_modules/@llblab/pi-state-flow/dist/package.json +1 -1
- package/node_modules/@llblab/pi-state-flow/docs/architecture.md +3 -3
- package/node_modules/@llblab/pi-state-flow/docs/usage.md +3 -2
- package/node_modules/@llblab/pi-state-flow/lib/context.ts +17 -8
- package/node_modules/@llblab/pi-state-flow/lib/extension.ts +6 -3
- package/node_modules/@llblab/pi-state-flow/lib/protocol.ts +4 -2
- package/node_modules/@llblab/pi-state-flow/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/AGENTS.md +13 -10
- package/node_modules/@llblab/pi-telegram/BACKLOG.md +1 -1
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +23 -0
- package/node_modules/@llblab/pi-telegram/README.md +10 -11
- package/node_modules/@llblab/pi-telegram/api/commands.ts +3 -0
- package/node_modules/@llblab/pi-telegram/dist/api/commands.d.ts +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/activity-verbosity.d.ts +1 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/activity-verbosity.js +15 -42
- package/node_modules/@llblab/pi-telegram/dist/lib/activity.d.ts +9 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/activity.js +39 -10
- package/node_modules/@llblab/pi-telegram/dist/lib/agent-messages.js +7 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +43 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +164 -20
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-api.d.ts +12 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-api.js +181 -77
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.d.ts +111 -31
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.js +1324 -359
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.d.ts +41 -91
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.js +969 -637
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-transport.js +16 -9
- package/node_modules/@llblab/pi-telegram/dist/lib/bus.d.ts +215 -18
- package/node_modules/@llblab/pi-telegram/dist/lib/bus.js +1048 -327
- package/node_modules/@llblab/pi-telegram/dist/lib/channel-posts.d.ts +5 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/channel-posts.js +434 -261
- package/node_modules/@llblab/pi-telegram/dist/lib/command-templates.d.ts +26 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/command-templates.js +92 -176
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +168 -82
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +1409 -291
- package/node_modules/@llblab/pi-telegram/dist/lib/config.d.ts +1 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/config.js +34 -76
- package/node_modules/@llblab/pi-telegram/dist/lib/delivery.d.ts +2 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/delivery.js +16 -16
- package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +642 -170
- package/node_modules/@llblab/pi-telegram/dist/lib/generative-app-worker.mjs +19 -7
- package/node_modules/@llblab/pi-telegram/dist/lib/generative-apps.d.ts +0 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/generative-apps.js +61 -49
- package/node_modules/@llblab/pi-telegram/dist/lib/inbound.d.ts +5 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/inbound.js +35 -59
- package/node_modules/@llblab/pi-telegram/dist/lib/journal.d.ts +40 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/journal.js +2102 -760
- package/node_modules/@llblab/pi-telegram/dist/lib/lifecycle.d.ts +2 -6
- package/node_modules/@llblab/pi-telegram/dist/lib/lifecycle.js +75 -58
- package/node_modules/@llblab/pi-telegram/dist/lib/locks.d.ts +34 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/locks.js +318 -119
- package/node_modules/@llblab/pi-telegram/dist/lib/logging.js +50 -14
- package/node_modules/@llblab/pi-telegram/dist/lib/media.d.ts +22 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/media.js +46 -28
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-model.d.ts +19 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-model.js +53 -11
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-queue.d.ts +5 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-queue.js +45 -63
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.d.ts +5 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.js +114 -78
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-status.d.ts +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-status.js +8 -25
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-thinking.d.ts +6 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-thinking.js +14 -21
- package/node_modules/@llblab/pi-telegram/dist/lib/menu.d.ts +8 -61
- package/node_modules/@llblab/pi-telegram/dist/lib/menu.js +92 -164
- package/node_modules/@llblab/pi-telegram/dist/lib/model.d.ts +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/model.js +2 -16
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-attachments.d.ts +6 -27
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-attachments.js +45 -38
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-buttons.js +22 -16
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-markup.d.ts +0 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-markup.js +51 -64
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-voice.js +6 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound.d.ts +6 -6
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound.js +61 -51
- package/node_modules/@llblab/pi-telegram/dist/lib/ownership.js +6 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/paths.d.ts +5 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/paths.js +37 -21
- package/node_modules/@llblab/pi-telegram/dist/lib/pi.d.ts +2 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/pi.js +8 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/polling.d.ts +23 -37
- package/node_modules/@llblab/pi-telegram/dist/lib/polling.js +71 -52
- package/node_modules/@llblab/pi-telegram/dist/lib/preview.js +39 -19
- package/node_modules/@llblab/pi-telegram/dist/lib/process-identity.d.ts +24 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/process-identity.js +133 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/prompts.js +2 -7
- package/node_modules/@llblab/pi-telegram/dist/lib/queue.d.ts +16 -16
- package/node_modules/@llblab/pi-telegram/dist/lib/queue.js +261 -116
- package/node_modules/@llblab/pi-telegram/dist/lib/recovery.d.ts +2 -83
- package/node_modules/@llblab/pi-telegram/dist/lib/recovery.js +20 -274
- package/node_modules/@llblab/pi-telegram/dist/lib/replies.d.ts +16 -24
- package/node_modules/@llblab/pi-telegram/dist/lib/replies.js +99 -116
- package/node_modules/@llblab/pi-telegram/dist/lib/routing.d.ts +121 -7
- package/node_modules/@llblab/pi-telegram/dist/lib/routing.js +4750 -1161
- package/node_modules/@llblab/pi-telegram/dist/lib/runtime.d.ts +1 -11
- package/node_modules/@llblab/pi-telegram/dist/lib/sections.d.ts +4 -29
- package/node_modules/@llblab/pi-telegram/dist/lib/sections.js +7 -20
- package/node_modules/@llblab/pi-telegram/dist/lib/skills.js +4 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/status.js +86 -57
- package/node_modules/@llblab/pi-telegram/dist/lib/sync.d.ts +6 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/sync.js +112 -45
- package/node_modules/@llblab/pi-telegram/dist/lib/target.d.ts +4 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/target.js +18 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.d.ts +18 -11
- package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.js +165 -49
- package/node_modules/@llblab/pi-telegram/dist/lib/text-groups.js +4 -39
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-cleanup-manager.d.ts +6 -27
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-cleanup-manager.js +369 -186
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-display.d.ts +1 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-display.js +58 -38
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-naming.d.ts +28 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-naming.js +175 -66
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-reconciler.d.ts +42 -16
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-reconciler.js +90 -50
- package/node_modules/@llblab/pi-telegram/dist/lib/threads.d.ts +88 -24
- package/node_modules/@llblab/pi-telegram/dist/lib/threads.js +2276 -842
- package/node_modules/@llblab/pi-telegram/dist/lib/time-injection.js +1 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/turns.js +1 -19
- package/node_modules/@llblab/pi-telegram/dist/lib/updates.d.ts +178 -82
- package/node_modules/@llblab/pi-telegram/dist/lib/updates.js +2438 -645
- package/node_modules/@llblab/pi-telegram/dist/lib/voice.d.ts +7 -22
- package/node_modules/@llblab/pi-telegram/dist/lib/voice.js +7 -22
- package/node_modules/@llblab/pi-telegram/dist/lib/wire.js +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-admission.d.ts +3 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-admission.js +425 -211
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-identity.js +8 -13
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.d.ts +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.js +328 -140
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-slots.js +8 -4
- package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +43 -35
- package/node_modules/@llblab/pi-telegram/docs/inbound.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +15 -3
- package/node_modules/@llblab/pi-telegram/docs/outbound.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/public-api.md +29 -1
- package/node_modules/@llblab/pi-telegram/docs/ui-style.md +25 -8
- package/node_modules/@llblab/pi-telegram/lib/activity-verbosity.ts +59 -82
- package/node_modules/@llblab/pi-telegram/lib/activity.ts +146 -102
- package/node_modules/@llblab/pi-telegram/lib/agent-messages.ts +12 -5
- package/node_modules/@llblab/pi-telegram/lib/bindings.ts +390 -82
- package/node_modules/@llblab/pi-telegram/lib/bus-api.ts +262 -99
- package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +2440 -601
- package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +2209 -1290
- package/node_modules/@llblab/pi-telegram/lib/bus-transport.ts +46 -12
- package/node_modules/@llblab/pi-telegram/lib/bus.ts +1673 -444
- package/node_modules/@llblab/pi-telegram/lib/channel-posts.ts +848 -327
- package/node_modules/@llblab/pi-telegram/lib/command-templates.ts +152 -265
- package/node_modules/@llblab/pi-telegram/lib/commands.ts +2398 -555
- package/node_modules/@llblab/pi-telegram/lib/config.ts +177 -170
- package/node_modules/@llblab/pi-telegram/lib/delivery.ts +41 -29
- package/node_modules/@llblab/pi-telegram/lib/extension.ts +1209 -431
- package/node_modules/@llblab/pi-telegram/lib/generative-app-worker.mjs +19 -7
- package/node_modules/@llblab/pi-telegram/lib/generative-apps.ts +324 -211
- package/node_modules/@llblab/pi-telegram/lib/inbound.ts +95 -97
- package/node_modules/@llblab/pi-telegram/lib/journal.ts +5329 -1712
- package/node_modules/@llblab/pi-telegram/lib/keyboard.ts +1 -3
- package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +128 -78
- package/node_modules/@llblab/pi-telegram/lib/locks.ts +703 -212
- package/node_modules/@llblab/pi-telegram/lib/logging.ts +147 -58
- package/node_modules/@llblab/pi-telegram/lib/media.ts +82 -43
- package/node_modules/@llblab/pi-telegram/lib/menu-model.ts +85 -18
- package/node_modules/@llblab/pi-telegram/lib/menu-queue.ts +88 -129
- package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +260 -155
- package/node_modules/@llblab/pi-telegram/lib/menu-status.ts +20 -60
- package/node_modules/@llblab/pi-telegram/lib/menu-thinking.ts +41 -56
- package/node_modules/@llblab/pi-telegram/lib/menu.ts +112 -285
- package/node_modules/@llblab/pi-telegram/lib/model.ts +8 -22
- package/node_modules/@llblab/pi-telegram/lib/outbound-attachments.ts +156 -125
- package/node_modules/@llblab/pi-telegram/lib/outbound-buttons.ts +76 -49
- package/node_modules/@llblab/pi-telegram/lib/outbound-markup.ts +96 -94
- package/node_modules/@llblab/pi-telegram/lib/outbound-voice.ts +33 -27
- package/node_modules/@llblab/pi-telegram/lib/outbound.ts +125 -125
- package/node_modules/@llblab/pi-telegram/lib/ownership.ts +19 -10
- package/node_modules/@llblab/pi-telegram/lib/paths.ts +148 -49
- package/node_modules/@llblab/pi-telegram/lib/pi.ts +35 -15
- package/node_modules/@llblab/pi-telegram/lib/polling.ts +165 -149
- package/node_modules/@llblab/pi-telegram/lib/preview.ts +93 -40
- package/node_modules/@llblab/pi-telegram/lib/process-identity.ts +164 -0
- package/node_modules/@llblab/pi-telegram/lib/prompts.ts +4 -11
- package/node_modules/@llblab/pi-telegram/lib/queue.ts +523 -289
- package/node_modules/@llblab/pi-telegram/lib/recovery.ts +50 -454
- package/node_modules/@llblab/pi-telegram/lib/rendering.ts +1 -3
- package/node_modules/@llblab/pi-telegram/lib/replies.ts +248 -209
- package/node_modules/@llblab/pi-telegram/lib/routing.ts +8527 -2149
- package/node_modules/@llblab/pi-telegram/lib/runtime.ts +1 -12
- package/node_modules/@llblab/pi-telegram/lib/sections.ts +24 -89
- package/node_modules/@llblab/pi-telegram/lib/setup.ts +1 -3
- package/node_modules/@llblab/pi-telegram/lib/skills.ts +9 -4
- package/node_modules/@llblab/pi-telegram/lib/status.ts +215 -110
- package/node_modules/@llblab/pi-telegram/lib/sync.ts +488 -359
- package/node_modules/@llblab/pi-telegram/lib/target.ts +22 -3
- package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +322 -122
- package/node_modules/@llblab/pi-telegram/lib/text-groups.ts +5 -39
- package/node_modules/@llblab/pi-telegram/lib/thread-cleanup-manager.ts +833 -331
- package/node_modules/@llblab/pi-telegram/lib/thread-display.ts +213 -97
- package/node_modules/@llblab/pi-telegram/lib/thread-naming.ts +232 -87
- package/node_modules/@llblab/pi-telegram/lib/thread-reconciler.ts +155 -72
- package/node_modules/@llblab/pi-telegram/lib/threads.ts +5329 -1817
- package/node_modules/@llblab/pi-telegram/lib/time-injection.ts +1 -4
- package/node_modules/@llblab/pi-telegram/lib/turns.ts +15 -28
- package/node_modules/@llblab/pi-telegram/lib/updates.ts +5194 -1600
- package/node_modules/@llblab/pi-telegram/lib/voice.ts +7 -35
- package/node_modules/@llblab/pi-telegram/lib/wire.ts +1 -1
- package/node_modules/@llblab/pi-telegram/lib/workspace-admission.ts +996 -416
- package/node_modules/@llblab/pi-telegram/lib/workspace-identity.ts +14 -17
- package/node_modules/@llblab/pi-telegram/lib/workspace-retirement.ts +878 -376
- package/node_modules/@llblab/pi-telegram/lib/workspace-slots.ts +22 -13
- package/node_modules/@llblab/pi-telegram/package.json +1 -1
- package/package.json +3 -3
- package/node_modules/@llblab/pi-telegram/scripts/check-downgrade.mjs +0 -472
- package/node_modules/@llblab/pi-telegram/scripts/measure-workspace.mjs +0 -108
|
@@ -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,68 +1972,116 @@ 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));
|
|
1704
|
-
// Lost
|
|
1705
|
-
const candidates = [
|
|
1706
|
-
|
|
1707
|
-
|
|
1980
|
+
const binding = deps.getJournalBindingKey?.(), snapshotIds = new Set(snapshot.entries.map((entry) => entry.updateId));
|
|
1981
|
+
// Lost expiry ACKs reconcile the exact held original against its body-free discard tombstone.
|
|
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 (
|
|
1711
|
-
|
|
1991
|
+
if (!lifetime ||
|
|
1992
|
+
entry.state !== "pending" ||
|
|
1993
|
+
!binding ||
|
|
1994
|
+
claims.get(entry.updateId) === "queued" ||
|
|
1995
|
+
unsettledExecutionsByUpdateId.get(entry.updateId)?.size)
|
|
1712
1996
|
continue;
|
|
1713
|
-
|
|
1997
|
+
// Classify the first startup snapshot before constructing any historical expiry carrier.
|
|
1998
|
+
if (expectedOwner.startupUpdateIds?.has(entry.updateId) &&
|
|
1999
|
+
!claims.has(entry.updateId) &&
|
|
2000
|
+
deps.shouldReviewHistoricalInput) {
|
|
2001
|
+
nextAtMs = Math.min(nextAtMs ?? Infinity, Math.max(getNowMs() + 1, lifetime.expiresAtMs));
|
|
1714
2002
|
continue;
|
|
2003
|
+
}
|
|
1715
2004
|
if (getNowMs() < lifetime.expiresAtMs) {
|
|
1716
2005
|
nextAtMs = Math.min(nextAtMs ?? Infinity, lifetime.expiresAtMs);
|
|
1717
2006
|
continue;
|
|
1718
2007
|
}
|
|
1719
2008
|
const existing = deferredSources.get(entry.updateId);
|
|
1720
|
-
if (existing && !isDeepStrictEqual(existing.entry, entry))
|
|
1721
|
-
|
|
1722
|
-
|
|
2009
|
+
if (existing && !isDeepStrictEqual(existing.entry, entry)) {
|
|
2010
|
+
// Lost clock/selection ACKs may leave only this immutable metadata behind its exact source; reconciliation grants expiry, never another dispatch.
|
|
2011
|
+
const previous = existing.entry.routingInput;
|
|
2012
|
+
if (!isDeepStrictEqual({ ...existing.entry, routingInput: lifetime }, entry) ||
|
|
2013
|
+
(previous &&
|
|
2014
|
+
(!isDeepStrictEqual({ ...previous, phase: lifetime.phase }, lifetime) ||
|
|
2015
|
+
(previous.phase === "selected" && lifetime.phase !== "selected"))))
|
|
2016
|
+
continue;
|
|
2017
|
+
existing.entry = structuredClone(entry);
|
|
2018
|
+
}
|
|
2019
|
+
const source = existing ?? {
|
|
2020
|
+
entry: structuredClone(entry),
|
|
2021
|
+
journalBindingKey: binding,
|
|
2022
|
+
historical: true,
|
|
2023
|
+
};
|
|
1723
2024
|
deferredSources.set(entry.updateId, source);
|
|
1724
2025
|
if (!claims.has(entry.updateId))
|
|
1725
2026
|
claims.set(entry.updateId, "historical");
|
|
1726
|
-
const current = () => owner === expectedOwner &&
|
|
1727
|
-
|
|
1728
|
-
deps.
|
|
1729
|
-
|
|
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";
|
|
1730
2036
|
let committed;
|
|
1731
2037
|
try {
|
|
1732
2038
|
if (current())
|
|
1733
|
-
await expireRoutingObserver({
|
|
2039
|
+
await expireRoutingObserver({
|
|
2040
|
+
original: structuredClone(entry),
|
|
2041
|
+
journalBindingKey: binding,
|
|
2042
|
+
isCurrent: current,
|
|
1734
2043
|
expire() {
|
|
1735
2044
|
if (!current())
|
|
1736
2045
|
return undefined;
|
|
1737
2046
|
if (committed)
|
|
1738
2047
|
return { ...committed, duplicate: true };
|
|
1739
2048
|
claims.set(entry.updateId, "abandoning");
|
|
1740
|
-
committed = expireRouting({
|
|
2049
|
+
committed = expireRouting({
|
|
2050
|
+
entry: source.entry,
|
|
2051
|
+
journalBindingKey: binding,
|
|
2052
|
+
operatorUserId: lifetime.operatorUserId,
|
|
2053
|
+
isCurrent: current,
|
|
2054
|
+
});
|
|
2055
|
+
// Source expiry is terminal before chooser/cleanup awaits; later failures must not retain a deferred/retry source.
|
|
1741
2056
|
claims.delete(entry.updateId);
|
|
1742
2057
|
deferredSources.delete(entry.updateId);
|
|
1743
2058
|
state.journalEntryCount = committed.entryCount;
|
|
1744
2059
|
state.journalSerializedBytes = committed.serializedBytes;
|
|
1745
2060
|
updateClaimCounts();
|
|
1746
2061
|
notifyStateChange();
|
|
1747
|
-
changed
|
|
2062
|
+
changed ||= !committed.duplicate;
|
|
1748
2063
|
return committed;
|
|
1749
|
-
}
|
|
2064
|
+
},
|
|
2065
|
+
}, expectedOwner.ctx, expectedOwner.controller.signal);
|
|
1750
2066
|
}
|
|
1751
2067
|
catch (error) {
|
|
1752
|
-
recordRuntimeEvent(error, {
|
|
2068
|
+
recordRuntimeEvent(error, {
|
|
2069
|
+
phase: "routing-input-expiry",
|
|
2070
|
+
updateId: entry.updateId,
|
|
2071
|
+
});
|
|
1753
2072
|
}
|
|
1754
2073
|
if (!committed)
|
|
1755
2074
|
nextAtMs = Math.min(nextAtMs ?? Infinity, getNowMs() + 60_000);
|
|
1756
2075
|
}
|
|
1757
|
-
if (nextAtMs !== undefined &&
|
|
2076
|
+
if (nextAtMs !== undefined &&
|
|
2077
|
+
owner === expectedOwner &&
|
|
2078
|
+
!expectedOwner.controller.signal.aborted) {
|
|
1758
2079
|
const token = {};
|
|
1759
2080
|
routingTimerToken = token;
|
|
1760
2081
|
routingTimer = scheduleRetry(() => {
|
|
1761
|
-
if (routingTimerToken !== token ||
|
|
2082
|
+
if (routingTimerToken !== token ||
|
|
2083
|
+
owner !== expectedOwner ||
|
|
2084
|
+
expectedOwner.controller.signal.aborted)
|
|
1762
2085
|
return;
|
|
1763
2086
|
routingTimer = undefined;
|
|
1764
2087
|
routingTimerToken = undefined;
|
|
@@ -1770,32 +2093,69 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1770
2093
|
};
|
|
1771
2094
|
const mutateRoutingSource = (input, select) => {
|
|
1772
2095
|
const expectedOwner = owner, ids = [...input.sourceUpdateIds];
|
|
1773
|
-
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))
|
|
1774
2100
|
return undefined;
|
|
1775
|
-
const sources = ids.map(id => deferredSources.get(id)), binding = sources[0]?.journalBindingKey;
|
|
1776
|
-
const current = () => !!expectedOwner &&
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
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);
|
|
1781
2117
|
if (!current() || !armRouting || !selectRouting || !expireRoutingObserver)
|
|
1782
2118
|
return undefined;
|
|
1783
|
-
const authority = {
|
|
1784
|
-
|
|
1785
|
-
|
|
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()) {
|
|
1786
2138
|
throw new Error("Telegram routing input lifetime ACK was not confirmed.");
|
|
1787
2139
|
}
|
|
1788
2140
|
const retained = deps.journal.read().entries;
|
|
1789
|
-
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()) {
|
|
1790
2143
|
throw new Error("Telegram routing input lifetime ACK was not retained.");
|
|
1791
2144
|
}
|
|
1792
|
-
|
|
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
|
+
});
|
|
1793
2149
|
pendingSignal = true;
|
|
1794
2150
|
launchDrain();
|
|
1795
|
-
return {
|
|
2151
|
+
return {
|
|
2152
|
+
issued: result.issued,
|
|
2153
|
+
lifetime: { ...result.entries[0].routingInput },
|
|
2154
|
+
};
|
|
1796
2155
|
};
|
|
1797
2156
|
const drain = async (expectedOwner) => {
|
|
1798
|
-
while (owner === expectedOwner &&
|
|
2157
|
+
while (owner === expectedOwner &&
|
|
2158
|
+
!expectedOwner.controller.signal.aborted) {
|
|
1799
2159
|
const authorityResult = checkAuthority(expectedOwner);
|
|
1800
2160
|
if (authorityResult)
|
|
1801
2161
|
return authorityResult;
|
|
@@ -1806,7 +2166,7 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1806
2166
|
scheduledRetryAtMs = await refreshJournalState(snapshot, expectedOwner);
|
|
1807
2167
|
// One complete validated baseline per generation, before batching or execution.
|
|
1808
2168
|
// Replayed v1 entries do not prove that a prior process never forwarded them.
|
|
1809
|
-
expectedOwner.startupUpdateIds ??= new Set(snapshot.entries.map(entry => entry.updateId));
|
|
2169
|
+
expectedOwner.startupUpdateIds ??= new Set(snapshot.entries.map((entry) => entry.updateId));
|
|
1810
2170
|
}
|
|
1811
2171
|
catch (error) {
|
|
1812
2172
|
return blockWithFailure("journal-read", "journal-read", error);
|
|
@@ -1823,24 +2183,39 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1823
2183
|
let blockedInputCustody;
|
|
1824
2184
|
delete state.blockedInputCustody;
|
|
1825
2185
|
for (const candidate of snapshot.entries) {
|
|
2186
|
+
if (livePreparation?.ids.has(candidate.updateId) &&
|
|
2187
|
+
(!livePreparation.released || livePreparation.status?.issued))
|
|
2188
|
+
continue;
|
|
1826
2189
|
if (snapshot.version === TELEGRAM_UPDATE_JOURNAL_CUSTODY_VERSION &&
|
|
1827
2190
|
(candidate.state === "retry-wait" || candidate.state === "failed")) {
|
|
1828
2191
|
hasUnavailableInputCustody = true;
|
|
1829
|
-
blockedInputCustody ??= {
|
|
2192
|
+
blockedInputCustody ??= {
|
|
2193
|
+
updateId: candidate.updateId,
|
|
2194
|
+
kind: "legacy-retry-state",
|
|
2195
|
+
};
|
|
1830
2196
|
continue;
|
|
1831
2197
|
}
|
|
1832
|
-
if (snapshot.version === TELEGRAM_UPDATE_JOURNAL_CUSTODY_VERSION &&
|
|
2198
|
+
if (snapshot.version === TELEGRAM_UPDATE_JOURNAL_CUSTODY_VERSION &&
|
|
2199
|
+
candidate.inputClaim) {
|
|
1833
2200
|
if (candidate.inputClaim.phase === "running") {
|
|
1834
2201
|
hasOutcomeUnknownInput = true;
|
|
1835
|
-
if (!blockedInputCustody ||
|
|
1836
|
-
blockedInputCustody
|
|
1837
|
-
|
|
2202
|
+
if (!blockedInputCustody ||
|
|
2203
|
+
blockedInputCustody.kind !== "running-outcome-unknown")
|
|
2204
|
+
blockedInputCustody = {
|
|
2205
|
+
updateId: candidate.updateId,
|
|
2206
|
+
kind: "running-outcome-unknown",
|
|
2207
|
+
};
|
|
1838
2208
|
continue;
|
|
1839
2209
|
}
|
|
1840
|
-
if (candidate.inputClaim.handoff ||
|
|
2210
|
+
if (candidate.inputClaim.handoff ||
|
|
2211
|
+
!isTelegramUpdateJournalQueueOwnerProcess(candidate.inputClaim.owner, expectedOwner.queueOwnerIdentity)) {
|
|
1841
2212
|
hasUnavailableInputCustody = true;
|
|
1842
|
-
blockedInputCustody ??= {
|
|
1843
|
-
|
|
2213
|
+
blockedInputCustody ??= {
|
|
2214
|
+
updateId: candidate.updateId,
|
|
2215
|
+
kind: candidate.inputClaim.handoff
|
|
2216
|
+
? "handoff-frozen"
|
|
2217
|
+
: "foreign-ready",
|
|
2218
|
+
};
|
|
1844
2219
|
continue;
|
|
1845
2220
|
}
|
|
1846
2221
|
}
|
|
@@ -1870,6 +2245,9 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1870
2245
|
const completedUpdateIds = [];
|
|
1871
2246
|
let snapshotInvalidated = false;
|
|
1872
2247
|
for (const entry of entries) {
|
|
2248
|
+
if (livePreparation?.ids.has(entry.updateId) &&
|
|
2249
|
+
(!livePreparation.released || livePreparation.status?.issued))
|
|
2250
|
+
continue;
|
|
1873
2251
|
const priorExecutions = unsettledExecutionsByUpdateId.get(entry.updateId);
|
|
1874
2252
|
if (priorExecutions?.size) {
|
|
1875
2253
|
const completionResult = commitCompletedBatch(expectedOwner, completedUpdateIds);
|
|
@@ -1888,7 +2266,9 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1888
2266
|
completedUpdateIds.push({ updateId: entry.updateId });
|
|
1889
2267
|
continue;
|
|
1890
2268
|
}
|
|
1891
|
-
if (isTelegramUpdateJournalLegacyFamilyVersion(snapshot.version) &&
|
|
2269
|
+
if (isTelegramUpdateJournalLegacyFamilyVersion(snapshot.version) &&
|
|
2270
|
+
entry.state === "pending" &&
|
|
2271
|
+
deps.journal.inspectPendingRetention) {
|
|
1892
2272
|
const authorityResult = checkAuthority(expectedOwner, entry.updateId);
|
|
1893
2273
|
if (authorityResult)
|
|
1894
2274
|
return authorityResult;
|
|
@@ -1899,7 +2279,10 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1899
2279
|
}
|
|
1900
2280
|
catch (error) {
|
|
1901
2281
|
unverifiable = true;
|
|
1902
|
-
recordRuntimeEvent(error, {
|
|
2282
|
+
recordRuntimeEvent(error, {
|
|
2283
|
+
phase: "pending-retention-inspection",
|
|
2284
|
+
updateId: entry.updateId,
|
|
2285
|
+
});
|
|
1903
2286
|
}
|
|
1904
2287
|
if (retained || unverifiable) {
|
|
1905
2288
|
// A copy is protection evidence only. A fresh owner must explicitly retry
|
|
@@ -1912,19 +2295,28 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1912
2295
|
continue;
|
|
1913
2296
|
}
|
|
1914
2297
|
}
|
|
1915
|
-
if (isTelegramUpdateJournalLegacyFamilyVersion(snapshot.version) &&
|
|
2298
|
+
if (isTelegramUpdateJournalLegacyFamilyVersion(snapshot.version) &&
|
|
2299
|
+
(entry.state === "pending" || entry.state === "retry-wait")) {
|
|
1916
2300
|
const historical = expectedOwner.startupUpdateIds.has(entry.updateId);
|
|
2301
|
+
let revived = false;
|
|
1917
2302
|
const journalBindingKey = deps.getJournalBindingKey?.();
|
|
1918
2303
|
// Classifiers and handler projections receive no mutable alias to CAS evidence.
|
|
1919
2304
|
if (journalBindingKey)
|
|
1920
|
-
deferredSources.set(entry.updateId, {
|
|
1921
|
-
|
|
1922
|
-
|
|
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;
|
|
1923
2313
|
if (classifyPending) {
|
|
1924
2314
|
const checkHistoricalAuthority = () => {
|
|
1925
|
-
if (owner !== expectedOwner ||
|
|
2315
|
+
if (owner !== expectedOwner ||
|
|
2316
|
+
expectedOwner.controller.signal.aborted)
|
|
1926
2317
|
return "aborted";
|
|
1927
|
-
if (deps.getJournalBindingKey?.() !== journalBindingKey ||
|
|
2318
|
+
if (deps.getJournalBindingKey?.() !== journalBindingKey ||
|
|
2319
|
+
deps.isContextCurrent?.(expectedOwner.ctx) === false) {
|
|
1928
2320
|
transition("blocked", entry.updateId, "authority-lost");
|
|
1929
2321
|
return "blocked";
|
|
1930
2322
|
}
|
|
@@ -1946,12 +2338,18 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1946
2338
|
const authorityResult = checkHistoricalAuthority();
|
|
1947
2339
|
if (authorityResult)
|
|
1948
2340
|
return authorityResult;
|
|
1949
|
-
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())))))
|
|
1950
2347
|
return blockWithFailure("execution", "historical-review-classification", new TelegramUpdateAdmissionOutcomeError("Historical classification must return a boolean or retain verdict."), entry.updateId);
|
|
1951
2348
|
// Classification may await a domain read; accepted/changed source evidence wins.
|
|
1952
2349
|
try {
|
|
1953
2350
|
const current = deps.journal.read();
|
|
1954
|
-
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)) {
|
|
1955
2353
|
snapshotInvalidated = true;
|
|
1956
2354
|
break;
|
|
1957
2355
|
}
|
|
@@ -1959,38 +2357,96 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1959
2357
|
catch (error) {
|
|
1960
2358
|
return blockWithFailure("journal-read", "historical-source-recheck", error, entry.updateId);
|
|
1961
2359
|
}
|
|
1962
|
-
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") {
|
|
1963
2369
|
claims.set(entry.updateId, "retained");
|
|
1964
2370
|
transition("deferred", entry.updateId);
|
|
1965
2371
|
continue;
|
|
1966
2372
|
}
|
|
1967
|
-
if (review &&
|
|
1968
|
-
|
|
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
|
+
});
|
|
1969
2382
|
continue;
|
|
1970
2383
|
}
|
|
1971
|
-
if (review) {
|
|
2384
|
+
if (review === true) {
|
|
1972
2385
|
claims.set(entry.updateId, "historical");
|
|
1973
2386
|
transition("deferred", entry.updateId);
|
|
1974
2387
|
continue;
|
|
1975
2388
|
}
|
|
1976
2389
|
}
|
|
1977
|
-
if (entry.state === "pending" && entry.routingInput) {
|
|
1978
|
-
if (historical && spendHistorical) {
|
|
1979
|
-
completedUpdateIds.push({ updateId: entry.updateId, spent: true });
|
|
1980
|
-
continue;
|
|
1981
|
-
}
|
|
2390
|
+
if (!revived && entry.state === "pending" && entry.routingInput) {
|
|
1982
2391
|
claims.set(entry.updateId, "historical");
|
|
1983
2392
|
transition("deferred", entry.updateId);
|
|
1984
2393
|
continue;
|
|
1985
2394
|
}
|
|
1986
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
|
+
}
|
|
1987
2422
|
clearRetryTimer();
|
|
1988
2423
|
transition("executing", entry.updateId);
|
|
1989
|
-
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);
|
|
1990
2438
|
if (execution === TELEGRAM_UPDATE_WORKER_EXECUTION_ABORTED) {
|
|
1991
2439
|
return "aborted";
|
|
1992
2440
|
}
|
|
1993
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
|
+
}
|
|
1994
2450
|
if (deps.executeCustodiedUpdate) {
|
|
1995
2451
|
transition("blocked", entry.updateId, "execution");
|
|
1996
2452
|
return "blocked";
|
|
@@ -2039,7 +2495,12 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
2039
2495
|
return postExecutionAuthority;
|
|
2040
2496
|
const claimableUpdateIds = new Set([
|
|
2041
2497
|
entry.updateId,
|
|
2042
|
-
...[...claims]
|
|
2498
|
+
...[...claims]
|
|
2499
|
+
.filter(([id, claim]) => !livePreparation?.ids.has(id) &&
|
|
2500
|
+
claim !== "abandoning" &&
|
|
2501
|
+
claim !== "historical" &&
|
|
2502
|
+
claim !== "retained")
|
|
2503
|
+
.map(([id]) => id),
|
|
2043
2504
|
]);
|
|
2044
2505
|
let outcome;
|
|
2045
2506
|
try {
|
|
@@ -2049,11 +2510,37 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
2049
2510
|
return blockWithFailure("invalid-outcome", "invalid-outcome", error, entry.updateId);
|
|
2050
2511
|
}
|
|
2051
2512
|
if (outcome.kind === "deferred") {
|
|
2052
|
-
if (outcome.routingReview &&
|
|
2513
|
+
if (outcome.routingReview &&
|
|
2514
|
+
!deferredSources.get(entry.updateId)?.historical) {
|
|
2053
2515
|
return blockWithFailure("invalid-outcome", "historical-review-source", new TelegramUpdateAdmissionOutcomeError("Historical review needs an exact startup source."), entry.updateId);
|
|
2054
2516
|
}
|
|
2055
2517
|
claims.set(entry.updateId, outcome.routingReview ? "historical" : "deferred");
|
|
2056
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
|
+
}
|
|
2057
2544
|
continue;
|
|
2058
2545
|
}
|
|
2059
2546
|
if (outcome.kind === "queued") {
|
|
@@ -2068,7 +2555,12 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
2068
2555
|
snapshotInvalidated = true;
|
|
2069
2556
|
break;
|
|
2070
2557
|
}
|
|
2071
|
-
completedUpdateIds.push({
|
|
2558
|
+
completedUpdateIds.push({
|
|
2559
|
+
updateId: entry.updateId,
|
|
2560
|
+
...(outcome.expectedSource
|
|
2561
|
+
? { expectedSource: outcome.expectedSource }
|
|
2562
|
+
: {}),
|
|
2563
|
+
});
|
|
2072
2564
|
}
|
|
2073
2565
|
const completionResult = commitCompletedBatch(expectedOwner, completedUpdateIds);
|
|
2074
2566
|
if (completionResult)
|
|
@@ -2076,8 +2568,7 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
2076
2568
|
if (!snapshotInvalidated && !hasMoreEntries)
|
|
2077
2569
|
continue;
|
|
2078
2570
|
await yieldToEventLoop();
|
|
2079
|
-
if (owner !== expectedOwner ||
|
|
2080
|
-
expectedOwner.controller.signal.aborted) {
|
|
2571
|
+
if (owner !== expectedOwner || expectedOwner.controller.signal.aborted) {
|
|
2081
2572
|
return "aborted";
|
|
2082
2573
|
}
|
|
2083
2574
|
}
|
|
@@ -2092,7 +2583,9 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
2092
2583
|
try {
|
|
2093
2584
|
startupKey = deps.getJournalBindingKey?.();
|
|
2094
2585
|
}
|
|
2095
|
-
catch {
|
|
2586
|
+
catch {
|
|
2587
|
+
/* Missing startup identity grants no notification. */
|
|
2588
|
+
}
|
|
2096
2589
|
}
|
|
2097
2590
|
let lastResult;
|
|
2098
2591
|
const run = async () => {
|
|
@@ -2113,28 +2606,55 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
2113
2606
|
if (pendingSignal && !expectedOwner.controller.signal.aborted) {
|
|
2114
2607
|
launchDrain();
|
|
2115
2608
|
}
|
|
2116
|
-
else if (lastResult === "idle" &&
|
|
2609
|
+
else if (lastResult === "idle" &&
|
|
2610
|
+
startupKey &&
|
|
2611
|
+
onHeldSourcesPrepared &&
|
|
2612
|
+
!expectedOwner.heldSourcesPreparedIssued) {
|
|
2117
2613
|
try {
|
|
2118
2614
|
const ctx = expectedOwner.ctx, key = startupKey;
|
|
2119
|
-
const isCurrent = () => owner === expectedOwner &&
|
|
2120
|
-
|
|
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);
|
|
2121
2621
|
if (!isCurrent())
|
|
2122
2622
|
return;
|
|
2123
2623
|
expectedOwner.heldSourcesPreparedIssued = true;
|
|
2124
2624
|
// Do not couple lifecycle/stop or the worker drain to a held controller/API reply.
|
|
2125
|
-
void Promise.resolve()
|
|
2126
|
-
|
|
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) => {
|
|
2127
2639
|
try {
|
|
2128
|
-
deps.recordRuntimeEvent?.("inbound-worker", error, {
|
|
2640
|
+
deps.recordRuntimeEvent?.("inbound-worker", error, {
|
|
2641
|
+
phase: "held-source-preparation",
|
|
2642
|
+
});
|
|
2643
|
+
}
|
|
2644
|
+
catch {
|
|
2645
|
+
/* Diagnostic only. */
|
|
2129
2646
|
}
|
|
2130
|
-
catch { /* Diagnostic only. */ }
|
|
2131
2647
|
});
|
|
2132
2648
|
}
|
|
2133
2649
|
catch (error) {
|
|
2134
2650
|
try {
|
|
2135
|
-
deps.recordRuntimeEvent?.("inbound-worker", error, {
|
|
2651
|
+
deps.recordRuntimeEvent?.("inbound-worker", error, {
|
|
2652
|
+
phase: "held-source-preparation",
|
|
2653
|
+
});
|
|
2654
|
+
}
|
|
2655
|
+
catch {
|
|
2656
|
+
/* Diagnostic only. */
|
|
2136
2657
|
}
|
|
2137
|
-
catch { /* Diagnostic only. */ }
|
|
2138
2658
|
}
|
|
2139
2659
|
}
|
|
2140
2660
|
}
|
|
@@ -2153,22 +2673,34 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
2153
2673
|
const source = deferredSources.get(input.updateId);
|
|
2154
2674
|
const abandonPending = deps.journal.abandonPending;
|
|
2155
2675
|
const isCurrent = () => {
|
|
2156
|
-
if (!expectedOwner ||
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
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)))
|
|
2161
2687
|
return false;
|
|
2162
|
-
return input.isCurrent() &&
|
|
2163
|
-
deps.
|
|
2688
|
+
return (input.isCurrent() &&
|
|
2689
|
+
deps.isContextCurrent?.(expectedOwner.ctx) !== false &&
|
|
2690
|
+
deps.getJournalBindingKey?.() === source.journalBindingKey &&
|
|
2691
|
+
deps.hasAuthority(expectedOwner.ctx));
|
|
2164
2692
|
};
|
|
2165
2693
|
if (!source || !abandonPending || !isCurrent())
|
|
2166
2694
|
return undefined;
|
|
2167
2695
|
// Retain this claim on failure, including unknown commit: never reopen dispatch.
|
|
2168
2696
|
claims.set(input.updateId, "abandoning");
|
|
2169
2697
|
try {
|
|
2170
|
-
const result = abandonPending({
|
|
2171
|
-
|
|
2698
|
+
const result = abandonPending({
|
|
2699
|
+
...input,
|
|
2700
|
+
entry: source.entry,
|
|
2701
|
+
journalBindingKey: source.journalBindingKey,
|
|
2702
|
+
isCurrent,
|
|
2703
|
+
});
|
|
2172
2704
|
claims.delete(input.updateId);
|
|
2173
2705
|
deferredSources.delete(input.updateId);
|
|
2174
2706
|
state.journalEntryCount = result.entryCount;
|
|
@@ -2179,7 +2711,10 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
2179
2711
|
return result;
|
|
2180
2712
|
}
|
|
2181
2713
|
catch (error) {
|
|
2182
|
-
recordRuntimeEvent(error, {
|
|
2714
|
+
recordRuntimeEvent(error, {
|
|
2715
|
+
phase: "pending-abandonment",
|
|
2716
|
+
updateId: input.updateId,
|
|
2717
|
+
});
|
|
2183
2718
|
updateClaimCounts();
|
|
2184
2719
|
notifyStateChange();
|
|
2185
2720
|
throw error;
|
|
@@ -2189,34 +2724,93 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
2189
2724
|
const expectedOwner = owner;
|
|
2190
2725
|
if (!expectedOwner)
|
|
2191
2726
|
return undefined;
|
|
2192
|
-
const { signal, journalBindingKey, isCurrent: callerIsCurrent, afterUpdateId } = input;
|
|
2193
|
-
const isOwnerCurrent = () => owner === expectedOwner &&
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
(
|
|
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)))
|
|
2198
2741
|
return undefined;
|
|
2199
|
-
const matches = [...deferredSources]
|
|
2200
|
-
|
|
2742
|
+
const matches = [...deferredSources]
|
|
2743
|
+
.filter(([id, source]) => claims.get(id) === kind &&
|
|
2744
|
+
source.journalBindingKey === journalBindingKey &&
|
|
2745
|
+
(afterUpdateId === undefined || id > afterUpdateId))
|
|
2201
2746
|
.sort(([a], [b]) => a - b);
|
|
2202
|
-
const sources = matches
|
|
2747
|
+
const sources = matches
|
|
2748
|
+
.slice(0, TELEGRAM_HELD_SOURCE_INSPECTION_LIMIT)
|
|
2749
|
+
.map(([updateId, source]) => {
|
|
2203
2750
|
let committed;
|
|
2204
|
-
return {
|
|
2751
|
+
return {
|
|
2752
|
+
original: structuredClone(source.entry),
|
|
2753
|
+
retry(authority) {
|
|
2205
2754
|
const authorized = () => isOwnerCurrent() && authority.isCurrent() && isCurrent();
|
|
2206
2755
|
if (!authorized())
|
|
2207
2756
|
return undefined;
|
|
2208
2757
|
if (committed)
|
|
2209
2758
|
return { ...committed, duplicate: true };
|
|
2210
|
-
const stillProtected = () => authorized() &&
|
|
2759
|
+
const stillProtected = () => authorized() &&
|
|
2760
|
+
(claims.get(updateId) === kind ||
|
|
2761
|
+
claims.get(updateId) === "abandoning") &&
|
|
2211
2762
|
deferredSources.get(updateId) === source;
|
|
2212
2763
|
if (!stillProtected())
|
|
2213
2764
|
return undefined;
|
|
2214
2765
|
committed = abandonSource({ ...authority, updateId, signal, isCurrent: stillProtected }, source);
|
|
2215
2766
|
return committed;
|
|
2216
|
-
}
|
|
2767
|
+
},
|
|
2768
|
+
};
|
|
2217
2769
|
});
|
|
2218
|
-
return isCurrent()
|
|
2219
|
-
|
|
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
|
+
};
|
|
2220
2814
|
};
|
|
2221
2815
|
return {
|
|
2222
2816
|
start(ctx) {
|
|
@@ -2238,6 +2832,8 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
2238
2832
|
controller: createAbortController(),
|
|
2239
2833
|
queueOwnerIdentity,
|
|
2240
2834
|
};
|
|
2835
|
+
livePreparation = undefined;
|
|
2836
|
+
delete state.preparedInputCount;
|
|
2241
2837
|
claims.clear();
|
|
2242
2838
|
deferredSources.clear();
|
|
2243
2839
|
committedQueueReceipts.clear();
|
|
@@ -2251,25 +2847,7 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
2251
2847
|
state.journalEntryCount = 0;
|
|
2252
2848
|
state.journalSerializedBytes = 0;
|
|
2253
2849
|
state.oldestAdmittedAtMs = undefined;
|
|
2254
|
-
state.
|
|
2255
|
-
delete state.abandoningClaimCount;
|
|
2256
|
-
delete state.historicalClaimCount;
|
|
2257
|
-
state.queuedClaimCount = 0;
|
|
2258
|
-
state.foreignQueuedCount = 0;
|
|
2259
|
-
delete state.foreignQueuedOwner;
|
|
2260
|
-
state.retryWaitCount = 0;
|
|
2261
|
-
state.failedCount = 0;
|
|
2262
|
-
state.nextRetryUpdateId = undefined;
|
|
2263
|
-
state.nextRetryAtMs = undefined;
|
|
2264
|
-
state.nextRetryAttemptCount = undefined;
|
|
2265
|
-
state.nextRetryFailureClass = undefined;
|
|
2266
|
-
state.failedUpdateId = undefined;
|
|
2267
|
-
state.failedFailureId = undefined;
|
|
2268
|
-
state.failedAttemptCount = undefined;
|
|
2269
|
-
state.failedClass = undefined;
|
|
2270
|
-
state.failedSummary = undefined;
|
|
2271
|
-
state.terminalFailureAtMs = undefined;
|
|
2272
|
-
state.unsettledExecutionCount = unsettledExecutions.size;
|
|
2850
|
+
clearTelegramUpdateWorkerClaimState(state, unsettledExecutions.size);
|
|
2273
2851
|
state.lastCompletedUpdateId = undefined;
|
|
2274
2852
|
state.lastCompletedAtMs = undefined;
|
|
2275
2853
|
state.lastFailureAtMs = undefined;
|
|
@@ -2277,6 +2855,401 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
2277
2855
|
notifyStateChange();
|
|
2278
2856
|
launchDrain();
|
|
2279
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
|
+
},
|
|
2280
3253
|
signal() {
|
|
2281
3254
|
if (!owner || owner.controller.signal.aborted)
|
|
2282
3255
|
return;
|
|
@@ -2284,51 +3257,294 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
2284
3257
|
pendingSignal = true;
|
|
2285
3258
|
launchDrain();
|
|
2286
3259
|
},
|
|
2287
|
-
armRoutingInput(input) {
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
3260
|
+
armRoutingInput(input) {
|
|
3261
|
+
return mutateRoutingSource(input, false)?.lifetime;
|
|
3262
|
+
},
|
|
3263
|
+
selectRoutingInput(input) {
|
|
3264
|
+
return mutateRoutingSource(input, true)?.issued === true;
|
|
3265
|
+
},
|
|
3266
|
+
isRoutingInputCurrent(input) {
|
|
3267
|
+
return (owner?.controller.signal === input.signal &&
|
|
3268
|
+
!input.signal.aborted &&
|
|
3269
|
+
deferredSources.has(input.updateId) &&
|
|
3270
|
+
claims.get(input.updateId) !== "abandoning");
|
|
3271
|
+
},
|
|
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,
|
|
2299
3323
|
};
|
|
2300
|
-
|
|
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())
|
|
2301
3345
|
return undefined;
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
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
|
+
});
|
|
2306
3355
|
return undefined;
|
|
2307
|
-
|
|
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
|
+
});
|
|
2308
3500
|
},
|
|
2309
3501
|
isHistoricalSource(input) {
|
|
2310
3502
|
const source = deferredSources.get(input.updateId);
|
|
2311
|
-
const isCurrent = () => owner?.controller.signal === input.signal &&
|
|
2312
|
-
|
|
3503
|
+
const isCurrent = () => owner?.controller.signal === input.signal &&
|
|
3504
|
+
!input.signal.aborted &&
|
|
3505
|
+
source?.historical === true &&
|
|
3506
|
+
deferredSources.get(input.updateId) === source;
|
|
2313
3507
|
if (!isCurrent())
|
|
2314
3508
|
return false;
|
|
2315
3509
|
// Domain eligibility observes original evidence, never mutable handler projections.
|
|
2316
|
-
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;
|
|
2317
3521
|
},
|
|
2318
3522
|
supportsDeferredAbandonment(input) {
|
|
2319
3523
|
const expectedOwner = owner;
|
|
2320
3524
|
const source = deferredSources.get(input.updateId);
|
|
2321
|
-
if (!expectedOwner ||
|
|
2322
|
-
|
|
2323
|
-
|
|
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 ||
|
|
2324
3533
|
claims.get(input.updateId) === "queued")
|
|
2325
3534
|
return false;
|
|
2326
|
-
return deps.isContextCurrent?.(expectedOwner.ctx) !== false &&
|
|
2327
|
-
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");
|
|
2328
3547
|
},
|
|
2329
|
-
abandonDeferred(input) { return abandonSource(input); },
|
|
2330
|
-
inspectAbandoning(input) { return inspectRecovery(input, "abandoning"); },
|
|
2331
|
-
inspectHistorical(input) { return inspectRecovery(input, "historical"); },
|
|
2332
3548
|
async settleDeferred(input) {
|
|
2333
3549
|
const expectedOwner = owner;
|
|
2334
3550
|
if (!expectedOwner ||
|
|
@@ -2346,7 +3562,12 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
2346
3562
|
return;
|
|
2347
3563
|
}
|
|
2348
3564
|
const claim = claims.get(input.updateId);
|
|
2349
|
-
if (
|
|
3565
|
+
if (livePreparation?.ids.has(input.updateId))
|
|
3566
|
+
return;
|
|
3567
|
+
if (claim === "abandoning" ||
|
|
3568
|
+
claim === "historical" ||
|
|
3569
|
+
claim === "retained" ||
|
|
3570
|
+
(!claim && routingJournal?.inspectExpiry(input.updateId)))
|
|
2350
3571
|
return;
|
|
2351
3572
|
if (input.outcome.kind === "complete") {
|
|
2352
3573
|
// Expiry may retire only a still-deferred source, never accepted queue work.
|
|
@@ -2362,10 +3583,27 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
2362
3583
|
}
|
|
2363
3584
|
if (completion.kind !== "complete")
|
|
2364
3585
|
return;
|
|
2365
|
-
const
|
|
2366
|
-
|
|
2367
|
-
|
|
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) {
|
|
2368
3603
|
transition("idle", input.updateId);
|
|
3604
|
+
if (source && isCurrent())
|
|
3605
|
+
return { source, isCurrent };
|
|
3606
|
+
}
|
|
2369
3607
|
return;
|
|
2370
3608
|
}
|
|
2371
3609
|
if (input.outcome.kind === "deferred") {
|
|
@@ -2380,7 +3618,14 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
2380
3618
|
}
|
|
2381
3619
|
let outcome;
|
|
2382
3620
|
try {
|
|
2383
|
-
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
|
+
]));
|
|
2384
3629
|
}
|
|
2385
3630
|
catch (error) {
|
|
2386
3631
|
blockWithFailure("invalid-outcome", "late-invalid-outcome", error, input.updateId);
|
|
@@ -2408,7 +3653,9 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
2408
3653
|
},
|
|
2409
3654
|
async settleCustodied(input) {
|
|
2410
3655
|
const expectedOwner = owner;
|
|
2411
|
-
if (!expectedOwner ||
|
|
3656
|
+
if (!expectedOwner ||
|
|
3657
|
+
expectedOwner.controller.signal !== input.signal ||
|
|
3658
|
+
input.signal.aborted)
|
|
2412
3659
|
return;
|
|
2413
3660
|
if (checkAuthority(expectedOwner, input.updateId))
|
|
2414
3661
|
return;
|
|
@@ -2432,7 +3679,8 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
2432
3679
|
}
|
|
2433
3680
|
const receipt = normalizeQueueReceipt(input.result.queueReceipt);
|
|
2434
3681
|
const existing = committedQueueReceipts.get(receipt.receiptId);
|
|
2435
|
-
if (existing &&
|
|
3682
|
+
if (existing &&
|
|
3683
|
+
areTelegramQueueAdmissionReceiptsEqual(existing.receipt, receipt) &&
|
|
2436
3684
|
areTelegramUpdateJournalQueueOwnersEqual(existing.queueOwner, input.result.queueReceipt.queueOwner))
|
|
2437
3685
|
return;
|
|
2438
3686
|
if (existing) {
|
|
@@ -2440,7 +3688,7 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
2440
3688
|
return;
|
|
2441
3689
|
}
|
|
2442
3690
|
if (!receipt.sourceUpdateIds.includes(input.updateId) ||
|
|
2443
|
-
receipt.sourceUpdateIds.some(updateId => claims.get(updateId) !== "deferred")) {
|
|
3691
|
+
receipt.sourceUpdateIds.some((updateId) => claims.get(updateId) !== "deferred")) {
|
|
2444
3692
|
blockWithFailure("invalid-outcome", "late-custody-unclaimed", new TelegramUpdateAdmissionOutcomeError(`Telegram update ${input.updateId} reported custodied queue without exact deferred claims.`), input.updateId);
|
|
2445
3693
|
return;
|
|
2446
3694
|
}
|
|
@@ -2459,21 +3707,29 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
2459
3707
|
}
|
|
2460
3708
|
transition("queued", input.updateId);
|
|
2461
3709
|
},
|
|
2462
|
-
isQueueReceiptCommitted: receipt => getCurrentQueueReceipt(receipt) !== undefined,
|
|
3710
|
+
isQueueReceiptCommitted: (receipt) => getCurrentQueueReceipt(receipt) !== undefined,
|
|
2463
3711
|
getQueueReceiptOwner(receipt) {
|
|
2464
3712
|
const committed = getCurrentQueueReceipt(receipt);
|
|
2465
3713
|
return committed ? { ...committed.queueOwner } : undefined;
|
|
2466
3714
|
},
|
|
2467
3715
|
getQueueReceiptSettlementOwner(receipt, ctx, reason) {
|
|
2468
3716
|
const expectedOwner = owner, committed = committedQueueReceipts.get(receipt.receiptId);
|
|
2469
|
-
if (!expectedOwner ||
|
|
2470
|
-
|
|
2471
|
-
!
|
|
2472
|
-
(
|
|
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()) ||
|
|
2473
3726
|
!isTelegramUpdateJournalQueueOwnerProcess(committed.queueOwner, expectedOwner.queueOwnerIdentity))
|
|
2474
3727
|
return undefined;
|
|
2475
3728
|
const attempt = scopedQueueCompletionAttempts.get(receipt.receiptId);
|
|
2476
|
-
if (attempt
|
|
3729
|
+
if (attempt
|
|
3730
|
+
? attempt.owner !== expectedOwner ||
|
|
3731
|
+
attempt.reason !== reason ||
|
|
3732
|
+
attempt.journalBindingKey !== deps.getJournalBindingKey?.()
|
|
2477
3733
|
: !getCurrentQueueReceipt(receipt))
|
|
2478
3734
|
return undefined;
|
|
2479
3735
|
return { ...committed.queueOwner };
|
|
@@ -2486,7 +3742,9 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
2486
3742
|
return false;
|
|
2487
3743
|
}
|
|
2488
3744
|
// An ACK permits only local completion reconciliation, never execution readiness.
|
|
2489
|
-
const receipts = input.sourceCompletions === undefined
|
|
3745
|
+
const receipts = input.sourceCompletions === undefined
|
|
3746
|
+
? input.receipts.filter((receipt) => !isQueueReceiptCompletionAcknowledged(receipt))
|
|
3747
|
+
: input.receipts;
|
|
2490
3748
|
if (receipts.length === 0)
|
|
2491
3749
|
return input.sourceCompletions === undefined;
|
|
2492
3750
|
const normalizedReceipts = receipts.map(normalizeQueueReceipt);
|
|
@@ -2514,83 +3772,138 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
2514
3772
|
}
|
|
2515
3773
|
}
|
|
2516
3774
|
const completionBinding = deps.getJournalBindingKey?.();
|
|
2517
|
-
const ordinaryObserverBound = normalizedReceipts.every(receipt => committedQueueReceipts.get(receipt.receiptId).receipt
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
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;
|
|
2521
3784
|
let removedUpdateIds;
|
|
2522
3785
|
let scopedCompletionCurrent;
|
|
2523
3786
|
try {
|
|
2524
|
-
const prepared = normalizedReceipts.flatMap(receipt => committedQueueReceipts.get(receipt.receiptId).sourceCompletions ??
|
|
2525
|
-
|
|
2526
|
-
|
|
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)))) {
|
|
2527
3796
|
throw new Error("Telegram queued completion conflicts with its prepared source scopes.");
|
|
2528
3797
|
}
|
|
2529
|
-
if (input.sourceCompletions === undefined &&
|
|
2530
|
-
|
|
2531
|
-
|
|
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));
|
|
2532
3803
|
if (!scoped.length || !ordinary.length)
|
|
2533
3804
|
throw new Error("Telegram mixed queue completion has incomplete scoped authority.");
|
|
2534
3805
|
const binding = deps.getJournalBindingKey?.();
|
|
2535
|
-
const current = () => owner === expectedOwner &&
|
|
2536
|
-
|
|
2537
|
-
|
|
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;
|
|
2538
3813
|
// Independent whole receipts need independent ACKs. Ordinary siblings must not acquire invented scopes.
|
|
2539
|
-
if (!current() ||
|
|
3814
|
+
if (!current() ||
|
|
3815
|
+
!completeQueueReceipts({
|
|
3816
|
+
receipts: scoped,
|
|
3817
|
+
ctx: input.ctx,
|
|
3818
|
+
reason: input.reason,
|
|
3819
|
+
}) ||
|
|
3820
|
+
!current())
|
|
2540
3821
|
return false;
|
|
2541
|
-
return completeQueueReceipts({
|
|
3822
|
+
return completeQueueReceipts({
|
|
3823
|
+
receipts: ordinary,
|
|
3824
|
+
ctx: input.ctx,
|
|
3825
|
+
reason: input.reason,
|
|
3826
|
+
});
|
|
2542
3827
|
}
|
|
2543
3828
|
if (sourceCompletions === undefined) {
|
|
2544
|
-
if (queuedCompletions.some(receipt => requiredScopedQueueReceipts.has(receipt.receiptId))) {
|
|
3829
|
+
if (queuedCompletions.some((receipt) => requiredScopedQueueReceipts.has(receipt.receiptId))) {
|
|
2545
3830
|
throw new Error("Telegram scoped queue completion cannot downgrade required source scopes.");
|
|
2546
3831
|
}
|
|
2547
|
-
removedUpdateIds =
|
|
3832
|
+
removedUpdateIds =
|
|
3833
|
+
deps.journal.completeQueued(queuedCompletions).removedUpdateIds;
|
|
2548
3834
|
}
|
|
2549
3835
|
else {
|
|
2550
3836
|
for (const receipt of queuedCompletions)
|
|
2551
3837
|
requiredScopedQueueReceipts.add(receipt.receiptId);
|
|
2552
3838
|
queuedCompletions.sort((a, b) => a.receiptId < b.receiptId ? -1 : a.receiptId > b.receiptId ? 1 : 0);
|
|
2553
3839
|
const journalBindingKey = deps.getJournalBindingKey?.();
|
|
2554
|
-
const current = () => owner === expectedOwner &&
|
|
2555
|
-
|
|
3840
|
+
const current = () => owner === expectedOwner &&
|
|
3841
|
+
!expectedOwner.controller.signal.aborted &&
|
|
3842
|
+
(deps.isContextCurrent?.(input.ctx) ??
|
|
3843
|
+
expectedOwner.ctx === input.ctx) &&
|
|
3844
|
+
isQueueOwnerIdentityCurrent(expectedOwner) &&
|
|
2556
3845
|
deps.getJournalBindingKey?.() === journalBindingKey;
|
|
2557
|
-
if (!journalBindingKey ||
|
|
2558
|
-
!
|
|
3846
|
+
if (!journalBindingKey ||
|
|
3847
|
+
!current() ||
|
|
3848
|
+
!completeQueuedExact ||
|
|
3849
|
+
!inspectCompletion ||
|
|
3850
|
+
!Array.isArray(sourceCompletions) ||
|
|
3851
|
+
(input.sourceCompletions !== undefined &&
|
|
3852
|
+
sourceCompletions.length !== sourceUpdateIds.size)) {
|
|
2559
3853
|
throw new Error("Telegram scoped queue completion requires full current source authority.");
|
|
2560
3854
|
}
|
|
2561
3855
|
scopedCompletionCurrent = current;
|
|
2562
|
-
const completions = normalizeTelegramQueueSourceCompletions(sourceCompletions, sourceUpdateIds, journalBindingKey, input.sourceCompletions !== undefined)
|
|
2563
|
-
|
|
2564
|
-
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)))) {
|
|
2565
3858
|
throw new Error("Telegram scoped queue completion requires a proven origin for every whole receipt.");
|
|
2566
3859
|
}
|
|
2567
|
-
const previous = queuedCompletions.map(receipt => scopedQueueCompletionAttempts.get(receipt.receiptId));
|
|
2568
|
-
const retained = previous.find(attempt => attempt !== undefined);
|
|
2569
|
-
if (retained &&
|
|
2570
|
-
|
|
2571
|
-
|
|
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))) {
|
|
2572
3869
|
throw new Error("Telegram scoped queue completion has conflicting issued authority.");
|
|
2573
3870
|
}
|
|
2574
3871
|
if (!retained) {
|
|
2575
|
-
const attempt = {
|
|
2576
|
-
|
|
2577
|
-
|
|
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
|
+
};
|
|
2578
3883
|
if (!current())
|
|
2579
3884
|
throw new Error("Telegram scoped queue completion authority changed before disposal.");
|
|
2580
3885
|
// Mark before issuance: an exception may follow a committed rename, and cannot license another disposal.
|
|
2581
3886
|
for (const receipt of queuedCompletions)
|
|
2582
3887
|
scopedQueueCompletionAttempts.set(receipt.receiptId, attempt);
|
|
2583
|
-
const result = completeQueuedExact(queuedCompletions.map(
|
|
2584
|
-
|
|
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)) {
|
|
2585
3895
|
throw new Error("Telegram scoped queue completion ACK was not confirmed.");
|
|
2586
3896
|
}
|
|
2587
3897
|
const removed = new Set(result.removedUpdateIds);
|
|
2588
|
-
if (removed.size !== sourceUpdateIds.size ||
|
|
3898
|
+
if (removed.size !== sourceUpdateIds.size ||
|
|
3899
|
+
[...sourceUpdateIds].some((id) => !removed.has(id))) {
|
|
2589
3900
|
throw new Error("Telegram scoped queue completion did not remove every source.");
|
|
2590
3901
|
}
|
|
2591
3902
|
}
|
|
2592
3903
|
for (const completion of completions) {
|
|
2593
|
-
if (!current() ||
|
|
3904
|
+
if (!current() ||
|
|
3905
|
+
!isDeepStrictEqual(inspectCompletion({ ...completion }), completion) ||
|
|
3906
|
+
!current()) {
|
|
2594
3907
|
throw new Error("Telegram scoped queue completion ACK was not retained under current authority.");
|
|
2595
3908
|
}
|
|
2596
3909
|
}
|
|
@@ -2633,7 +3946,10 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
2633
3946
|
observeQueuedCompletion?.(normalizeQueueReceipt(receipt), input.ctx);
|
|
2634
3947
|
}
|
|
2635
3948
|
catch (error) {
|
|
2636
|
-
recordRuntimeEvent(error, {
|
|
3949
|
+
recordRuntimeEvent(error, {
|
|
3950
|
+
phase: "queue-receipt-completion-observer",
|
|
3951
|
+
receiptId: receipt.receiptId,
|
|
3952
|
+
});
|
|
2637
3953
|
}
|
|
2638
3954
|
}
|
|
2639
3955
|
// Ordinary queued disposal is source completion too; notify only after a whole positive ACK and fresh origin authority.
|
|
@@ -2645,7 +3961,10 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
2645
3961
|
deps.onUpdateCompleted?.(updateId, input.ctx, completionBinding);
|
|
2646
3962
|
}
|
|
2647
3963
|
catch (error) {
|
|
2648
|
-
recordRuntimeEvent(error, {
|
|
3964
|
+
recordRuntimeEvent(error, {
|
|
3965
|
+
phase: "queue-source-completion-observer",
|
|
3966
|
+
updateId,
|
|
3967
|
+
});
|
|
2649
3968
|
}
|
|
2650
3969
|
}
|
|
2651
3970
|
if (!scopedCompletionCurrent || scopedCompletionCurrent()) {
|
|
@@ -2666,6 +3985,8 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
2666
3985
|
if (owner !== expectedOwner)
|
|
2667
3986
|
return;
|
|
2668
3987
|
owner = undefined;
|
|
3988
|
+
livePreparation = undefined;
|
|
3989
|
+
delete state.preparedInputCount;
|
|
2669
3990
|
drainPromise = undefined;
|
|
2670
3991
|
claims.clear();
|
|
2671
3992
|
deferredSources.clear();
|
|
@@ -2675,30 +3996,12 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
2675
3996
|
state.phaseStartedAtMs = getNowMs();
|
|
2676
3997
|
state.currentUpdateId = undefined;
|
|
2677
3998
|
state.blockedReason = undefined;
|
|
2678
|
-
state.
|
|
2679
|
-
delete state.abandoningClaimCount;
|
|
2680
|
-
delete state.historicalClaimCount;
|
|
2681
|
-
state.queuedClaimCount = 0;
|
|
2682
|
-
state.foreignQueuedCount = 0;
|
|
2683
|
-
delete state.foreignQueuedOwner;
|
|
2684
|
-
state.retryWaitCount = 0;
|
|
2685
|
-
state.failedCount = 0;
|
|
2686
|
-
state.nextRetryUpdateId = undefined;
|
|
2687
|
-
state.nextRetryAtMs = undefined;
|
|
2688
|
-
state.nextRetryAttemptCount = undefined;
|
|
2689
|
-
state.nextRetryFailureClass = undefined;
|
|
2690
|
-
state.failedUpdateId = undefined;
|
|
2691
|
-
state.failedFailureId = undefined;
|
|
2692
|
-
state.failedAttemptCount = undefined;
|
|
2693
|
-
state.failedClass = undefined;
|
|
2694
|
-
state.failedSummary = undefined;
|
|
2695
|
-
state.terminalFailureAtMs = undefined;
|
|
2696
|
-
state.unsettledExecutionCount = unsettledExecutions.size;
|
|
3999
|
+
clearTelegramUpdateWorkerClaimState(state, unsettledExecutions.size);
|
|
2697
4000
|
notifyStateChange();
|
|
2698
4001
|
},
|
|
2699
4002
|
async waitForDrain() {
|
|
2700
4003
|
await (drainPromise ?? Promise.resolve());
|
|
2701
|
-
await Promise.allSettled([...pendingQueuePublications.values()].map(value => value.task));
|
|
4004
|
+
await Promise.allSettled([...pendingQueuePublications.values()].map((value) => value.task));
|
|
2702
4005
|
},
|
|
2703
4006
|
getState() {
|
|
2704
4007
|
return getTelegramUpdateWorkerStateSnapshot(state);
|
|
@@ -2791,19 +4094,6 @@ function getOrCreateUpdateHandlerRegistry() {
|
|
|
2791
4094
|
export function getTelegramUpdateHandlerRegistry() {
|
|
2792
4095
|
return getOrCreateUpdateHandlerRegistry();
|
|
2793
4096
|
}
|
|
2794
|
-
/**
|
|
2795
|
-
* Wrap a default polling `handleUpdate` with the public update handler registry.
|
|
2796
|
-
*/
|
|
2797
|
-
export function createTelegramUpdateHandle(deps) {
|
|
2798
|
-
const registry = deps.registry ?? getOrCreateUpdateHandlerRegistry();
|
|
2799
|
-
const { defaultHandle } = deps;
|
|
2800
|
-
return async (update, ctx) => {
|
|
2801
|
-
const verdict = await registry.dispatch(update);
|
|
2802
|
-
if (verdict === "consume")
|
|
2803
|
-
return;
|
|
2804
|
-
await defaultHandle(update, ctx);
|
|
2805
|
-
};
|
|
2806
|
-
}
|
|
2807
4097
|
function mergeTelegramReportedAdmissionOutcome(current, next, updateId) {
|
|
2808
4098
|
if (!current || current.kind === "deferred")
|
|
2809
4099
|
return next;
|
|
@@ -2815,16 +4105,20 @@ function mergeTelegramReportedAdmissionOutcome(current, next, updateId) {
|
|
|
2815
4105
|
if (!next.expectedSource)
|
|
2816
4106
|
return current;
|
|
2817
4107
|
if (current.expectedSource.updateId === next.expectedSource.updateId &&
|
|
2818
|
-
current.expectedSource.journalBindingKey ===
|
|
4108
|
+
current.expectedSource.journalBindingKey ===
|
|
4109
|
+
next.expectedSource.journalBindingKey &&
|
|
2819
4110
|
current.expectedSource.sourceSha256 === next.expectedSource.sourceSha256) {
|
|
2820
4111
|
if (!current.expectedSource.completionSha256)
|
|
2821
4112
|
return next;
|
|
2822
|
-
if (!next.expectedSource.completionSha256 ||
|
|
4113
|
+
if (!next.expectedSource.completionSha256 ||
|
|
4114
|
+
current.expectedSource.completionSha256 ===
|
|
4115
|
+
next.expectedSource.completionSha256)
|
|
2823
4116
|
return current;
|
|
2824
4117
|
}
|
|
2825
4118
|
throw new TelegramUpdateAdmissionOutcomeError(`Telegram update ${updateId} reported conflicting completion sources.`);
|
|
2826
4119
|
}
|
|
2827
|
-
if (current.kind === "queued" &&
|
|
4120
|
+
if (current.kind === "queued" &&
|
|
4121
|
+
next.kind === "queued" &&
|
|
2828
4122
|
areTelegramQueueAdmissionReceiptsEqual(current, next))
|
|
2829
4123
|
return current;
|
|
2830
4124
|
throw new TelegramUpdateAdmissionOutcomeError(`Telegram update ${updateId} reported conflicting queue outcomes.`);
|
|
@@ -2836,17 +4130,23 @@ export function createTelegramInputCustodyWorkerJournalPort(store) {
|
|
|
2836
4130
|
return {
|
|
2837
4131
|
read: () => store.read(),
|
|
2838
4132
|
isQueueReceiptCurrent(receipt, owner) {
|
|
2839
|
-
const entries = store
|
|
2840
|
-
|
|
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 &&
|
|
2841
4138
|
entries.every((entry, index) => entry.updateId === receipt.sourceUpdateIds[index] &&
|
|
2842
|
-
entry.state === "queued" &&
|
|
2843
|
-
entry.
|
|
4139
|
+
entry.state === "queued" &&
|
|
4140
|
+
entry.queueKind === receipt.queueKind &&
|
|
4141
|
+
!entry.queueHandoff &&
|
|
4142
|
+
entry.queueOwner !== undefined &&
|
|
4143
|
+
areTelegramUpdateJournalQueueOwnersEqual(entry.queueOwner, owner)));
|
|
2844
4144
|
},
|
|
2845
4145
|
markQueued: legacyMutation,
|
|
2846
4146
|
markExecutionFailure: legacyMutation,
|
|
2847
4147
|
// The common drain batches excluded IDs here; the store rejects all non-vetoed input atomically.
|
|
2848
|
-
removeCompleted: updateIds => store.removeExcluded(updateIds),
|
|
2849
|
-
completeQueued: receipts => store.completeQueued(receipts),
|
|
4148
|
+
removeCompleted: (updateIds) => store.removeExcluded(updateIds),
|
|
4149
|
+
completeQueued: (receipts) => store.completeQueued(receipts),
|
|
2850
4150
|
inputCustody: {
|
|
2851
4151
|
acquireInput: store.acquireInput,
|
|
2852
4152
|
startInput: store.startInput,
|
|
@@ -2859,7 +4159,7 @@ export function createTelegramInputCustodyLegacyDispositionRuntime(deps) {
|
|
|
2859
4159
|
const withBinding = (recoveryKey, operation) => {
|
|
2860
4160
|
if (!recoveryKey)
|
|
2861
4161
|
throw new TelegramUpdateAdmissionOutcomeError("Telegram legacy custody disposition binding is unavailable.");
|
|
2862
|
-
return deps.withBindingReference(recoveryKey, binding => {
|
|
4162
|
+
return deps.withBindingReference(recoveryKey, (binding) => {
|
|
2863
4163
|
if (binding.recoveryKey !== recoveryKey)
|
|
2864
4164
|
throw new TelegramUpdateAdmissionOutcomeError("Telegram legacy custody disposition binding is unavailable.");
|
|
2865
4165
|
return operation(binding.journal);
|
|
@@ -2867,10 +4167,10 @@ export function createTelegramInputCustodyLegacyDispositionRuntime(deps) {
|
|
|
2867
4167
|
};
|
|
2868
4168
|
return {
|
|
2869
4169
|
list(recoveryKey) {
|
|
2870
|
-
return withBinding(recoveryKey, journal => journal.listLegacyCustodyCandidates());
|
|
4170
|
+
return withBinding(recoveryKey, (journal) => journal.listLegacyCustodyCandidates());
|
|
2871
4171
|
},
|
|
2872
4172
|
apply(recoveryKey, authority) {
|
|
2873
|
-
return withBinding(recoveryKey, journal => journal.applyLegacyCustodyDisposition(authority));
|
|
4173
|
+
return withBinding(recoveryKey, (journal) => journal.applyLegacyCustodyDisposition(authority));
|
|
2874
4174
|
},
|
|
2875
4175
|
};
|
|
2876
4176
|
}
|
|
@@ -2879,44 +4179,62 @@ export function createTelegramInputCustodyHandoffClient(deps) {
|
|
|
2879
4179
|
async transfer(input) {
|
|
2880
4180
|
const reconciled = deps.resolveAcceptedReference?.({
|
|
2881
4181
|
sourceUpdateId: input.receipt.updateId,
|
|
2882
|
-
recipientBindingKey: input.recipientBindingKey
|
|
4182
|
+
recipientBindingKey: input.recipientBindingKey,
|
|
2883
4183
|
});
|
|
2884
4184
|
if (reconciled)
|
|
2885
4185
|
return { ...reconciled, duplicate: true };
|
|
2886
|
-
const offered = deps.journal.offerInputHandoff({
|
|
2887
|
-
|
|
4186
|
+
const offered = deps.journal.offerInputHandoff({
|
|
4187
|
+
receipt: input.receipt,
|
|
4188
|
+
recipientOwner: input.recipientOwner,
|
|
4189
|
+
handoffToken: input.handoffToken,
|
|
4190
|
+
});
|
|
2888
4191
|
const envelope = {
|
|
2889
|
-
kind: "leader.offerInputCustodyHandoff",
|
|
4192
|
+
kind: "leader.offerInputCustodyHandoff",
|
|
4193
|
+
requestId: input.requestId,
|
|
2890
4194
|
recipientInstanceId: input.recipientInstanceId,
|
|
2891
4195
|
recipientRegistrationGeneration: input.recipientRegistrationGeneration,
|
|
2892
4196
|
recipientBindingKey: input.recipientBindingKey,
|
|
2893
4197
|
sourceRecoveryKey: offered.source.journalBindingKey,
|
|
2894
|
-
source: offered.source,
|
|
2895
|
-
|
|
4198
|
+
source: offered.source,
|
|
4199
|
+
handoffId: offered.handoff.handoffId,
|
|
4200
|
+
sentAtMs: input.sentAtMs,
|
|
4201
|
+
...(input.auth ? { auth: input.auth } : {}),
|
|
2896
4202
|
};
|
|
2897
4203
|
const response = await deps.sendEnvelope(envelope);
|
|
2898
|
-
if (response?.kind !== "bus.ack" ||
|
|
2899
|
-
|
|
4204
|
+
if (response?.kind !== "bus.ack" ||
|
|
4205
|
+
response.requestId !== input.requestId ||
|
|
4206
|
+
!response.ok ||
|
|
4207
|
+
!response.result ||
|
|
4208
|
+
typeof response.result !== "object" ||
|
|
2900
4209
|
Array.isArray(response.result))
|
|
2901
4210
|
throw new Error("Telegram input custody handoff acknowledgement is missing or rejected.");
|
|
2902
4211
|
const result = response.result;
|
|
2903
4212
|
const source = result.source;
|
|
2904
4213
|
if (result.sourceRecoveryKey !== offered.source.journalBindingKey ||
|
|
2905
|
-
!source ||
|
|
2906
|
-
source
|
|
4214
|
+
!source ||
|
|
4215
|
+
typeof source !== "object" ||
|
|
4216
|
+
Array.isArray(source) ||
|
|
4217
|
+
source.updateId !==
|
|
4218
|
+
offered.source.updateId ||
|
|
2907
4219
|
!source.owner ||
|
|
2908
4220
|
typeof source.owner !== "object" ||
|
|
2909
4221
|
Array.isArray(source.owner) ||
|
|
2910
4222
|
typeof source.owner.acquisitionId !== "string" ||
|
|
2911
|
-
source.owner
|
|
4223
|
+
source.owner
|
|
4224
|
+
.handoffId !== offered.handoff.handoffId ||
|
|
2912
4225
|
typeof result.duplicate !== "boolean")
|
|
2913
4226
|
throw new Error("Telegram input custody handoff acknowledgement returned mismatched authority.");
|
|
2914
|
-
return {
|
|
2915
|
-
|
|
4227
|
+
return {
|
|
4228
|
+
sourceRecoveryKey: result.sourceRecoveryKey,
|
|
4229
|
+
source: {
|
|
4230
|
+
updateId: source.updateId,
|
|
4231
|
+
owner: {
|
|
2916
4232
|
acquisitionId: source.owner.acquisitionId,
|
|
2917
|
-
handoffId: source.owner.handoffId
|
|
2918
|
-
}
|
|
2919
|
-
|
|
4233
|
+
handoffId: source.owner.handoffId,
|
|
4234
|
+
},
|
|
4235
|
+
},
|
|
4236
|
+
duplicate: result.duplicate,
|
|
4237
|
+
};
|
|
2920
4238
|
},
|
|
2921
4239
|
};
|
|
2922
4240
|
}
|
|
@@ -2924,56 +4242,88 @@ export function createTelegramInputCustodyHandoffAcceptanceRuntime(deps) {
|
|
|
2924
4242
|
return {
|
|
2925
4243
|
accept(input, ctx) {
|
|
2926
4244
|
const binding = deps.resolveBinding(input.sourceRecoveryKey);
|
|
2927
|
-
if (!binding ||
|
|
4245
|
+
if (!binding ||
|
|
4246
|
+
binding.recoveryKey !== input.sourceRecoveryKey ||
|
|
2928
4247
|
binding.recipientBindingKey !== input.recipientBindingKey ||
|
|
2929
4248
|
input.source.journalBindingKey !== input.sourceRecoveryKey)
|
|
2930
4249
|
throw new Error("Telegram input custody handoff binding is unavailable or changed.");
|
|
2931
|
-
const accepted = binding.journal.acceptInputHandoff({
|
|
2932
|
-
|
|
4250
|
+
const accepted = binding.journal.acceptInputHandoff({
|
|
4251
|
+
source: input.source,
|
|
4252
|
+
recipientOwner: binding.recipientOwner,
|
|
4253
|
+
handoffId: input.handoffId,
|
|
4254
|
+
});
|
|
2933
4255
|
const acceptedHandoffId = accepted.receipt.owner.handoffId;
|
|
2934
4256
|
if (!acceptedHandoffId || acceptedHandoffId !== input.handoffId)
|
|
2935
4257
|
throw new Error("Telegram input custody handoff acceptance returned mismatched authority.");
|
|
2936
4258
|
binding.signalWorker(ctx);
|
|
2937
|
-
return {
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
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
|
+
};
|
|
2941
4270
|
},
|
|
2942
4271
|
};
|
|
2943
4272
|
}
|
|
2944
4273
|
export function createTelegramInputCustodyForwardReferenceResolver(deps) {
|
|
2945
|
-
return input => {
|
|
4274
|
+
return (input) => {
|
|
2946
4275
|
if (input.recipientBindingKey !== deps.recipientBindingKey)
|
|
2947
4276
|
return undefined;
|
|
2948
|
-
const entry = deps.journal
|
|
4277
|
+
const entry = deps.journal
|
|
4278
|
+
.read()
|
|
4279
|
+
.entries.find((candidate) => candidate.updateId === input.sourceUpdateId);
|
|
2949
4280
|
const claim = entry?.inputClaim;
|
|
2950
|
-
if (!entry ||
|
|
2951
|
-
|
|
4281
|
+
if (!entry ||
|
|
4282
|
+
entry.state !== "pending" ||
|
|
4283
|
+
entry.preApprovalExcluded !== false ||
|
|
4284
|
+
!claim ||
|
|
4285
|
+
claim.phase !== "ready" ||
|
|
4286
|
+
claim.handoff ||
|
|
2952
4287
|
claim.recipientBindingKey !== deps.recipientBindingKey ||
|
|
2953
4288
|
!claim.owner.handoffId ||
|
|
2954
4289
|
claim.owner.sessionGeneration !== deps.recipientOwner.sessionGeneration ||
|
|
2955
4290
|
!isTelegramUpdateJournalQueueOwnerProcess(claim.owner, deps.recipientOwner))
|
|
2956
4291
|
return undefined;
|
|
2957
|
-
return {
|
|
2958
|
-
|
|
2959
|
-
|
|
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
|
+
};
|
|
2960
4302
|
};
|
|
2961
4303
|
}
|
|
2962
4304
|
export function createTelegramInputCustodySourceReferenceWakeRuntime(deps) {
|
|
2963
4305
|
return {
|
|
2964
4306
|
wakeSource(input, ctx) {
|
|
2965
4307
|
const binding = deps.resolveBinding(input.sourceRecoveryKey);
|
|
2966
|
-
if (!binding ||
|
|
4308
|
+
if (!binding ||
|
|
4309
|
+
binding.recoveryKey !== input.sourceRecoveryKey ||
|
|
2967
4310
|
binding.recipientBindingKey !== input.recipientBindingKey)
|
|
2968
4311
|
throw new Error("Telegram follower source-reference binding is unavailable or changed.");
|
|
2969
|
-
const entry = binding.journal
|
|
4312
|
+
const entry = binding.journal
|
|
4313
|
+
.read()
|
|
4314
|
+
.entries.find((candidate) => candidate.updateId === input.sourceUpdateId);
|
|
2970
4315
|
const claim = entry?.inputClaim;
|
|
2971
|
-
if (!entry ||
|
|
2972
|
-
|
|
4316
|
+
if (!entry ||
|
|
4317
|
+
entry.state !== "pending" ||
|
|
4318
|
+
entry.preApprovalExcluded !== false ||
|
|
4319
|
+
!claim ||
|
|
4320
|
+
claim.phase !== "ready" ||
|
|
4321
|
+
claim.handoff ||
|
|
2973
4322
|
claim.recipientBindingKey !== input.recipientBindingKey ||
|
|
2974
4323
|
claim.owner.acquisitionId !== input.sourceClaim.acquisitionId ||
|
|
2975
4324
|
claim.owner.handoffId !== input.sourceClaim.handoffId ||
|
|
2976
|
-
claim.owner.sessionGeneration !==
|
|
4325
|
+
claim.owner.sessionGeneration !==
|
|
4326
|
+
binding.recipientOwner.sessionGeneration ||
|
|
2977
4327
|
!isTelegramUpdateJournalQueueOwnerProcess(claim.owner, binding.recipientOwner))
|
|
2978
4328
|
throw new Error("Telegram follower source-reference claim is unavailable or changed.");
|
|
2979
4329
|
binding.signalWorker(ctx);
|
|
@@ -2982,10 +4332,10 @@ export function createTelegramInputCustodySourceReferenceWakeRuntime(deps) {
|
|
|
2982
4332
|
}
|
|
2983
4333
|
export function createTelegramInputCustodyBusBindingRuntime(deps) {
|
|
2984
4334
|
const acceptance = createTelegramInputCustodyHandoffAcceptanceRuntime({
|
|
2985
|
-
resolveBinding: deps.resolveBinding
|
|
4335
|
+
resolveBinding: deps.resolveBinding,
|
|
2986
4336
|
});
|
|
2987
4337
|
const wake = createTelegramInputCustodySourceReferenceWakeRuntime({
|
|
2988
|
-
resolveBinding: deps.resolveBinding
|
|
4338
|
+
resolveBinding: deps.resolveBinding,
|
|
2989
4339
|
});
|
|
2990
4340
|
return {
|
|
2991
4341
|
acceptHandoff: acceptance.accept,
|
|
@@ -3015,7 +4365,7 @@ export function evaluateTelegramInputCustodyActivationReadiness(input) {
|
|
|
3015
4365
|
return { enabled: false, blocker: "migration-incomplete" };
|
|
3016
4366
|
if (input.sourceStatus !== "absent" && input.sourceStatus !== "v3")
|
|
3017
4367
|
return { enabled: false, blocker: "source-unready" };
|
|
3018
|
-
if (input.peerReadiness.some(readiness => readiness !== "ready"))
|
|
4368
|
+
if (input.peerReadiness.some((readiness) => readiness !== "ready"))
|
|
3019
4369
|
return { enabled: false, blocker: "peer-capability-mismatch" };
|
|
3020
4370
|
return { enabled: true };
|
|
3021
4371
|
}
|
|
@@ -3035,9 +4385,17 @@ export function createTelegramInputCustodyReadinessEvidenceStore(deps) {
|
|
|
3035
4385
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
3036
4386
|
throw new Error("Telegram custody readiness evidence is malformed.");
|
|
3037
4387
|
const snapshot = value;
|
|
3038
|
-
if (snapshot.version !== 1 ||
|
|
3039
|
-
|
|
3040
|
-
|
|
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)))
|
|
3041
4399
|
throw new Error("Telegram custody readiness evidence is malformed.");
|
|
3042
4400
|
const validate = (evidence, kind) => {
|
|
3043
4401
|
if (evidence === undefined)
|
|
@@ -3045,61 +4403,104 @@ export function createTelegramInputCustodyReadinessEvidenceStore(deps) {
|
|
|
3045
4403
|
if (!evidence || typeof evidence !== "object" || Array.isArray(evidence))
|
|
3046
4404
|
throw new Error("Telegram custody readiness evidence is malformed.");
|
|
3047
4405
|
const record = evidence;
|
|
3048
|
-
const statuses = kind === "writer"
|
|
4406
|
+
const statuses = kind === "writer"
|
|
4407
|
+
? ["excluded", "present", "unknown"]
|
|
3049
4408
|
: ["complete", "incomplete", "unknown"];
|
|
3050
|
-
const writerKeys = [
|
|
3051
|
-
|
|
3052
|
-
"
|
|
4409
|
+
const writerKeys = [
|
|
4410
|
+
"startupAuthorityId",
|
|
4411
|
+
"closureOperationId",
|
|
4412
|
+
"writerInventorySha256",
|
|
4413
|
+
];
|
|
4414
|
+
const migrationKeys = [
|
|
4415
|
+
"migrationAuthorityId",
|
|
4416
|
+
"startupAuthorityId",
|
|
4417
|
+
"closureOperationId",
|
|
4418
|
+
"migrationInventorySha256",
|
|
4419
|
+
"resultingSourceFamily",
|
|
4420
|
+
];
|
|
3053
4421
|
const linkedKeys = kind === "writer" ? writerKeys : migrationKeys;
|
|
3054
|
-
const suppliedLinkedKeys = linkedKeys.filter(key => record[key] !== undefined);
|
|
3055
|
-
const linkedMalformed = suppliedLinkedKeys.length === linkedKeys.length &&
|
|
3056
|
-
typeof record.
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
typeof record.
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
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) ||
|
|
3068
4452
|
linkedMalformed)
|
|
3069
4453
|
throw new Error("Telegram custody readiness evidence is malformed.");
|
|
3070
|
-
return {
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
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
|
+
};
|
|
3083
4475
|
};
|
|
3084
4476
|
const writerExclusion = validate(snapshot.writerExclusion, "writer");
|
|
3085
4477
|
const migration = validate(snapshot.migration, "migration");
|
|
3086
4478
|
let startupExclusion;
|
|
3087
4479
|
if (snapshot.startupExclusion !== undefined) {
|
|
3088
4480
|
const candidate = snapshot.startupExclusion;
|
|
3089
|
-
if (!candidate ||
|
|
4481
|
+
if (!candidate ||
|
|
4482
|
+
typeof candidate.profileKey !== "string" ||
|
|
3090
4483
|
typeof candidate.recoveryKey !== "string")
|
|
3091
4484
|
throw new Error("Telegram custody readiness evidence is malformed.");
|
|
3092
|
-
startupExclusion = normalizeTelegramInputCustodyStartupExclusionAuthority(candidate, {
|
|
4485
|
+
startupExclusion = normalizeTelegramInputCustodyStartupExclusionAuthority(candidate, {
|
|
4486
|
+
profileKey: candidate.profileKey,
|
|
4487
|
+
recoveryKey: candidate.recoveryKey,
|
|
4488
|
+
});
|
|
3093
4489
|
if (!startupExclusion)
|
|
3094
4490
|
throw new Error("Telegram custody readiness evidence is malformed.");
|
|
3095
4491
|
}
|
|
3096
4492
|
let migrationCompletion;
|
|
3097
4493
|
if (snapshot.migrationCompletion !== undefined) {
|
|
3098
4494
|
const candidate = snapshot.migrationCompletion;
|
|
3099
|
-
if (!candidate ||
|
|
4495
|
+
if (!candidate ||
|
|
4496
|
+
typeof candidate.profileKey !== "string" ||
|
|
3100
4497
|
typeof candidate.recoveryKey !== "string")
|
|
3101
4498
|
throw new Error("Telegram custody readiness evidence is malformed.");
|
|
3102
|
-
migrationCompletion =
|
|
4499
|
+
migrationCompletion =
|
|
4500
|
+
normalizeTelegramInputCustodyMigrationCompletionAuthority(candidate, {
|
|
4501
|
+
profileKey: candidate.profileKey,
|
|
4502
|
+
recoveryKey: candidate.recoveryKey,
|
|
4503
|
+
});
|
|
3103
4504
|
if (!migrationCompletion)
|
|
3104
4505
|
throw new Error("Telegram custody readiness evidence is malformed.");
|
|
3105
4506
|
}
|
|
@@ -3112,48 +4513,72 @@ export function createTelegramInputCustodyReadinessEvidenceStore(deps) {
|
|
|
3112
4513
|
identities.push(startupExclusion);
|
|
3113
4514
|
if (migrationCompletion)
|
|
3114
4515
|
identities.push(migrationCompletion);
|
|
3115
|
-
if (identities.some(evidence => evidence.profileKey !== identities[0]?.profileKey ||
|
|
4516
|
+
if (identities.some((evidence) => evidence.profileKey !== identities[0]?.profileKey ||
|
|
3116
4517
|
evidence.recoveryKey !== identities[0]?.recoveryKey))
|
|
3117
4518
|
throw new Error("Telegram custody readiness evidence has conflicting identities.");
|
|
3118
|
-
if (writerExclusion?.startupAuthorityId &&
|
|
4519
|
+
if (writerExclusion?.startupAuthorityId &&
|
|
4520
|
+
startupExclusion &&
|
|
3119
4521
|
(writerExclusion.startupAuthorityId !== startupExclusion.authorityId ||
|
|
3120
|
-
writerExclusion.closureOperationId !==
|
|
3121
|
-
|
|
4522
|
+
writerExclusion.closureOperationId !==
|
|
4523
|
+
startupExclusion.closureOperationId ||
|
|
4524
|
+
writerExclusion.writerInventorySha256 !==
|
|
4525
|
+
startupExclusion.writerInventorySha256))
|
|
3122
4526
|
throw new Error("Telegram custody readiness evidence has conflicting authorities.");
|
|
3123
|
-
if (migration?.migrationAuthorityId &&
|
|
4527
|
+
if (migration?.migrationAuthorityId &&
|
|
4528
|
+
migrationCompletion &&
|
|
3124
4529
|
(migration.migrationAuthorityId !== migrationCompletion.authorityId ||
|
|
3125
|
-
migration.startupAuthorityId !==
|
|
3126
|
-
|
|
3127
|
-
migration.
|
|
3128
|
-
|
|
4530
|
+
migration.startupAuthorityId !==
|
|
4531
|
+
migrationCompletion.startupAuthorityId ||
|
|
4532
|
+
migration.closureOperationId !==
|
|
4533
|
+
migrationCompletion.closureOperationId ||
|
|
4534
|
+
migration.migrationInventorySha256 !==
|
|
4535
|
+
migrationCompletion.migrationInventorySha256 ||
|
|
4536
|
+
migration.resultingSourceFamily !==
|
|
4537
|
+
migrationCompletion.resultingSourceFamily))
|
|
3129
4538
|
throw new Error("Telegram custody readiness evidence has conflicting migration authorities.");
|
|
3130
|
-
if (migrationCompletion &&
|
|
3131
|
-
|
|
3132
|
-
|
|
4539
|
+
if (migrationCompletion &&
|
|
4540
|
+
startupExclusion &&
|
|
4541
|
+
(migrationCompletion.startupAuthorityId !==
|
|
4542
|
+
startupExclusion.authorityId ||
|
|
4543
|
+
migrationCompletion.closureOperationId !==
|
|
4544
|
+
startupExclusion.closureOperationId))
|
|
3133
4545
|
throw new Error("Telegram custody readiness evidence has conflicting cutover authorities.");
|
|
3134
|
-
return {
|
|
4546
|
+
return {
|
|
4547
|
+
version: 1,
|
|
4548
|
+
revision: snapshot.revision,
|
|
3135
4549
|
...(writerExclusion ? { writerExclusion } : {}),
|
|
3136
4550
|
...(migration ? { migration } : {}),
|
|
3137
4551
|
...(startupExclusion ? { startupExclusion } : {}),
|
|
3138
|
-
...(migrationCompletion ? { migrationCompletion } : {})
|
|
4552
|
+
...(migrationCompletion ? { migrationCompletion } : {}),
|
|
4553
|
+
};
|
|
3139
4554
|
};
|
|
3140
4555
|
const read = () => decode(deps.readRetained());
|
|
3141
4556
|
const publish = (input) => deps.withSerialization(() => {
|
|
3142
4557
|
const current = read();
|
|
3143
4558
|
if (current.revision !== input.expectedRevision)
|
|
3144
4559
|
throw new Error("Telegram custody readiness evidence revision changed.");
|
|
3145
|
-
const next = {
|
|
3146
|
-
...
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
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
|
+
};
|
|
3150
4571
|
const serialized = `${JSON.stringify(next)}\n`;
|
|
3151
4572
|
const validated = decode(serialized);
|
|
3152
4573
|
const normalizedEvidence = input.kind === "writer-exclusion"
|
|
3153
|
-
? validated.writerExclusion
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
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))
|
|
3157
4582
|
throw new Error("Telegram custody readiness evidence publication is unauthorized.");
|
|
3158
4583
|
deps.publishRetained(serialized);
|
|
3159
4584
|
return validated;
|
|
@@ -3164,23 +4589,45 @@ export function normalizeTelegramInputCustodyStartupExclusionAuthority(value, ex
|
|
|
3164
4589
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
3165
4590
|
return undefined;
|
|
3166
4591
|
const record = value;
|
|
3167
|
-
const keys = [
|
|
3168
|
-
"
|
|
3169
|
-
|
|
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 ||
|
|
3170
4605
|
(record.status !== "enforced" && record.status !== "revoked") ||
|
|
3171
|
-
record.profileKey !== expected.profileKey ||
|
|
3172
|
-
|
|
3173
|
-
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 ||
|
|
3174
4613
|
record.closureOperationId.length > 512 ||
|
|
3175
4614
|
typeof record.writerInventorySha256 !== "string" ||
|
|
3176
4615
|
!/^[a-f0-9]{64}$/u.test(record.writerInventorySha256) ||
|
|
3177
4616
|
record.allowedWriterProtocol !== "custody-v3" ||
|
|
3178
|
-
!Number.isSafeInteger(record.authorizedAtMs) ||
|
|
4617
|
+
!Number.isSafeInteger(record.authorizedAtMs) ||
|
|
4618
|
+
record.authorizedAtMs < 0)
|
|
3179
4619
|
return undefined;
|
|
3180
|
-
return {
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
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
|
+
};
|
|
3184
4631
|
}
|
|
3185
4632
|
export function evaluateTelegramInputCustodyWriterExclusionEvidence(input) {
|
|
3186
4633
|
const identity = { ...input.expected };
|
|
@@ -3189,14 +4636,17 @@ export function evaluateTelegramInputCustodyWriterExclusionEvidence(input) {
|
|
|
3189
4636
|
input.inventory.profileKey !== input.expected.profileKey ||
|
|
3190
4637
|
input.inventory.recoveryKey !== input.expected.recoveryKey ||
|
|
3191
4638
|
!/^[a-f0-9]{64}$/u.test(input.inventory.writerInventorySha256) ||
|
|
3192
|
-
!authority ||
|
|
4639
|
+
!authority ||
|
|
4640
|
+
authority.status !== "enforced" ||
|
|
3193
4641
|
authority.profileKey !== input.expected.profileKey ||
|
|
3194
4642
|
authority.recoveryKey !== input.expected.recoveryKey ||
|
|
3195
4643
|
authority.writerInventorySha256 !== input.inventory.writerInventorySha256)
|
|
3196
4644
|
return { version: 1, status: "unknown", ...identity };
|
|
3197
|
-
const authorityBinding = {
|
|
4645
|
+
const authorityBinding = {
|
|
4646
|
+
startupAuthorityId: authority.authorityId,
|
|
3198
4647
|
closureOperationId: authority.closureOperationId,
|
|
3199
|
-
writerInventorySha256: authority.writerInventorySha256
|
|
4648
|
+
writerInventorySha256: authority.writerInventorySha256,
|
|
4649
|
+
};
|
|
3200
4650
|
let unknown = false;
|
|
3201
4651
|
for (const writer of input.inventory.writers) {
|
|
3202
4652
|
let liveness;
|
|
@@ -3207,49 +4657,82 @@ export function evaluateTelegramInputCustodyWriterExclusionEvidence(input) {
|
|
|
3207
4657
|
liveness = "unverifiable";
|
|
3208
4658
|
}
|
|
3209
4659
|
if (liveness === "alive")
|
|
3210
|
-
return {
|
|
3211
|
-
|
|
4660
|
+
return {
|
|
4661
|
+
version: 1,
|
|
4662
|
+
status: "present",
|
|
4663
|
+
...identity,
|
|
4664
|
+
...authorityBinding,
|
|
4665
|
+
};
|
|
3212
4666
|
if (liveness !== "dead")
|
|
3213
4667
|
unknown = true;
|
|
3214
4668
|
}
|
|
3215
|
-
return {
|
|
3216
|
-
|
|
4669
|
+
return {
|
|
4670
|
+
version: 1,
|
|
4671
|
+
status: unknown ? "unknown" : "excluded",
|
|
4672
|
+
...identity,
|
|
4673
|
+
...authorityBinding,
|
|
4674
|
+
};
|
|
3217
4675
|
}
|
|
3218
4676
|
export function normalizeTelegramInputCustodyMigrationCompletionAuthority(value, expected) {
|
|
3219
4677
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
3220
4678
|
return undefined;
|
|
3221
4679
|
const record = value;
|
|
3222
|
-
const keys = [
|
|
3223
|
-
"
|
|
3224
|
-
"
|
|
3225
|
-
|
|
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 ||
|
|
3226
4694
|
(record.status !== "authorized" && record.status !== "revoked") ||
|
|
3227
|
-
record.profileKey !== expected.profileKey ||
|
|
3228
|
-
|
|
3229
|
-
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 ||
|
|
3230
4702
|
record.startupAuthorityId.length > 512 ||
|
|
3231
|
-
typeof record.closureOperationId !== "string" ||
|
|
4703
|
+
typeof record.closureOperationId !== "string" ||
|
|
4704
|
+
!record.closureOperationId ||
|
|
3232
4705
|
record.closureOperationId.length > 512 ||
|
|
3233
4706
|
typeof record.migrationInventorySha256 !== "string" ||
|
|
3234
4707
|
!/^[a-f0-9]{64}$/u.test(record.migrationInventorySha256) ||
|
|
3235
|
-
(record.resultingSourceFamily !== "absent" &&
|
|
3236
|
-
|
|
4708
|
+
(record.resultingSourceFamily !== "absent" &&
|
|
4709
|
+
record.resultingSourceFamily !== "v3") ||
|
|
4710
|
+
!Number.isSafeInteger(record.authorizedAtMs) ||
|
|
4711
|
+
record.authorizedAtMs < 0)
|
|
3237
4712
|
return undefined;
|
|
3238
|
-
return {
|
|
4713
|
+
return {
|
|
4714
|
+
version: 1,
|
|
4715
|
+
status: record.status,
|
|
4716
|
+
authorityId: record.authorityId,
|
|
3239
4717
|
startupAuthorityId: record.startupAuthorityId,
|
|
3240
|
-
closureOperationId: record.closureOperationId,
|
|
4718
|
+
closureOperationId: record.closureOperationId,
|
|
4719
|
+
profileKey: expected.profileKey,
|
|
3241
4720
|
recoveryKey: expected.recoveryKey,
|
|
3242
4721
|
migrationInventorySha256: record.migrationInventorySha256,
|
|
3243
4722
|
resultingSourceFamily: record.resultingSourceFamily,
|
|
3244
|
-
authorizedAtMs: record.authorizedAtMs
|
|
4723
|
+
authorizedAtMs: record.authorizedAtMs,
|
|
4724
|
+
};
|
|
3245
4725
|
}
|
|
3246
4726
|
export function executeTelegramInputCustodyWriterCutover(input) {
|
|
3247
|
-
const matchesAuthority = (candidate) => Boolean(candidate &&
|
|
4727
|
+
const matchesAuthority = (candidate) => Boolean(candidate &&
|
|
4728
|
+
candidate.status === "enforced" &&
|
|
4729
|
+
candidate.version === 1 &&
|
|
3248
4730
|
candidate.profileKey === input.expected.profileKey &&
|
|
3249
4731
|
candidate.recoveryKey === input.expected.recoveryKey &&
|
|
3250
4732
|
candidate.authorityId === input.startupAuthority.authorityId &&
|
|
3251
4733
|
candidate.closureOperationId === input.closure.operationId &&
|
|
3252
|
-
candidate.writerInventorySha256 ===
|
|
4734
|
+
candidate.writerInventorySha256 ===
|
|
4735
|
+
input.startupAuthority.writerInventorySha256 &&
|
|
3253
4736
|
candidate.allowedWriterProtocol === "custody-v3" &&
|
|
3254
4737
|
candidate.authorizedAtMs === input.startupAuthority.authorizedAtMs);
|
|
3255
4738
|
if (input.closure.profileKey !== input.expected.profileKey ||
|
|
@@ -3257,8 +4740,10 @@ export function executeTelegramInputCustodyWriterCutover(input) {
|
|
|
3257
4740
|
!matchesAuthority(input.evidenceStore.read().startupExclusion))
|
|
3258
4741
|
return { kind: "blocked", blocker: "startup-authority" };
|
|
3259
4742
|
const evidence = evaluateTelegramInputCustodyWriterExclusionEvidence({
|
|
3260
|
-
expected: input.expected,
|
|
3261
|
-
|
|
4743
|
+
expected: input.expected,
|
|
4744
|
+
inventory: input.inventory,
|
|
4745
|
+
startupAuthority: input.startupAuthority,
|
|
4746
|
+
getProcessLiveness: input.getProcessLiveness,
|
|
3262
4747
|
});
|
|
3263
4748
|
if (evidence.status !== "excluded")
|
|
3264
4749
|
return { kind: "blocked", blocker: "writer-inventory", evidence };
|
|
@@ -3277,30 +4762,43 @@ export function executeTelegramInputCustodyWriterCutover(input) {
|
|
|
3277
4762
|
if (!matchesAuthority(current.startupExclusion))
|
|
3278
4763
|
return { kind: "blocked", blocker: "startup-authority", evidence };
|
|
3279
4764
|
const existing = current.writerExclusion;
|
|
3280
|
-
if (existing?.status === "excluded" &&
|
|
4765
|
+
if (existing?.status === "excluded" &&
|
|
4766
|
+
existing.profileKey === evidence.profileKey &&
|
|
3281
4767
|
existing.recoveryKey === evidence.recoveryKey &&
|
|
3282
4768
|
existing.startupAuthorityId === evidence.startupAuthorityId &&
|
|
3283
4769
|
existing.closureOperationId === evidence.closureOperationId &&
|
|
3284
4770
|
existing.writerInventorySha256 === evidence.writerInventorySha256)
|
|
3285
|
-
return {
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
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
|
+
});
|
|
3289
4782
|
return { kind: "completed", mode: installed.mode, evidence, resumed: false };
|
|
3290
4783
|
}
|
|
3291
4784
|
function matchesTelegramInputCustodyActivationEvidence(evidence, expected) {
|
|
3292
|
-
return Boolean(evidence &&
|
|
4785
|
+
return Boolean(evidence &&
|
|
4786
|
+
evidence.profileKey === expected.profileKey &&
|
|
3293
4787
|
evidence.recoveryKey === expected.recoveryKey);
|
|
3294
4788
|
}
|
|
3295
4789
|
export function executeTelegramInputCustodyMigrationCompletion(input) {
|
|
3296
|
-
const matchesAuthority = (candidate) => Boolean(candidate &&
|
|
4790
|
+
const matchesAuthority = (candidate) => Boolean(candidate &&
|
|
4791
|
+
candidate.version === 1 &&
|
|
4792
|
+
candidate.status === "authorized" &&
|
|
3297
4793
|
candidate.profileKey === input.expected.profileKey &&
|
|
3298
4794
|
candidate.recoveryKey === input.expected.recoveryKey &&
|
|
3299
4795
|
candidate.authorityId === input.authority.authorityId &&
|
|
3300
4796
|
candidate.startupAuthorityId === input.authority.startupAuthorityId &&
|
|
3301
4797
|
candidate.closureOperationId === input.authority.closureOperationId &&
|
|
3302
|
-
candidate.migrationInventorySha256 ===
|
|
3303
|
-
|
|
4798
|
+
candidate.migrationInventorySha256 ===
|
|
4799
|
+
input.authority.migrationInventorySha256 &&
|
|
4800
|
+
candidate.resultingSourceFamily ===
|
|
4801
|
+
input.authority.resultingSourceFamily &&
|
|
3304
4802
|
candidate.authorizedAtMs === input.authority.authorizedAtMs);
|
|
3305
4803
|
const current = input.evidenceStore.read();
|
|
3306
4804
|
if (!matchesAuthority(current.migrationCompletion))
|
|
@@ -3316,15 +4814,19 @@ export function executeTelegramInputCustodyMigrationCompletion(input) {
|
|
|
3316
4814
|
}
|
|
3317
4815
|
if (source !== input.authority.resultingSourceFamily)
|
|
3318
4816
|
return { kind: "blocked", blocker: "source-drift" };
|
|
3319
|
-
const evidence = {
|
|
3320
|
-
|
|
4817
|
+
const evidence = {
|
|
4818
|
+
version: 1,
|
|
4819
|
+
status: "complete",
|
|
4820
|
+
...input.expected,
|
|
3321
4821
|
migrationAuthorityId: input.authority.authorityId,
|
|
3322
4822
|
startupAuthorityId: input.authority.startupAuthorityId,
|
|
3323
4823
|
closureOperationId: input.authority.closureOperationId,
|
|
3324
4824
|
migrationInventorySha256: input.authority.migrationInventorySha256,
|
|
3325
|
-
resultingSourceFamily: input.authority.resultingSourceFamily
|
|
4825
|
+
resultingSourceFamily: input.authority.resultingSourceFamily,
|
|
4826
|
+
};
|
|
3326
4827
|
const existing = current.migration;
|
|
3327
|
-
if (existing?.status === "complete" &&
|
|
4828
|
+
if (existing?.status === "complete" &&
|
|
4829
|
+
existing.profileKey === evidence.profileKey &&
|
|
3328
4830
|
existing.recoveryKey === evidence.recoveryKey &&
|
|
3329
4831
|
existing.migrationAuthorityId === evidence.migrationAuthorityId &&
|
|
3330
4832
|
existing.startupAuthorityId === evidence.startupAuthorityId &&
|
|
@@ -3332,7 +4834,11 @@ export function executeTelegramInputCustodyMigrationCompletion(input) {
|
|
|
3332
4834
|
existing.migrationInventorySha256 === evidence.migrationInventorySha256 &&
|
|
3333
4835
|
existing.resultingSourceFamily === evidence.resultingSourceFamily)
|
|
3334
4836
|
return { kind: "completed", evidence: existing, resumed: true };
|
|
3335
|
-
input.evidenceStore.publish({
|
|
4837
|
+
input.evidenceStore.publish({
|
|
4838
|
+
expectedRevision: current.revision,
|
|
4839
|
+
kind: "migration",
|
|
4840
|
+
evidence,
|
|
4841
|
+
});
|
|
3336
4842
|
return { kind: "completed", evidence, resumed: false };
|
|
3337
4843
|
}
|
|
3338
4844
|
export function createTelegramInputCustodyProvenReadinessResolver(deps) {
|
|
@@ -3349,26 +4855,30 @@ export function createTelegramInputCustodyProvenReadinessResolver(deps) {
|
|
|
3349
4855
|
const expected = deps.expectedIdentity();
|
|
3350
4856
|
const authority = deps.readMigrationCompletionAuthority();
|
|
3351
4857
|
return matchesTelegramInputCustodyActivationEvidence(authority, expected) &&
|
|
3352
|
-
authority?.status === "authorized" &&
|
|
3353
|
-
|
|
4858
|
+
authority?.status === "authorized" &&
|
|
4859
|
+
authority.resultingSourceFamily === source
|
|
4860
|
+
? source
|
|
4861
|
+
: "ambiguous";
|
|
3354
4862
|
},
|
|
3355
4863
|
areLegacyWritersExcluded() {
|
|
3356
4864
|
const expected = deps.expectedIdentity();
|
|
3357
4865
|
const evidence = deps.readWriterExclusionEvidence();
|
|
3358
4866
|
const authority = deps.readStartupExclusionAuthority();
|
|
3359
4867
|
const mode = deps.readWriterProtocolMode();
|
|
3360
|
-
return matchesTelegramInputCustodyActivationEvidence(evidence, expected) &&
|
|
4868
|
+
return (matchesTelegramInputCustodyActivationEvidence(evidence, expected) &&
|
|
3361
4869
|
matchesTelegramInputCustodyActivationEvidence(authority, expected) &&
|
|
3362
4870
|
matchesTelegramInputCustodyActivationEvidence(mode, expected) &&
|
|
3363
|
-
evidence?.version === 1 &&
|
|
3364
|
-
|
|
4871
|
+
evidence?.version === 1 &&
|
|
4872
|
+
evidence.status === "excluded" &&
|
|
4873
|
+
authority?.version === 1 &&
|
|
4874
|
+
authority.status === "enforced" &&
|
|
3365
4875
|
mode?.protocol === "custody-v3" &&
|
|
3366
4876
|
evidence.startupAuthorityId === authority.authorityId &&
|
|
3367
4877
|
evidence.startupAuthorityId === mode.startupAuthorityId &&
|
|
3368
4878
|
evidence.closureOperationId === authority.closureOperationId &&
|
|
3369
4879
|
evidence.closureOperationId === mode.closureOperationId &&
|
|
3370
4880
|
evidence.writerInventorySha256 === authority.writerInventorySha256 &&
|
|
3371
|
-
evidence.writerInventorySha256 === mode.writerInventorySha256;
|
|
4881
|
+
evidence.writerInventorySha256 === mode.writerInventorySha256);
|
|
3372
4882
|
},
|
|
3373
4883
|
isHistoricalMigrationComplete() {
|
|
3374
4884
|
const expected = deps.expectedIdentity();
|
|
@@ -3383,10 +4893,12 @@ export function createTelegramInputCustodyProvenReadinessResolver(deps) {
|
|
|
3383
4893
|
catch {
|
|
3384
4894
|
source = "ambiguous";
|
|
3385
4895
|
}
|
|
3386
|
-
return matchesTelegramInputCustodyActivationEvidence(evidence, expected) &&
|
|
4896
|
+
return (matchesTelegramInputCustodyActivationEvidence(evidence, expected) &&
|
|
3387
4897
|
matchesTelegramInputCustodyActivationEvidence(authority, expected) &&
|
|
3388
|
-
evidence?.version === 1 &&
|
|
3389
|
-
|
|
4898
|
+
evidence?.version === 1 &&
|
|
4899
|
+
evidence.status === "complete" &&
|
|
4900
|
+
authority?.version === 1 &&
|
|
4901
|
+
authority.status === "authorized" &&
|
|
3390
4902
|
authority.resultingSourceFamily === source &&
|
|
3391
4903
|
evidence.migrationAuthorityId === authority.authorityId &&
|
|
3392
4904
|
evidence.startupAuthorityId === authority.startupAuthorityId &&
|
|
@@ -3394,8 +4906,9 @@ export function createTelegramInputCustodyProvenReadinessResolver(deps) {
|
|
|
3394
4906
|
evidence.startupAuthorityId === mode?.startupAuthorityId &&
|
|
3395
4907
|
evidence.closureOperationId === authority.closureOperationId &&
|
|
3396
4908
|
evidence.closureOperationId === mode?.closureOperationId &&
|
|
3397
|
-
evidence.migrationInventorySha256 ===
|
|
3398
|
-
|
|
4909
|
+
evidence.migrationInventorySha256 ===
|
|
4910
|
+
authority.migrationInventorySha256 &&
|
|
4911
|
+
evidence.resultingSourceFamily === authority.resultingSourceFamily);
|
|
3399
4912
|
},
|
|
3400
4913
|
listPeerReadiness: deps.listPeerReadiness,
|
|
3401
4914
|
});
|
|
@@ -3409,14 +4922,18 @@ export function createTelegramInputCustodyActivationReadinessResolver(deps) {
|
|
|
3409
4922
|
try {
|
|
3410
4923
|
legacyWritersExcluded = deps.areLegacyWritersExcluded();
|
|
3411
4924
|
}
|
|
3412
|
-
catch {
|
|
4925
|
+
catch {
|
|
4926
|
+
/* blocked */
|
|
4927
|
+
}
|
|
3413
4928
|
if (!legacyWritersExcluded)
|
|
3414
4929
|
return { enabled: false, blocker: "legacy-writers-present" };
|
|
3415
4930
|
let historicalMigrationComplete = false;
|
|
3416
4931
|
try {
|
|
3417
4932
|
historicalMigrationComplete = deps.isHistoricalMigrationComplete();
|
|
3418
4933
|
}
|
|
3419
|
-
catch {
|
|
4934
|
+
catch {
|
|
4935
|
+
/* blocked */
|
|
4936
|
+
}
|
|
3420
4937
|
if (!historicalMigrationComplete)
|
|
3421
4938
|
return { enabled: false, blocker: "migration-incomplete" };
|
|
3422
4939
|
let sourceStatus;
|
|
@@ -3435,8 +4952,13 @@ export function createTelegramInputCustodyActivationReadinessResolver(deps) {
|
|
|
3435
4952
|
catch {
|
|
3436
4953
|
peerReadiness = ["unknown"];
|
|
3437
4954
|
}
|
|
3438
|
-
return evaluateTelegramInputCustodyActivationReadiness({
|
|
3439
|
-
|
|
4955
|
+
return evaluateTelegramInputCustodyActivationReadiness({
|
|
4956
|
+
requested,
|
|
4957
|
+
sourceStatus,
|
|
4958
|
+
legacyWritersExcluded,
|
|
4959
|
+
historicalMigrationComplete,
|
|
4960
|
+
peerReadiness,
|
|
4961
|
+
});
|
|
3440
4962
|
};
|
|
3441
4963
|
}
|
|
3442
4964
|
export function createTelegramInputCustodyLifecycleBindingResolver(deps) {
|
|
@@ -3447,21 +4969,33 @@ export function createTelegramInputCustodyLifecycleBindingResolver(deps) {
|
|
|
3447
4969
|
return undefined;
|
|
3448
4970
|
const source = deps.resolveInputJournal();
|
|
3449
4971
|
const recipientBindingKey = deps.getRecipientBindingKey()?.trim();
|
|
3450
|
-
if (!source ||
|
|
4972
|
+
if (!source ||
|
|
4973
|
+
!source.runtimeKey ||
|
|
4974
|
+
!source.recoveryKey ||
|
|
4975
|
+
!recipientBindingKey)
|
|
3451
4976
|
return undefined;
|
|
3452
4977
|
let port = ports.get(source.journal);
|
|
3453
4978
|
if (!port) {
|
|
3454
4979
|
port = createTelegramInputCustodyWorkerJournalPort(source.journal);
|
|
3455
4980
|
ports.set(source.journal, port);
|
|
3456
4981
|
}
|
|
3457
|
-
return {
|
|
4982
|
+
return {
|
|
4983
|
+
runtimeKey: JSON.stringify({
|
|
4984
|
+
source: source.runtimeKey,
|
|
4985
|
+
recipientBindingKey,
|
|
4986
|
+
}),
|
|
3458
4987
|
recoveryKey: source.recoveryKey,
|
|
3459
|
-
recipientBindingKey,
|
|
4988
|
+
recipientBindingKey,
|
|
4989
|
+
journal: {
|
|
4990
|
+
...port,
|
|
4991
|
+
appendBatch: source.journal.appendBatch,
|
|
3460
4992
|
discardQueued: source.journal.discardQueued,
|
|
3461
4993
|
recoverDeadQueueOwner: source.journal.recoverDeadQueueOwner,
|
|
3462
4994
|
offerQueuedHandoff: source.journal.offerQueuedHandoff,
|
|
3463
4995
|
acceptQueuedHandoff: source.journal.acceptQueuedHandoff,
|
|
3464
|
-
cancelQueuedHandoff: source.journal.cancelQueuedHandoff
|
|
4996
|
+
cancelQueuedHandoff: source.journal.cancelQueuedHandoff,
|
|
4997
|
+
},
|
|
4998
|
+
};
|
|
3465
4999
|
};
|
|
3466
5000
|
}
|
|
3467
5001
|
export function createTelegramCustodiedExecutionSession(input) {
|
|
@@ -3471,10 +5005,12 @@ export function createTelegramCustodiedExecutionSession(input) {
|
|
|
3471
5005
|
const receipt = receipts.get(updateId);
|
|
3472
5006
|
if (!receipt) {
|
|
3473
5007
|
const duplicate = settledQueues.get(updateId);
|
|
3474
|
-
if (duplicate &&
|
|
5008
|
+
if (duplicate &&
|
|
5009
|
+
outcome.kind === "queued" &&
|
|
3475
5010
|
duplicate.queueReceipt.queueKind === outcome.queueKind &&
|
|
3476
5011
|
duplicate.queueReceipt.receiptId === outcome.receiptId &&
|
|
3477
|
-
duplicate.queueReceipt.sourceUpdateIds.length ===
|
|
5012
|
+
duplicate.queueReceipt.sourceUpdateIds.length ===
|
|
5013
|
+
outcome.sourceUpdateIds.length &&
|
|
3478
5014
|
duplicate.queueReceipt.sourceUpdateIds.every((id, index) => id === outcome.sourceUpdateIds[index])) {
|
|
3479
5015
|
settledQueues.delete(updateId);
|
|
3480
5016
|
return duplicate;
|
|
@@ -3486,12 +5022,18 @@ export function createTelegramCustodiedExecutionSession(input) {
|
|
|
3486
5022
|
if (outcome.kind === "queued") {
|
|
3487
5023
|
if (!outcome.sourceUpdateIds.includes(updateId))
|
|
3488
5024
|
throw new TelegramUpdateAdmissionOutcomeError("Telegram grouped queue custody omitted the settling input.");
|
|
3489
|
-
const grouped = outcome.sourceUpdateIds.map(sourceUpdateId => receipts.get(sourceUpdateId));
|
|
3490
|
-
if (grouped.some(candidate => !candidate))
|
|
5025
|
+
const grouped = outcome.sourceUpdateIds.map((sourceUpdateId) => receipts.get(sourceUpdateId));
|
|
5026
|
+
if (grouped.some((candidate) => !candidate))
|
|
3491
5027
|
throw new TelegramUpdateAdmissionOutcomeError("Telegram grouped queue custody omitted an exact running source receipt.");
|
|
3492
|
-
const queued = input.journal.queueInputs({
|
|
3493
|
-
|
|
3494
|
-
|
|
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
|
+
};
|
|
3495
5037
|
for (const sourceUpdateId of outcome.sourceUpdateIds) {
|
|
3496
5038
|
receipts.delete(sourceUpdateId);
|
|
3497
5039
|
if (sourceUpdateId !== updateId)
|
|
@@ -3506,8 +5048,11 @@ export function createTelegramCustodiedExecutionSession(input) {
|
|
|
3506
5048
|
return {
|
|
3507
5049
|
settle,
|
|
3508
5050
|
async execute(update, handler) {
|
|
3509
|
-
const acquired = input.journal.acquireInput({
|
|
3510
|
-
|
|
5051
|
+
const acquired = input.journal.acquireInput({
|
|
5052
|
+
updateId: update.update_id,
|
|
5053
|
+
recipientBindingKey: input.recipientBindingKey,
|
|
5054
|
+
executionUpdate: update,
|
|
5055
|
+
});
|
|
3511
5056
|
const started = input.journal.startInput(acquired.receipt);
|
|
3512
5057
|
if (!started.started)
|
|
3513
5058
|
return { status: "outcome-unknown", receipt: acquired.receipt };
|
|
@@ -3535,6 +5080,10 @@ export function createTelegramUpdateAdmissionHandle(deps) {
|
|
|
3535
5080
|
if (deps.onLateOutcome && !deps.onLateOutcomeError) {
|
|
3536
5081
|
throw new Error("Telegram late admission outcomes require a diagnostic error sink.");
|
|
3537
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) {
|
|
3538
5087
|
const registry = deps.registry ?? getOrCreateUpdateHandlerRegistry();
|
|
3539
5088
|
let nextExecutionGeneration = 0;
|
|
3540
5089
|
return async (update, ctx, signal) => {
|
|
@@ -3551,26 +5100,41 @@ export function createTelegramUpdateAdmissionHandle(deps) {
|
|
|
3551
5100
|
generation,
|
|
3552
5101
|
updateId: update.update_id,
|
|
3553
5102
|
signal,
|
|
3554
|
-
isCurrent: () => !signal.aborted &&
|
|
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),
|
|
3555
5112
|
assertCurrent() {
|
|
3556
|
-
if (
|
|
5113
|
+
if (!execution.isCurrent()) {
|
|
3557
5114
|
throw signal.reason ?? new DOMException("Aborted", "AbortError");
|
|
3558
5115
|
}
|
|
3559
5116
|
},
|
|
3560
5117
|
};
|
|
3561
5118
|
execution.assertCurrent();
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
5119
|
+
if (!capture) {
|
|
5120
|
+
const verdict = await registry.dispatch(update, execution);
|
|
5121
|
+
execution.assertCurrent();
|
|
5122
|
+
if (verdict === "consume")
|
|
5123
|
+
return { kind: "complete" };
|
|
5124
|
+
}
|
|
3566
5125
|
let immediate = true;
|
|
3567
5126
|
let settlementReported = false;
|
|
3568
5127
|
let outcome;
|
|
5128
|
+
let completionAttempted = false;
|
|
5129
|
+
let completedSource;
|
|
3569
5130
|
const boundUpdate = bindTelegramUpdateExecutionFence(bindTelegramUpdateAdmissionSource(update, (next) => {
|
|
3570
|
-
if (
|
|
5131
|
+
if (!execution.isCurrent())
|
|
3571
5132
|
return;
|
|
3572
5133
|
if (next.kind === "deferred" && next.routingReview) {
|
|
3573
|
-
if (!immediate ||
|
|
5134
|
+
if (!immediate ||
|
|
5135
|
+
settlementReported ||
|
|
5136
|
+
routingClaims > 0 ||
|
|
5137
|
+
!deps.isHistoricalSource?.({ updateId: update.update_id, signal })) {
|
|
3574
5138
|
throw new TelegramUpdateAdmissionOutcomeError("Historical review cannot override accepted or selected work.");
|
|
3575
5139
|
}
|
|
3576
5140
|
suspended = true;
|
|
@@ -3584,15 +5148,34 @@ export function createTelegramUpdateAdmissionHandle(deps) {
|
|
|
3584
5148
|
if (!deps.onLateOutcome) {
|
|
3585
5149
|
throw new TelegramUpdateAdmissionOutcomeError(`Telegram update ${update.update_id} reported a late outcome without an owner.`);
|
|
3586
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;
|
|
3587
5161
|
void Promise.resolve()
|
|
3588
|
-
.then(() => {
|
|
3589
|
-
if (
|
|
5162
|
+
.then(async () => {
|
|
5163
|
+
if (!execution.isCurrent())
|
|
3590
5164
|
return;
|
|
3591
|
-
|
|
5165
|
+
const completion = await deps.onLateOutcome(reported, {
|
|
3592
5166
|
updateId: update.update_id,
|
|
3593
5167
|
ctx,
|
|
3594
5168
|
signal,
|
|
3595
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
|
+
};
|
|
3596
5179
|
})
|
|
3597
5180
|
.catch((error) => {
|
|
3598
5181
|
try {
|
|
@@ -3602,9 +5185,16 @@ export function createTelegramUpdateAdmissionHandle(deps) {
|
|
|
3602
5185
|
// Diagnostic sinks must not create an unhandled late Promise.
|
|
3603
5186
|
}
|
|
3604
5187
|
});
|
|
3605
|
-
}, {
|
|
3606
|
-
|
|
3607
|
-
|
|
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())
|
|
3608
5198
|
return undefined;
|
|
3609
5199
|
if (abandoned)
|
|
3610
5200
|
return { ...abandoned, duplicate: true };
|
|
@@ -3612,23 +5202,91 @@ export function createTelegramUpdateAdmissionHandle(deps) {
|
|
|
3612
5202
|
suspended = true;
|
|
3613
5203
|
// Failed/unknown publication keeps dispatch suspended for exact cancellation retry.
|
|
3614
5204
|
// An ineligible request that attempted no publication remains inert.
|
|
3615
|
-
const result = deps.abandonDeferred({
|
|
5205
|
+
const result = deps.abandonDeferred({
|
|
5206
|
+
...authority,
|
|
5207
|
+
updateId: update.update_id,
|
|
5208
|
+
signal,
|
|
5209
|
+
isCurrent,
|
|
5210
|
+
});
|
|
3616
5211
|
if (result)
|
|
3617
5212
|
abandoned = result;
|
|
3618
5213
|
else
|
|
3619
5214
|
suspended = wasSuspended;
|
|
3620
5215
|
return result;
|
|
3621
|
-
},
|
|
3622
|
-
|
|
3623
|
-
|
|
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,
|
|
3624
5223
|
inspectSource: () => execution.isCurrent() && !immediate && !settlementReported
|
|
3625
|
-
? deps.inspectDeferredSource?.({
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
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) => {
|
|
3632
5290
|
execution.assertCurrent();
|
|
3633
5291
|
if (settlementReported || routingClaims || suspended)
|
|
3634
5292
|
return undefined;
|
|
@@ -3638,7 +5296,13 @@ export function createTelegramUpdateAdmissionHandle(deps) {
|
|
|
3638
5296
|
if (routingSourceIds && !isDeepStrictEqual(ids, routingSourceIds))
|
|
3639
5297
|
throw new TelegramUpdateAdmissionOutcomeError("Telegram routing input source membership changed.");
|
|
3640
5298
|
try {
|
|
3641
|
-
routingLifetime = deps.armRoutingInput?.({
|
|
5299
|
+
routingLifetime = deps.armRoutingInput?.({
|
|
5300
|
+
updateId: update.update_id,
|
|
5301
|
+
signal,
|
|
5302
|
+
operatorUserId,
|
|
5303
|
+
sourceUpdateIds: ids,
|
|
5304
|
+
...(chooser ? { chooser } : {}),
|
|
5305
|
+
});
|
|
3642
5306
|
}
|
|
3643
5307
|
catch (error) {
|
|
3644
5308
|
routingClockUnknown = true;
|
|
@@ -3658,8 +5322,13 @@ export function createTelegramUpdateAdmissionHandle(deps) {
|
|
|
3658
5322
|
if (!isDeepStrictEqual(sourceUpdateIds ?? [update.update_id], routingSourceIds))
|
|
3659
5323
|
throw new TelegramUpdateAdmissionOutcomeError("Telegram routing input selection changed its source membership.");
|
|
3660
5324
|
routingSelectionAttempted = true;
|
|
3661
|
-
routingSelectionConfirmed =
|
|
3662
|
-
|
|
5325
|
+
routingSelectionConfirmed =
|
|
5326
|
+
deps.selectRoutingInput?.({
|
|
5327
|
+
updateId: update.update_id,
|
|
5328
|
+
signal,
|
|
5329
|
+
operatorUserId: routingLifetime.operatorUserId,
|
|
5330
|
+
sourceUpdateIds: routingSourceIds,
|
|
5331
|
+
}) === true;
|
|
3663
5332
|
if (!routingSelectionConfirmed)
|
|
3664
5333
|
throw new TelegramUpdateAdmissionOutcomeError("Telegram routing input choice is expired or unavailable.");
|
|
3665
5334
|
}
|
|
@@ -3671,11 +5340,19 @@ export function createTelegramUpdateAdmissionHandle(deps) {
|
|
|
3671
5340
|
released = true;
|
|
3672
5341
|
routingClaims -= 1;
|
|
3673
5342
|
};
|
|
3674
|
-
}
|
|
5343
|
+
},
|
|
5344
|
+
}), execution);
|
|
3675
5345
|
try {
|
|
3676
5346
|
execution.assertCurrent();
|
|
3677
|
-
|
|
3678
|
-
|
|
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)
|
|
3679
5356
|
execution.assertCurrent();
|
|
3680
5357
|
}
|
|
3681
5358
|
finally {
|
|
@@ -3685,47 +5362,63 @@ export function createTelegramUpdateAdmissionHandle(deps) {
|
|
|
3685
5362
|
};
|
|
3686
5363
|
}
|
|
3687
5364
|
export function createTelegramCustodiedUpdateAdmissionHandle(deps) {
|
|
3688
|
-
const session = createTelegramCustodiedExecutionSession({
|
|
3689
|
-
|
|
5365
|
+
const session = createTelegramCustodiedExecutionSession({
|
|
5366
|
+
journal: deps.journal,
|
|
5367
|
+
recipientBindingKey: deps.recipientBindingKey,
|
|
5368
|
+
});
|
|
3690
5369
|
const admission = createTelegramUpdateAdmissionHandle({
|
|
3691
5370
|
defaultHandle: deps.defaultHandle,
|
|
3692
5371
|
registry: deps.registry,
|
|
3693
5372
|
async onLateOutcome(outcome, details) {
|
|
3694
5373
|
const result = await session.settle(details.updateId, outcome);
|
|
3695
|
-
deps.onCustodiedLateSettlement?.(result, {
|
|
5374
|
+
deps.onCustodiedLateSettlement?.(result, {
|
|
5375
|
+
updateId: details.updateId,
|
|
5376
|
+
signal: details.signal,
|
|
5377
|
+
});
|
|
3696
5378
|
},
|
|
3697
5379
|
onLateOutcomeError: deps.onLateOutcomeError,
|
|
3698
5380
|
});
|
|
3699
|
-
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));
|
|
3700
5382
|
}
|
|
3701
5383
|
// A dispatched prompt can remain in memory until agent_start. Remember only acknowledged
|
|
3702
5384
|
// removal of these exact receipt objects for later local discard, never for dispatch/replay.
|
|
3703
5385
|
const acknowledgedQueueReceiptCompletions = new WeakMap();
|
|
3704
5386
|
const isQueueReceiptCompletionAcknowledged = (receipt) => {
|
|
3705
5387
|
const acknowledged = acknowledgedQueueReceiptCompletions.get(receipt);
|
|
3706
|
-
return acknowledged !== undefined &&
|
|
5388
|
+
return (acknowledged !== undefined &&
|
|
5389
|
+
areTelegramQueueAdmissionReceiptsEqual(acknowledged, receipt));
|
|
3707
5390
|
};
|
|
3708
5391
|
export function createTelegramQueueAdmissionSettlementMuxRuntime(runtimes) {
|
|
3709
5392
|
// Plan the whole request before mutation; unready is not an acknowledgement of completion.
|
|
3710
5393
|
const complete = (items, ctx, kind) => {
|
|
3711
5394
|
const plan = new Map();
|
|
3712
|
-
const reason = kind === "prompt"
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
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 ?? [])) {
|
|
3716
5406
|
if (isQueueReceiptCompletionAcknowledged(receipt))
|
|
3717
5407
|
continue;
|
|
3718
|
-
const candidates = runtimes.filter(runtime => runtime.getQueueReceiptSettlementOwner
|
|
5408
|
+
const candidates = runtimes.filter((runtime) => runtime.getQueueReceiptSettlementOwner
|
|
5409
|
+
? !!settlementOwner(runtime, receipt)
|
|
3719
5410
|
: runtime.isItemReady({ admissionReceipts: [receipt] }));
|
|
3720
5411
|
const selected = candidates[0];
|
|
3721
5412
|
if (!selected)
|
|
3722
5413
|
return false;
|
|
3723
5414
|
if (candidates.length > 1) {
|
|
3724
5415
|
const owner = settlementOwner(selected, receipt);
|
|
3725
|
-
if (!owner ||
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
5416
|
+
if (!owner ||
|
|
5417
|
+
candidates.some((runtime) => {
|
|
5418
|
+
const candidate = settlementOwner(runtime, receipt);
|
|
5419
|
+
return (!candidate ||
|
|
5420
|
+
!areTelegramUpdateJournalQueueOwnersEqual(owner, candidate));
|
|
5421
|
+
}))
|
|
3729
5422
|
return false;
|
|
3730
5423
|
}
|
|
3731
5424
|
const receipts = plan.get(selected) ?? [];
|
|
@@ -3734,8 +5427,10 @@ export function createTelegramQueueAdmissionSettlementMuxRuntime(runtimes) {
|
|
|
3734
5427
|
}
|
|
3735
5428
|
for (const [runtime, receipts] of plan) {
|
|
3736
5429
|
const item = { admissionReceipts: receipts };
|
|
3737
|
-
const completed = kind === "discard"
|
|
3738
|
-
|
|
5430
|
+
const completed = kind === "discard"
|
|
5431
|
+
? runtime.onItemsDiscarded([item], ctx)
|
|
5432
|
+
: kind === "prompt"
|
|
5433
|
+
? runtime.onPromptHandedOff(item, ctx)
|
|
3739
5434
|
: runtime.onControlSettled(item, ctx);
|
|
3740
5435
|
if (completed !== true)
|
|
3741
5436
|
return false;
|
|
@@ -3772,12 +5467,17 @@ export function createTelegramQueueAdmissionSettlementRuntime(worker) {
|
|
|
3772
5467
|
receipts.push(receipt);
|
|
3773
5468
|
}
|
|
3774
5469
|
}
|
|
3775
|
-
return receipts.length === 0 ||
|
|
5470
|
+
return (receipts.length === 0 ||
|
|
5471
|
+
worker.completeQueueReceipts({ receipts, ctx, reason }));
|
|
3776
5472
|
};
|
|
3777
5473
|
return {
|
|
3778
5474
|
isItemReady: (item) => (item.admissionReceipts ?? []).every(worker.isQueueReceiptCommitted),
|
|
3779
5475
|
getQueueReceiptOwner: worker.getQueueReceiptOwner,
|
|
3780
|
-
...(worker.getQueueReceiptSettlementOwner
|
|
5476
|
+
...(worker.getQueueReceiptSettlementOwner
|
|
5477
|
+
? {
|
|
5478
|
+
getQueueReceiptSettlementOwner: worker.getQueueReceiptSettlementOwner,
|
|
5479
|
+
}
|
|
5480
|
+
: {}),
|
|
3781
5481
|
onPromptHandedOff: (item, ctx) => complete([item], ctx, "prompt-handoff"),
|
|
3782
5482
|
onControlSettled: (item, ctx) => complete([item], ctx, "control-settlement"),
|
|
3783
5483
|
onItemsDiscarded: (items, ctx) => complete(items, ctx, "discard"),
|
|
@@ -3951,8 +5651,7 @@ export function createTelegramQueueHandoffReconciliationRuntimeAssembly(deps) {
|
|
|
3951
5651
|
createRecipientJournalBindingKey(recipient) {
|
|
3952
5652
|
if (!recipient.profileKey || !recipient.sessionId)
|
|
3953
5653
|
return undefined;
|
|
3954
|
-
return deps.createRecipientJournalResolver(recipient.profileKey, recipient.sessionId)()
|
|
3955
|
-
?.recoveryKey;
|
|
5654
|
+
return deps.createRecipientJournalResolver(recipient.profileKey, recipient.sessionId)()?.recoveryKey;
|
|
3956
5655
|
},
|
|
3957
5656
|
getQueuedItems: deps.queueStore.getQueuedItems,
|
|
3958
5657
|
getReceiptOwner(receipt) {
|
|
@@ -4006,7 +5705,9 @@ export function createTelegramQueueHandoffReconciler(deps) {
|
|
|
4006
5705
|
(followerRegistered && deps.canHandoffWithLeader?.() === false)) {
|
|
4007
5706
|
return;
|
|
4008
5707
|
}
|
|
4009
|
-
const followers = deps
|
|
5708
|
+
const followers = deps
|
|
5709
|
+
.listFollowers()
|
|
5710
|
+
.filter((follower) => follower.instanceId !== deps.donorInstanceId);
|
|
4010
5711
|
if (followers.length === 0)
|
|
4011
5712
|
return;
|
|
4012
5713
|
for (const item of [...deps.getQueuedItems()]) {
|
|
@@ -4052,7 +5753,12 @@ export function createTelegramQueueHandoffReconciler(deps) {
|
|
|
4052
5753
|
stageRemote: async ({ payload: sourcePayload }) => {
|
|
4053
5754
|
const payload = {
|
|
4054
5755
|
...sourcePayload,
|
|
4055
|
-
admissionReceipts: [
|
|
5756
|
+
admissionReceipts: [
|
|
5757
|
+
{
|
|
5758
|
+
...sourcePayload.admissionReceipts[0],
|
|
5759
|
+
journalBindingKey: recipientJournalBindingKey,
|
|
5760
|
+
},
|
|
5761
|
+
],
|
|
4056
5762
|
};
|
|
4057
5763
|
if (followerRegistered) {
|
|
4058
5764
|
return deps.stageThroughFollower({
|
|
@@ -4088,7 +5794,7 @@ export function createTelegramQueueHandoffReconciler(deps) {
|
|
|
4088
5794
|
typeof response.result !== "object" ||
|
|
4089
5795
|
!queueOwner) {
|
|
4090
5796
|
throw new Error(response.kind === "bus.ack"
|
|
4091
|
-
? response.message ?? "Telegram queue handoff was rejected."
|
|
5797
|
+
? (response.message ?? "Telegram queue handoff was rejected.")
|
|
4092
5798
|
: "Telegram queue handoff returned no acknowledgement.");
|
|
4093
5799
|
}
|
|
4094
5800
|
return {
|
|
@@ -4149,7 +5855,7 @@ export function createTelegramUpdateAdmissionRuntimeBinding(deps) {
|
|
|
4149
5855
|
let follower;
|
|
4150
5856
|
let settlement;
|
|
4151
5857
|
let inputCustodyBus;
|
|
4152
|
-
const getActive = () => deps.isFollowerRegistered() ? follower : leader;
|
|
5858
|
+
const getActive = () => (deps.isFollowerRegistered() ? follower : leader);
|
|
4153
5859
|
return {
|
|
4154
5860
|
bind(input) {
|
|
4155
5861
|
leader = input.leader;
|
|
@@ -4211,12 +5917,14 @@ export function createTelegramUpdateAdmissionLifecycleRuntime(deps) {
|
|
|
4211
5917
|
let foreignQueueOwnerLiveness;
|
|
4212
5918
|
let releaseSourceReference;
|
|
4213
5919
|
let sourceReferenceBinding;
|
|
5920
|
+
let activeContext;
|
|
4214
5921
|
const withRetainedSourceReference = (operation) => {
|
|
4215
5922
|
if (!journal || !worker)
|
|
4216
5923
|
throw new Error("Telegram update admission worker is not active.");
|
|
4217
5924
|
// Synchronous retained-source access needs a reference, not renewed execution authority.
|
|
4218
5925
|
const release = !releaseSourceReference && sourceReferenceBinding
|
|
4219
|
-
? deps.acquireSourceReference?.(sourceReferenceBinding)
|
|
5926
|
+
? deps.acquireSourceReference?.(sourceReferenceBinding)
|
|
5927
|
+
: undefined;
|
|
4220
5928
|
try {
|
|
4221
5929
|
return operation(journal);
|
|
4222
5930
|
}
|
|
@@ -4224,8 +5932,9 @@ export function createTelegramUpdateAdmissionLifecycleRuntime(deps) {
|
|
|
4224
5932
|
release?.();
|
|
4225
5933
|
}
|
|
4226
5934
|
};
|
|
4227
|
-
const readObservedJournal = () => withRetainedSourceReference(source => source.read());
|
|
5935
|
+
const readObservedJournal = () => withRetainedSourceReference((source) => source.read());
|
|
4228
5936
|
const stopCurrent = async (forget) => {
|
|
5937
|
+
activeContext = undefined;
|
|
4229
5938
|
await worker?.stop();
|
|
4230
5939
|
const release = releaseSourceReference;
|
|
4231
5940
|
releaseSourceReference = undefined;
|
|
@@ -4233,7 +5942,9 @@ export function createTelegramUpdateAdmissionLifecycleRuntime(deps) {
|
|
|
4233
5942
|
release?.();
|
|
4234
5943
|
}
|
|
4235
5944
|
catch (error) {
|
|
4236
|
-
deps.recordRuntimeEvent?.("inbound-worker", error, {
|
|
5945
|
+
deps.recordRuntimeEvent?.("inbound-worker", error, {
|
|
5946
|
+
phase: "source-reference-release",
|
|
5947
|
+
});
|
|
4237
5948
|
}
|
|
4238
5949
|
if (!forget)
|
|
4239
5950
|
return;
|
|
@@ -4252,12 +5963,15 @@ export function createTelegramUpdateAdmissionLifecycleRuntime(deps) {
|
|
|
4252
5963
|
await stopCurrent(true);
|
|
4253
5964
|
return;
|
|
4254
5965
|
}
|
|
4255
|
-
if (replace ||
|
|
5966
|
+
if (replace ||
|
|
5967
|
+
!worker ||
|
|
5968
|
+
activeRuntimeKey !== binding.runtimeKey ||
|
|
4256
5969
|
workerInputCustody !== binding.journal.inputCustody) {
|
|
4257
5970
|
const { runtimeKey, recoveryKey } = binding;
|
|
4258
5971
|
await stopCurrent(true);
|
|
4259
5972
|
binding = deps.resolveBinding();
|
|
4260
|
-
if (!binding ||
|
|
5973
|
+
if (!binding ||
|
|
5974
|
+
binding.runtimeKey !== runtimeKey ||
|
|
4261
5975
|
binding.recoveryKey !== recoveryKey) {
|
|
4262
5976
|
throw new Error("Telegram update source binding changed during startup.");
|
|
4263
5977
|
}
|
|
@@ -4356,6 +6070,7 @@ export function createTelegramUpdateAdmissionLifecycleRuntime(deps) {
|
|
|
4356
6070
|
}
|
|
4357
6071
|
}
|
|
4358
6072
|
worker.start(ctx);
|
|
6073
|
+
activeContext = ctx;
|
|
4359
6074
|
};
|
|
4360
6075
|
const runExclusive = (task) => {
|
|
4361
6076
|
const next = operation.then(task, task);
|
|
@@ -4413,7 +6128,7 @@ export function createTelegramUpdateAdmissionLifecycleRuntime(deps) {
|
|
|
4413
6128
|
if (!journal || !worker || !journal.cancelQueuedHandoff) {
|
|
4414
6129
|
throw new Error("Telegram update journal queue handoff cancellation is not available.");
|
|
4415
6130
|
}
|
|
4416
|
-
const result = withRetainedSourceReference(source => source.cancelQueuedHandoff(input));
|
|
6131
|
+
const result = withRetainedSourceReference((source) => source.cancelQueuedHandoff(input));
|
|
4417
6132
|
worker.signal();
|
|
4418
6133
|
return result;
|
|
4419
6134
|
},
|
|
@@ -4460,6 +6175,31 @@ export function createTelegramUpdateAdmissionLifecycleRuntime(deps) {
|
|
|
4460
6175
|
isTelegramReactionDependencyForQueueItem(entry.update, item)));
|
|
4461
6176
|
},
|
|
4462
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
|
+
},
|
|
4463
6203
|
getState: () => {
|
|
4464
6204
|
const state = worker?.getState();
|
|
4465
6205
|
if (!state)
|
|
@@ -4481,19 +6221,25 @@ export function createTelegramUpdateAdmissionLifecycleRuntime(deps) {
|
|
|
4481
6221
|
/** Compose source-bound routing and late grouped settlement under one worker. */
|
|
4482
6222
|
export function createTelegramUpdateAdmissionWorkerRuntime(deps) {
|
|
4483
6223
|
let worker;
|
|
4484
|
-
const
|
|
6224
|
+
const admissionDeps = {
|
|
4485
6225
|
defaultHandle: deps.defaultHandle,
|
|
4486
6226
|
registry: deps.registry,
|
|
4487
|
-
abandonDeferred: input => worker?.abandonDeferred?.(input),
|
|
4488
|
-
armRoutingInput: input => worker?.armRoutingInput?.(input),
|
|
4489
|
-
selectRoutingInput: input => worker?.selectRoutingInput?.(input) === true,
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
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,
|
|
4495
6241
|
onLateOutcome(outcome, details) {
|
|
4496
|
-
worker?.settleDeferred({
|
|
6242
|
+
return worker?.settleDeferred({
|
|
4497
6243
|
updateId: details.updateId,
|
|
4498
6244
|
outcome,
|
|
4499
6245
|
signal: details.signal,
|
|
@@ -4505,24 +6251,54 @@ export function createTelegramUpdateAdmissionWorkerRuntime(deps) {
|
|
|
4505
6251
|
updateId,
|
|
4506
6252
|
});
|
|
4507
6253
|
},
|
|
4508
|
-
}
|
|
4509
|
-
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;
|
|
4510
6270
|
if (deps.inputCustody && !custodyBindingKey)
|
|
4511
6271
|
throw new Error("Telegram custodied admission worker requires one recipient binding key.");
|
|
4512
6272
|
const executeCustodiedUpdate = deps.inputCustody && custodyBindingKey
|
|
4513
6273
|
? createTelegramCustodiedUpdateAdmissionHandle({
|
|
4514
|
-
journal: deps.inputCustody,
|
|
4515
|
-
|
|
6274
|
+
journal: deps.inputCustody,
|
|
6275
|
+
recipientBindingKey: custodyBindingKey,
|
|
6276
|
+
defaultHandle: deps.defaultHandle,
|
|
6277
|
+
registry: deps.registry,
|
|
4516
6278
|
onLateOutcomeError(error, updateId) {
|
|
4517
|
-
deps.recordRuntimeEvent?.("inbound-worker", error, {
|
|
6279
|
+
deps.recordRuntimeEvent?.("inbound-worker", error, {
|
|
6280
|
+
phase: "late-custodied-admission-handler",
|
|
6281
|
+
updateId,
|
|
6282
|
+
});
|
|
4518
6283
|
},
|
|
4519
6284
|
onCustodiedLateSettlement(result, details) {
|
|
4520
|
-
worker?.settleCustodied({
|
|
6285
|
+
worker?.settleCustodied({
|
|
6286
|
+
updateId: details.updateId,
|
|
6287
|
+
result,
|
|
6288
|
+
signal: details.signal,
|
|
6289
|
+
});
|
|
4521
6290
|
},
|
|
4522
|
-
})
|
|
6291
|
+
})
|
|
6292
|
+
: undefined;
|
|
4523
6293
|
worker = createTelegramUpdateWorkerRuntime({
|
|
4524
6294
|
...deps,
|
|
4525
|
-
|
|
6295
|
+
admitPreparedLiveInput,
|
|
6296
|
+
...(executeCustodiedUpdate
|
|
6297
|
+
? {
|
|
6298
|
+
executeCustodiedUpdate: async (update, ctx, signal) => executeCustodiedUpdate(deps.prepareUpdateForExecution?.(update) ??
|
|
6299
|
+
update, ctx, signal),
|
|
6300
|
+
}
|
|
6301
|
+
: {}),
|
|
4526
6302
|
async executeUpdate(update, ctx, signal) {
|
|
4527
6303
|
const typedUpdate = update;
|
|
4528
6304
|
try {
|
|
@@ -4542,13 +6318,19 @@ export function createTelegramUpdateAdmissionWorkerRuntime(deps) {
|
|
|
4542
6318
|
export function createTelegramUpdateAdmissionLifecycleAssembly(deps) {
|
|
4543
6319
|
const leader = createTelegramUpdateAdmissionLifecycleRuntime({
|
|
4544
6320
|
resolveBinding: deps.leader.resolveBinding,
|
|
4545
|
-
...(deps.acquireSourceReference
|
|
6321
|
+
...(deps.acquireSourceReference
|
|
6322
|
+
? {
|
|
6323
|
+
acquireSourceReference: (binding) => deps.acquireSourceReference("leader", binding),
|
|
6324
|
+
}
|
|
6325
|
+
: {}),
|
|
4546
6326
|
getQueueOwnerIdentity: deps.worker.getQueueOwnerIdentity,
|
|
4547
6327
|
createWorker(journal, binding) {
|
|
4548
6328
|
return createTelegramUpdateAdmissionWorkerRuntime({
|
|
4549
6329
|
...deps.worker,
|
|
4550
6330
|
journal,
|
|
4551
|
-
...(binding.journal.inputCustody
|
|
6331
|
+
...(binding.journal.inputCustody
|
|
6332
|
+
? { inputCustody: binding.journal.inputCustody }
|
|
6333
|
+
: {}),
|
|
4552
6334
|
getJournalBindingKey: () => binding.recoveryKey,
|
|
4553
6335
|
getRecipientBindingKey: () => binding.recipientBindingKey,
|
|
4554
6336
|
hasAuthority: deps.leader.hasAuthority,
|
|
@@ -4558,8 +6340,14 @@ export function createTelegramUpdateAdmissionLifecycleAssembly(deps) {
|
|
|
4558
6340
|
});
|
|
4559
6341
|
const follower = createTelegramUpdateAdmissionLifecycleRuntime({
|
|
4560
6342
|
getQueueOwnerIdentity: deps.worker.getQueueOwnerIdentity,
|
|
4561
|
-
...(deps.acquireSourceReference
|
|
4562
|
-
|
|
6343
|
+
...(deps.acquireSourceReference
|
|
6344
|
+
? {
|
|
6345
|
+
acquireSourceReference: (binding) => deps.acquireSourceReference("follower", binding),
|
|
6346
|
+
}
|
|
6347
|
+
: {}),
|
|
6348
|
+
...(deps.follower.prepareBinding
|
|
6349
|
+
? { prepareBinding: deps.follower.prepareBinding }
|
|
6350
|
+
: {}),
|
|
4563
6351
|
resolveBinding() {
|
|
4564
6352
|
const generation = deps.follower.getGeneration();
|
|
4565
6353
|
if (!deps.follower.isRegistered() || !generation)
|
|
@@ -4580,7 +6368,9 @@ export function createTelegramUpdateAdmissionLifecycleAssembly(deps) {
|
|
|
4580
6368
|
return createTelegramUpdateAdmissionWorkerRuntime({
|
|
4581
6369
|
...deps.worker,
|
|
4582
6370
|
journal,
|
|
4583
|
-
...(binding.journal.inputCustody
|
|
6371
|
+
...(binding.journal.inputCustody
|
|
6372
|
+
? { inputCustody: binding.journal.inputCustody }
|
|
6373
|
+
: {}),
|
|
4584
6374
|
getJournalBindingKey: () => binding.recoveryKey,
|
|
4585
6375
|
getRecipientBindingKey: () => binding.recipientBindingKey,
|
|
4586
6376
|
hasAuthority: () => binding.hasAuthority?.() ?? false,
|
|
@@ -4594,8 +6384,11 @@ export function createTelegramUpdateAdmissionLifecycleAssembly(deps) {
|
|
|
4594
6384
|
},
|
|
4595
6385
|
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
4596
6386
|
});
|
|
4597
|
-
deps.runtimeBinding.bind({
|
|
4598
|
-
|
|
6387
|
+
deps.runtimeBinding.bind({
|
|
6388
|
+
leader,
|
|
6389
|
+
follower,
|
|
6390
|
+
...(deps.inputCustodyBus ? { inputCustodyBus: deps.inputCustodyBus } : {}),
|
|
6391
|
+
});
|
|
4599
6392
|
return { leader, follower };
|
|
4600
6393
|
}
|
|
4601
6394
|
/** Own queue-owner projection and shared leader/follower worker composition. */
|