@llblab/pi-kit 0.27.5 → 0.28.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 +9 -0
- package/README.md +4 -4
- package/node_modules/@llblab/pi-state-flow/AGENTS.md +1 -1
- package/node_modules/@llblab/pi-state-flow/BACKLOG.md +6 -2
- package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +4 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/compaction.js +4 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +26 -13
- package/node_modules/@llblab/pi-state-flow/dist/package.json +1 -1
- package/node_modules/@llblab/pi-state-flow/docs/architecture.md +3 -1
- package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +2 -2
- package/node_modules/@llblab/pi-state-flow/docs/usage.md +2 -1
- package/node_modules/@llblab/pi-state-flow/lib/compaction.ts +4 -3
- package/node_modules/@llblab/pi-state-flow/lib/extension.ts +25 -12
- package/node_modules/@llblab/pi-state-flow/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/AGENTS.md +10 -8
- package/node_modules/@llblab/pi-telegram/BACKLOG.md +1 -9
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +13 -0
- package/node_modules/@llblab/pi-telegram/LICENSE +21 -0
- package/node_modules/@llblab/pi-telegram/README.md +15 -5
- 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 +5 -7
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +5 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +6 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-api.js +4 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.d.ts +54 -56
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.js +206 -139
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.d.ts +21 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.js +181 -25
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-transport.d.ts +4 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-transport.js +28 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/bus.d.ts +52 -56
- package/node_modules/@llblab/pi-telegram/dist/lib/bus.js +78 -246
- package/node_modules/@llblab/pi-telegram/dist/lib/channel-posts.d.ts +1 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/channel-posts.js +23 -26
- package/node_modules/@llblab/pi-telegram/dist/lib/command-templates.d.ts +0 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/command-templates.js +5 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +0 -23
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +15 -15
- package/node_modules/@llblab/pi-telegram/dist/lib/config.d.ts +0 -17
- package/node_modules/@llblab/pi-telegram/dist/lib/config.js +12 -14
- package/node_modules/@llblab/pi-telegram/dist/lib/delivery.d.ts +0 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/delivery.js +2 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +255 -61
- package/node_modules/@llblab/pi-telegram/dist/lib/generative-apps.d.ts +0 -19
- package/node_modules/@llblab/pi-telegram/dist/lib/inbound.d.ts +0 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/inbound.js +2 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/journal.d.ts +177 -16
- package/node_modules/@llblab/pi-telegram/dist/lib/journal.js +1115 -249
- package/node_modules/@llblab/pi-telegram/dist/lib/keyboard.d.ts +0 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/keyboard.js +2 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/lifecycle.js +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/locks.d.ts +100 -12
- package/node_modules/@llblab/pi-telegram/dist/lib/locks.js +380 -14
- package/node_modules/@llblab/pi-telegram/dist/lib/logging.d.ts +30 -12
- package/node_modules/@llblab/pi-telegram/dist/lib/logging.js +129 -72
- package/node_modules/@llblab/pi-telegram/dist/lib/media.d.ts +28 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/media.js +26 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-model.d.ts +0 -11
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-model.js +8 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.d.ts +0 -18
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.js +18 -18
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-status.d.ts +4 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-status.js +12 -7
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-thinking.d.ts +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-thinking.js +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/menu.d.ts +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/menu.js +4 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-attachments.d.ts +0 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-attachments.js +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-buttons.js +1 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-markup.d.ts +1 -6
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-markup.js +4 -21
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound.d.ts +0 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound.js +5 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/paths.d.ts +50 -7
- package/node_modules/@llblab/pi-telegram/dist/lib/paths.js +155 -17
- package/node_modules/@llblab/pi-telegram/dist/lib/pi.d.ts +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/pi.js +2 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/polling.d.ts +0 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/polling.js +5 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/preview.d.ts +0 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/preview.js +3 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/prompt-templates.d.ts +0 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/prompt-templates.js +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/queue.d.ts +3 -15
- package/node_modules/@llblab/pi-telegram/dist/lib/queue.js +69 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/recovery.d.ts +29 -9
- package/node_modules/@llblab/pi-telegram/dist/lib/recovery.js +104 -33
- package/node_modules/@llblab/pi-telegram/dist/lib/replies.d.ts +0 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/replies.js +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/routing.d.ts +73 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/routing.js +2206 -307
- package/node_modules/@llblab/pi-telegram/dist/lib/sections.d.ts +0 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/sections.js +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/status.d.ts +44 -9
- package/node_modules/@llblab/pi-telegram/dist/lib/status.js +149 -27
- package/node_modules/@llblab/pi-telegram/dist/lib/sync.d.ts +0 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/sync.js +5 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.d.ts +11 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.js +34 -23
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-cleanup-manager.d.ts +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-cleanup-manager.js +18 -21
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-naming.d.ts +32 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-naming.js +191 -7
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-reconciler.d.ts +2 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-reconciler.js +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/threads.d.ts +291 -59
- package/node_modules/@llblab/pi-telegram/dist/lib/threads.js +1647 -369
- package/node_modules/@llblab/pi-telegram/dist/lib/turns.d.ts +0 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/turns.js +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/updates.d.ts +184 -28
- package/node_modules/@llblab/pi-telegram/dist/lib/updates.js +1032 -114
- package/node_modules/@llblab/pi-telegram/dist/lib/wire.d.ts +12 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/wire.js +21 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-admission.d.ts +25 -17
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-admission.js +95 -20
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-identity.d.ts +20 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-identity.js +103 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.d.ts +43 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.js +148 -52
- package/node_modules/@llblab/pi-telegram/dist/package.json +6 -6
- package/node_modules/@llblab/pi-telegram/dist/skills/generated-control-surface/SKILL.md +3 -3
- package/node_modules/@llblab/pi-telegram/dist/skills/telegram-bridge/references/diagnosis.md +3 -3
- package/node_modules/@llblab/pi-telegram/docs/README.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/activity.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +152 -34
- package/node_modules/@llblab/pi-telegram/docs/callback-namespaces.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/delivery.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/generative-apps.md +2 -2
- package/node_modules/@llblab/pi-telegram/docs/inbound.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +125 -19
- package/node_modules/@llblab/pi-telegram/docs/public-api.md +3 -1
- package/node_modules/@llblab/pi-telegram/docs/ui-style.md +22 -8
- package/node_modules/@llblab/pi-telegram/docs/updates.md +1 -1
- package/node_modules/@llblab/pi-telegram/lib/activity-verbosity.ts +5 -9
- package/node_modules/@llblab/pi-telegram/lib/bindings.ts +6 -0
- package/node_modules/@llblab/pi-telegram/lib/bus-api.ts +5 -2
- package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +243 -224
- package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +178 -31
- package/node_modules/@llblab/pi-telegram/lib/bus-transport.ts +30 -8
- package/node_modules/@llblab/pi-telegram/lib/bus.ts +110 -334
- package/node_modules/@llblab/pi-telegram/lib/channel-posts.ts +29 -29
- package/node_modules/@llblab/pi-telegram/lib/command-templates.ts +5 -5
- package/node_modules/@llblab/pi-telegram/lib/commands.ts +15 -15
- package/node_modules/@llblab/pi-telegram/lib/config.ts +12 -17
- package/node_modules/@llblab/pi-telegram/lib/delivery.ts +2 -8
- package/node_modules/@llblab/pi-telegram/lib/extension.ts +257 -72
- package/node_modules/@llblab/pi-telegram/lib/generative-apps.ts +0 -22
- package/node_modules/@llblab/pi-telegram/lib/inbound.ts +2 -2
- package/node_modules/@llblab/pi-telegram/lib/journal.ts +1182 -326
- package/node_modules/@llblab/pi-telegram/lib/keyboard.ts +2 -2
- package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +1 -1
- package/node_modules/@llblab/pi-telegram/lib/locks.ts +402 -23
- package/node_modules/@llblab/pi-telegram/lib/logging.ts +151 -101
- package/node_modules/@llblab/pi-telegram/lib/media.ts +51 -9
- package/node_modules/@llblab/pi-telegram/lib/menu-model.ts +8 -8
- package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +18 -18
- package/node_modules/@llblab/pi-telegram/lib/menu-status.ts +11 -0
- package/node_modules/@llblab/pi-telegram/lib/menu-thinking.ts +2 -2
- package/node_modules/@llblab/pi-telegram/lib/menu.ts +5 -1
- package/node_modules/@llblab/pi-telegram/lib/outbound-attachments.ts +1 -1
- package/node_modules/@llblab/pi-telegram/lib/outbound-buttons.ts +1 -10
- package/node_modules/@llblab/pi-telegram/lib/outbound-markup.ts +5 -24
- package/node_modules/@llblab/pi-telegram/lib/outbound.ts +5 -5
- package/node_modules/@llblab/pi-telegram/lib/paths.ts +177 -17
- package/node_modules/@llblab/pi-telegram/lib/pi.ts +5 -2
- package/node_modules/@llblab/pi-telegram/lib/polling.ts +5 -5
- package/node_modules/@llblab/pi-telegram/lib/preview.ts +3 -3
- package/node_modules/@llblab/pi-telegram/lib/prompt-templates.ts +1 -1
- package/node_modules/@llblab/pi-telegram/lib/queue.ts +67 -9
- package/node_modules/@llblab/pi-telegram/lib/recovery.ts +104 -44
- package/node_modules/@llblab/pi-telegram/lib/replies.ts +1 -1
- package/node_modules/@llblab/pi-telegram/lib/routing.ts +1955 -375
- package/node_modules/@llblab/pi-telegram/lib/sections.ts +1 -1
- package/node_modules/@llblab/pi-telegram/lib/status.ts +156 -36
- package/node_modules/@llblab/pi-telegram/lib/sync.ts +4 -4
- package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +48 -33
- package/node_modules/@llblab/pi-telegram/lib/thread-cleanup-manager.ts +24 -21
- package/node_modules/@llblab/pi-telegram/lib/thread-naming.ts +267 -9
- package/node_modules/@llblab/pi-telegram/lib/thread-reconciler.ts +3 -1
- package/node_modules/@llblab/pi-telegram/lib/threads.ts +1697 -488
- package/node_modules/@llblab/pi-telegram/lib/turns.ts +1 -1
- package/node_modules/@llblab/pi-telegram/lib/updates.ts +1072 -151
- package/node_modules/@llblab/pi-telegram/lib/wire.ts +28 -0
- package/node_modules/@llblab/pi-telegram/lib/workspace-admission.ts +97 -47
- package/node_modules/@llblab/pi-telegram/lib/workspace-identity.ts +147 -0
- package/node_modules/@llblab/pi-telegram/lib/workspace-retirement.ts +188 -89
- package/node_modules/@llblab/pi-telegram/package.json +6 -6
- package/node_modules/@llblab/pi-telegram/scripts/audit-exports.mjs +100 -0
- package/node_modules/@llblab/pi-telegram/scripts/check-downgrade.mjs +80 -43
- package/node_modules/@llblab/pi-telegram/skills/generated-control-surface/SKILL.md +3 -3
- package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/references/diagnosis.md +3 -3
- package/package.json +3 -3
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
* Owns update extraction, authorization, execution planning, generation-fenced journal draining, and the public update-handler registry
|
|
5
5
|
*/
|
|
6
6
|
import { randomUUID } from "node:crypto";
|
|
7
|
+
import { isDeepStrictEqual } from "node:util";
|
|
8
|
+
import { isWireRecord as isTelegramUpdateAdmissionRecord, isNonEmptyWireString as isTelegramUpdateAdmissionString, } from "./wire.js";
|
|
7
9
|
import { createTelegramPrivateTarget, createTelegramThreadTarget, } from "./target.js";
|
|
8
|
-
import { TELEGRAM_UPDATE_JOURNAL_VERSION, TELEGRAM_UPDATE_JOURNAL_EXCLUSION_VERSION, TELEGRAM_UPDATE_JOURNAL_CUSTODY_VERSION, TELEGRAM_UPDATE_JOURNAL_FAILURE_CLASS_MAX_LENGTH, TELEGRAM_UPDATE_JOURNAL_FAILURE_SUMMARY_MAX_LENGTH, TELEGRAM_UPDATE_JOURNAL_QUEUE_OWNER_ID_MAX_LENGTH, areTelegramUpdateJournalQueueOwnersEqual, getTelegramUpdateJournalBindingPath, isTelegramUpdateJournalQueueOwnerProcess, parseTelegramUpdateJournalQueueOwner, } from "./journal.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";
|
|
9
11
|
import { areTelegramQueueAdmissionReceiptsEqual, createTelegramQueueHandoff, removeTelegramQueueItemByReceipt, } from "./queue.js";
|
|
10
12
|
import { createTelegramUserPairingRuntime, getTelegramAuthorizationState, } from "./config.js";
|
|
11
13
|
export const TELEGRAM_PRIORITY_REACTIONS = [
|
|
@@ -43,23 +45,6 @@ export function collectTelegramReactionEmojis(reactions) {
|
|
|
43
45
|
function getTelegramReactionEmoji(emojis, candidates) {
|
|
44
46
|
return candidates.find((emoji) => emojis.has(emoji));
|
|
45
47
|
}
|
|
46
|
-
export function getTelegramQueueReactionDisposition(reactions) {
|
|
47
|
-
const emojis = collectTelegramReactionEmojis(reactions);
|
|
48
|
-
const suppressionEmoji = getTelegramReactionEmoji(emojis, TELEGRAM_REMOVAL_REACTION_EMOJIS);
|
|
49
|
-
const priorityEmoji = getTelegramReactionEmoji(emojis, TELEGRAM_PRIORITY_REACTION_EMOJIS);
|
|
50
|
-
if (suppressionEmoji && priorityEmoji) {
|
|
51
|
-
return {
|
|
52
|
-
kind: "priority-suppressed",
|
|
53
|
-
priorityEmoji,
|
|
54
|
-
suppressionEmoji,
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
if (suppressionEmoji)
|
|
58
|
-
return { kind: "suppressed", emoji: suppressionEmoji };
|
|
59
|
-
if (priorityEmoji)
|
|
60
|
-
return { kind: "priority", emoji: priorityEmoji };
|
|
61
|
-
return { kind: "default" };
|
|
62
|
-
}
|
|
63
48
|
function getTelegramQueueReactionTransition(oldReactions, newReactions) {
|
|
64
49
|
const oldEmojis = collectTelegramReactionEmojis(oldReactions);
|
|
65
50
|
const newEmojis = collectTelegramReactionEmojis(newReactions);
|
|
@@ -149,7 +134,7 @@ export function getAuthorizedTelegramGuestMessage(update) {
|
|
|
149
134
|
}
|
|
150
135
|
return guestMessage;
|
|
151
136
|
}
|
|
152
|
-
|
|
137
|
+
class TelegramForeignUpdateSettlementError extends Error {
|
|
153
138
|
settlement;
|
|
154
139
|
constructor(operation, settlement) {
|
|
155
140
|
super(`Telegram ${operation} forwarding did not settle: ${settlement.failureClass}.`);
|
|
@@ -175,6 +160,7 @@ function rejectTelegramForeignUpdateSettlement(settlement, operation, source) {
|
|
|
175
160
|
}
|
|
176
161
|
export const TELEGRAM_INTERNAL_AGENT_MESSAGE = Symbol("telegram.internalAgentMessage");
|
|
177
162
|
const TELEGRAM_UPDATE_ADMISSION_BINDING = Symbol("telegram.update-admission.binding");
|
|
163
|
+
const TELEGRAM_HELD_SOURCE_INSPECTION_LIMIT = 20;
|
|
178
164
|
function bindTelegramUpdateAdmissionCarrier(value, binding) {
|
|
179
165
|
if (!value || typeof value !== "object")
|
|
180
166
|
return value;
|
|
@@ -195,13 +181,14 @@ function getTelegramUpdateAdmissionBinding(value) {
|
|
|
195
181
|
? binding
|
|
196
182
|
: undefined;
|
|
197
183
|
}
|
|
198
|
-
export function bindTelegramUpdateAdmissionSource(update, report) {
|
|
184
|
+
export function bindTelegramUpdateAdmissionSource(update, report, controls) {
|
|
199
185
|
if (!Number.isSafeInteger(update.update_id) || update.update_id < 0) {
|
|
200
186
|
throw new TelegramUpdateAdmissionOutcomeError("Telegram update admission requires a safe update_id.");
|
|
201
187
|
}
|
|
202
188
|
const binding = {
|
|
203
189
|
sourceUpdateId: update.update_id,
|
|
204
190
|
report,
|
|
191
|
+
...controls,
|
|
205
192
|
};
|
|
206
193
|
const callbackQuery = update.callback_query
|
|
207
194
|
? bindTelegramUpdateAdmissionCarrier({
|
|
@@ -232,6 +219,24 @@ export function bindTelegramUpdateAdmissionSource(update, report) {
|
|
|
232
219
|
: {}),
|
|
233
220
|
};
|
|
234
221
|
}
|
|
222
|
+
/**
|
|
223
|
+
* Conservative read-only census: any retained entry naming this Thread, in any nested Telegram object or state,
|
|
224
|
+
* is unresolved custody. It cannot see updates still in transit before journal append.
|
|
225
|
+
*/
|
|
226
|
+
export function collectTelegramJournalThreadUpdateIds(entries, target) {
|
|
227
|
+
const matches = (value, depth) => {
|
|
228
|
+
if (depth > 8 || !value || typeof value !== "object")
|
|
229
|
+
return false;
|
|
230
|
+
const record = value;
|
|
231
|
+
if (record.message_thread_id === target.threadId) {
|
|
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);
|
|
239
|
+
}
|
|
235
240
|
export function collectTelegramAdmissionSourceUpdateIds(values) {
|
|
236
241
|
const sourceUpdateIds = new Set();
|
|
237
242
|
for (const value of values) {
|
|
@@ -241,27 +246,122 @@ export function collectTelegramAdmissionSourceUpdateIds(values) {
|
|
|
241
246
|
}
|
|
242
247
|
return [...sourceUpdateIds].sort((left, right) => left - right);
|
|
243
248
|
}
|
|
249
|
+
/** Observe the original journal entry, never the mutable routed carrier, through current worker authority. */
|
|
250
|
+
export function inspectTelegramDeferredSource(value) {
|
|
251
|
+
const execution = getTelegramUpdateExecutionFence(value);
|
|
252
|
+
if (execution?.isCurrent() !== true)
|
|
253
|
+
return undefined;
|
|
254
|
+
return getTelegramUpdateAdmissionBinding(value)?.inspectSource?.();
|
|
255
|
+
}
|
|
256
|
+
/** A route-owned pre-disposition acceptance publisher; wraps only this carrier, never the shared worker binding. */
|
|
257
|
+
export function bindTelegramUpdateCompletionAcceptance(value, publish) {
|
|
258
|
+
const binding = getTelegramUpdateAdmissionBinding(value);
|
|
259
|
+
if (!binding || getTelegramUpdateExecutionFence(value)?.isCurrent() !== true) {
|
|
260
|
+
throw new TelegramUpdateAdmissionOutcomeError("Telegram completion acceptance requires a live source carrier.");
|
|
261
|
+
}
|
|
262
|
+
let retained;
|
|
263
|
+
let queued = false;
|
|
264
|
+
const scoped = { ...binding, report(outcome) {
|
|
265
|
+
if (outcome.kind !== "complete") {
|
|
266
|
+
if (outcome.kind === "queued")
|
|
267
|
+
queued = true;
|
|
268
|
+
binding.report(outcome);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
assertTelegramUpdateExecutionCurrent(value);
|
|
272
|
+
// A command's trailing implicit completion cannot dispose receipt-owned or uncertain queued work.
|
|
273
|
+
if (queued) {
|
|
274
|
+
if (outcome.expectedSource)
|
|
275
|
+
throw new TelegramUpdateAdmissionOutcomeError("Telegram completion acceptance cannot dispose a queued source.");
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
if (!retained) {
|
|
279
|
+
const published = validateTelegramUpdateAdmissionOutcome({ kind: "complete", expectedSource: publish() }, binding.sourceUpdateId, new Set([binding.sourceUpdateId]));
|
|
280
|
+
if (published.kind !== "complete" || !published.expectedSource?.completionSha256) {
|
|
281
|
+
throw new TelegramUpdateAdmissionOutcomeError("Telegram completion acceptance lacks its scoped journal proof.");
|
|
282
|
+
}
|
|
283
|
+
retained = published.expectedSource;
|
|
284
|
+
}
|
|
285
|
+
assertTelegramUpdateExecutionCurrent(value);
|
|
286
|
+
if (outcome.expectedSource && (outcome.expectedSource.updateId !== retained.updateId ||
|
|
287
|
+
outcome.expectedSource.journalBindingKey !== retained.journalBindingKey || outcome.expectedSource.sourceSha256 !== retained.sourceSha256 ||
|
|
288
|
+
outcome.expectedSource.completionSha256 !== undefined && outcome.expectedSource.completionSha256 !== retained.completionSha256)) {
|
|
289
|
+
throw new TelegramUpdateAdmissionOutcomeError("Telegram completion acceptance conflicts with its reported source.");
|
|
290
|
+
}
|
|
291
|
+
binding.report({ kind: "complete", expectedSource: { ...retained } });
|
|
292
|
+
} };
|
|
293
|
+
return carryTelegramUpdateExecutionFence(value, bindTelegramUpdateAdmissionCarrier(value, scoped));
|
|
294
|
+
}
|
|
244
295
|
/** Report source completion; true means reported, not a durable settlement acknowledgement. */
|
|
245
|
-
export function reportTelegramUpdateCompleted(value) {
|
|
296
|
+
export function reportTelegramUpdateCompleted(value, expectedSource) {
|
|
246
297
|
const binding = getTelegramUpdateAdmissionBinding(value);
|
|
247
298
|
if (!binding)
|
|
248
299
|
return false;
|
|
249
300
|
const execution = getTelegramUpdateExecutionFence(value);
|
|
250
301
|
if (execution && !execution.isCurrent())
|
|
251
302
|
return false;
|
|
252
|
-
binding.report({ kind: "complete" });
|
|
303
|
+
binding.report(validateTelegramUpdateAdmissionOutcome({ kind: "complete", ...(expectedSource !== undefined ? { expectedSource } : {}) }, binding.sourceUpdateId, new Set([binding.sourceUpdateId])));
|
|
253
304
|
return true;
|
|
254
305
|
}
|
|
255
306
|
export function reportTelegramUpdateDeferred(value) {
|
|
256
307
|
const binding = getTelegramUpdateAdmissionBinding(value);
|
|
257
308
|
if (!binding)
|
|
258
309
|
return false;
|
|
310
|
+
const execution = getTelegramUpdateExecutionFence(value);
|
|
311
|
+
if (execution && !execution.isCurrent())
|
|
312
|
+
return false;
|
|
259
313
|
binding.report({ kind: "deferred" });
|
|
260
314
|
return true;
|
|
261
315
|
}
|
|
316
|
+
/** Exact source-bound cancellation; undefined means no eligible attempt was made. */
|
|
317
|
+
export function abandonTelegramDeferredUpdate(value, authority) {
|
|
318
|
+
return getTelegramUpdateAdmissionBinding(value)?.abandon?.(authority);
|
|
319
|
+
}
|
|
320
|
+
/** Capability for chooser publication; the eventual action still requires exact deferred authority. */
|
|
321
|
+
export function supportsTelegramDeferredAbandonment(value, journalBindingKey) {
|
|
322
|
+
return getTelegramUpdateAdmissionBinding(value)?.supportsAbandonment?.(journalBindingKey) === true;
|
|
323
|
+
}
|
|
324
|
+
/** Observe protected attempts through a current carrier; the UI still owns human authorization. */
|
|
325
|
+
export function inspectTelegramAbandoningUpdates(value, request) {
|
|
326
|
+
return getTelegramUpdateAdmissionBinding(value)?.inspectAbandoning?.(request);
|
|
327
|
+
}
|
|
328
|
+
/** Historical inspection is not human confirmation or permission to stop delivery. */
|
|
329
|
+
export function inspectTelegramHistoricalInputs(value, request) {
|
|
330
|
+
return getTelegramUpdateAdmissionBinding(value)?.inspectHistorical?.(request);
|
|
331
|
+
}
|
|
332
|
+
export function isTelegramHistoricalInput(value, matchesOriginal) {
|
|
333
|
+
return getTelegramUpdateAdmissionBinding(value)?.isHistorical?.(matchesOriginal) === true;
|
|
334
|
+
}
|
|
335
|
+
/** Routing's last-boundary hold when ownership changed after early classification. */
|
|
336
|
+
export function reportTelegramHistoricalRoutingReview(value) {
|
|
337
|
+
if (!isTelegramHistoricalInput(value) || getTelegramUpdateExecutionFence(value)?.isCurrent() !== true)
|
|
338
|
+
return false;
|
|
339
|
+
getTelegramUpdateAdmissionBinding(value).report({ kind: "deferred", routingReview: true });
|
|
340
|
+
return true;
|
|
341
|
+
}
|
|
342
|
+
/** Arm only a positively published chooser; the journal owns the immutable hour deadline. */
|
|
343
|
+
export function armTelegramRoutingInputs(values, operatorUserId) {
|
|
344
|
+
const first = values[0], execution = getTelegramUpdateExecutionFence(first);
|
|
345
|
+
if (!execution || values.some(value => getTelegramUpdateExecutionFence(value)?.signal !== execution.signal))
|
|
346
|
+
return undefined;
|
|
347
|
+
for (const value of values)
|
|
348
|
+
assertTelegramUpdateExecutionCurrent(value);
|
|
349
|
+
const ids = collectTelegramAdmissionSourceUpdateIds(values);
|
|
350
|
+
if (!ids.length)
|
|
351
|
+
return undefined;
|
|
352
|
+
return getTelegramUpdateAdmissionBinding(first)?.armRoutingInput?.(operatorUserId, ids);
|
|
353
|
+
}
|
|
354
|
+
/** Reserve source execution; an actual choice must also freeze its durable TTL before any effect. */
|
|
355
|
+
export function acquireTelegramUpdateRouting(value, select = false, sourceUpdateIds) {
|
|
356
|
+
assertTelegramUpdateExecutionCurrent(value);
|
|
357
|
+
return getTelegramUpdateAdmissionBinding(value)?.acquireRouting?.(select, sourceUpdateIds) ?? (() => { });
|
|
358
|
+
}
|
|
262
359
|
export function reportTelegramQueueAdmission(values, receipts) {
|
|
263
360
|
const bindings = new Map();
|
|
264
361
|
for (const value of values) {
|
|
362
|
+
const execution = getTelegramUpdateExecutionFence(value);
|
|
363
|
+
if (execution && !execution.isCurrent())
|
|
364
|
+
return false;
|
|
265
365
|
const binding = getTelegramUpdateAdmissionBinding(value);
|
|
266
366
|
if (!binding)
|
|
267
367
|
continue;
|
|
@@ -427,7 +527,13 @@ function getForeignTelegramMessageOwnership(target, deps) {
|
|
|
427
527
|
: undefined;
|
|
428
528
|
}
|
|
429
529
|
function getForeignTelegramCallbackOwnership(query, deps) {
|
|
430
|
-
const
|
|
530
|
+
const messageTarget = getTelegramCallbackMessageTarget(query);
|
|
531
|
+
const currentInstanceId = deps.getCurrentInstanceId?.();
|
|
532
|
+
const knownOwner = messageTarget && deps.getMessageOwnership?.(messageTarget.chatId, messageTarget.messageId);
|
|
533
|
+
// A locally published chooser stays local when Restore moves its Thread to a follower.
|
|
534
|
+
if (currentInstanceId && knownOwner?.instanceId === currentInstanceId)
|
|
535
|
+
return undefined;
|
|
536
|
+
const messageOwnership = getForeignTelegramMessageOwnership(messageTarget, deps);
|
|
431
537
|
const targetOwnership = getForeignTelegramTargetOwnership(query.message ? getTelegramMessageTarget(query.message) : undefined, deps);
|
|
432
538
|
if (!messageOwnership)
|
|
433
539
|
return targetOwnership;
|
|
@@ -727,7 +833,11 @@ export async function executeTelegramUpdatePlan(plan, deps) {
|
|
|
727
833
|
deps.handleUnboundTelegramTopicMessage) {
|
|
728
834
|
assertExecutionCurrent();
|
|
729
835
|
await deps.handleUnboundTelegramTopicMessage(plan.message, deps.ctx);
|
|
730
|
-
|
|
836
|
+
// A terminal historical hold intentionally suspends this exact carrier.
|
|
837
|
+
// Do not turn its acknowledged deferral into a retryable execution failure.
|
|
838
|
+
if (getTelegramUpdateExecutionFence(plan.message) !== deps.execution ||
|
|
839
|
+
getTelegramUpdateAdmissionBinding(plan.message)?.isHistoricalReviewHeld?.() !== true)
|
|
840
|
+
assertExecutionCurrent();
|
|
731
841
|
return;
|
|
732
842
|
}
|
|
733
843
|
if (plan.kind === "edited-message") {
|
|
@@ -746,10 +856,28 @@ export async function executeTelegramUpdatePlan(plan, deps) {
|
|
|
746
856
|
}
|
|
747
857
|
}
|
|
748
858
|
// --- Durable update worker ---
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
859
|
+
const TELEGRAM_UPDATE_RETRY_BASE_DELAY_MS = 1_000;
|
|
860
|
+
const TELEGRAM_UPDATE_RETRY_MAX_DELAY_MS = 60_000;
|
|
861
|
+
const TELEGRAM_UPDATE_WORKER_BATCH_SIZE = 64;
|
|
862
|
+
function normalizeTelegramQueueSourceCompletions(value, sourceUpdateIds, journalBindingKey, full = true) {
|
|
863
|
+
if (!Array.isArray(value) || (full ? value.length !== sourceUpdateIds.size : value.length > sourceUpdateIds.size) || value.length === 0)
|
|
864
|
+
throw new Error("Telegram scoped queue completion requires full source coverage.");
|
|
865
|
+
const seen = new Set(), scopes = new Set();
|
|
866
|
+
return value.map(source => {
|
|
867
|
+
if (!Number.isSafeInteger(source?.updateId) || !sourceUpdateIds.has(source.updateId) || seen.has(source.updateId)) {
|
|
868
|
+
throw new Error("Telegram scoped queue completion has invalid source membership.");
|
|
869
|
+
}
|
|
870
|
+
const normalized = validateTelegramUpdateAdmissionOutcome({ kind: "complete", expectedSource: source }, source.updateId, sourceUpdateIds);
|
|
871
|
+
if (normalized.kind !== "complete" || !normalized.expectedSource?.completionSha256 ||
|
|
872
|
+
normalized.expectedSource.journalBindingKey !== journalBindingKey || scopes.has(normalized.expectedSource.completionSha256)) {
|
|
873
|
+
throw new Error("Telegram scoped queue completion has invalid source scope.");
|
|
874
|
+
}
|
|
875
|
+
seen.add(source.updateId);
|
|
876
|
+
scopes.add(normalized.expectedSource.completionSha256);
|
|
877
|
+
return { ...normalized.expectedSource, completionSha256: normalized.expectedSource.completionSha256 };
|
|
878
|
+
}).sort((a, b) => a.updateId - b.updateId);
|
|
879
|
+
}
|
|
880
|
+
class TelegramUpdateAdmissionOutcomeError extends Error {
|
|
753
881
|
constructor(message) {
|
|
754
882
|
super(message);
|
|
755
883
|
this.name = "TelegramUpdateAdmissionOutcomeError";
|
|
@@ -759,20 +887,29 @@ const TELEGRAM_UPDATE_WORKER_EXECUTION_ABORTED = Symbol("telegram.update-worker.
|
|
|
759
887
|
function getTelegramUpdateWorkerStateSnapshot(state) {
|
|
760
888
|
return { ...state };
|
|
761
889
|
}
|
|
762
|
-
function isTelegramUpdateAdmissionRecord(value) {
|
|
763
|
-
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
764
|
-
}
|
|
765
|
-
function isTelegramUpdateAdmissionString(value) {
|
|
766
|
-
return typeof value === "string" && value.length > 0;
|
|
767
|
-
}
|
|
768
890
|
function validateTelegramUpdateAdmissionOutcome(value, currentUpdateId, claimableUpdateIds) {
|
|
769
891
|
if (!isTelegramUpdateAdmissionRecord(value)) {
|
|
770
892
|
throw new TelegramUpdateAdmissionOutcomeError(`Telegram update ${currentUpdateId} returned no admission outcome.`);
|
|
771
893
|
}
|
|
772
|
-
if (value.kind === "complete")
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
894
|
+
if (value.kind === "complete") {
|
|
895
|
+
if (value.expectedSource === undefined)
|
|
896
|
+
return { kind: "complete" };
|
|
897
|
+
const source = value.expectedSource;
|
|
898
|
+
if (!isTelegramUpdateAdmissionRecord(source) || Object.keys(source).some(key => !["journalBindingKey", "updateId", "sourceSha256", "completionSha256"].includes(key)) ||
|
|
899
|
+
source.updateId !== currentUpdateId || !isTelegramUpdateAdmissionString(source.journalBindingKey) ||
|
|
900
|
+
typeof source.sourceSha256 !== "string" || !/^[a-f0-9]{64}$/u.test(source.sourceSha256) ||
|
|
901
|
+
source.completionSha256 !== undefined && (typeof source.completionSha256 !== "string" || !/^[a-f0-9]{64}$/u.test(source.completionSha256))) {
|
|
902
|
+
throw new TelegramUpdateAdmissionOutcomeError(`Telegram update ${currentUpdateId} returned an invalid completion source.`);
|
|
903
|
+
}
|
|
904
|
+
return { kind: "complete", expectedSource: { journalBindingKey: source.journalBindingKey,
|
|
905
|
+
updateId: currentUpdateId, sourceSha256: source.sourceSha256,
|
|
906
|
+
...(source.completionSha256 !== undefined ? { completionSha256: source.completionSha256 } : {}) } };
|
|
907
|
+
}
|
|
908
|
+
if (value.kind === "deferred") {
|
|
909
|
+
if (value.routingReview !== undefined && value.routingReview !== true)
|
|
910
|
+
throw new TelegramUpdateAdmissionOutcomeError("Invalid historical review outcome.");
|
|
911
|
+
return { kind: "deferred", ...(value.routingReview === true ? { routingReview: true } : {}) };
|
|
912
|
+
}
|
|
776
913
|
if (value.kind === "queued") {
|
|
777
914
|
if ((value.queueKind !== "prompt" && value.queueKind !== "control") ||
|
|
778
915
|
!isTelegramUpdateAdmissionString(value.receiptId) ||
|
|
@@ -871,12 +1008,23 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
871
1008
|
throw new Error("Telegram update retry scheduling requires matching schedule and cancel ports.");
|
|
872
1009
|
}
|
|
873
1010
|
const getNowMs = deps.getNowMs ?? Date.now;
|
|
1011
|
+
const onHeldSourcesPrepared = deps.onHeldSourcesPrepared;
|
|
1012
|
+
const spendHistorical = deps.spendHistoricalInput === true;
|
|
1013
|
+
const routingJournal = deps.journal.routingInputs;
|
|
1014
|
+
const armRouting = routingJournal?.arm.bind(routingJournal);
|
|
1015
|
+
const selectRouting = routingJournal?.select.bind(routingJournal);
|
|
1016
|
+
const expireRouting = routingJournal?.expire.bind(routingJournal);
|
|
1017
|
+
const expireRoutingObserver = deps.expireRoutingInput;
|
|
874
1018
|
const batchSize = deps.batchSize ?? TELEGRAM_UPDATE_WORKER_BATCH_SIZE;
|
|
875
1019
|
if (!Number.isSafeInteger(batchSize) || batchSize <= 0) {
|
|
876
1020
|
throw new Error("Telegram update worker batch size must be positive.");
|
|
877
1021
|
}
|
|
878
1022
|
const yieldToEventLoop = deps.yieldToEventLoop ??
|
|
879
1023
|
(() => new Promise((resolve) => setTimeout(resolve, 0)));
|
|
1024
|
+
const completeQueuedExact = deps.journal.completeQueuedExact?.bind(deps.journal);
|
|
1025
|
+
const completeExact = deps.journal.removeCompletedExact?.bind(deps.journal);
|
|
1026
|
+
const inspectCompletion = deps.journal.inspectSourceCompletion?.bind(deps.journal);
|
|
1027
|
+
const observeQueuedCompletion = deps.onQueueReceiptCompleted;
|
|
880
1028
|
const fallbackQueueOwnerInstanceId = `worker-${randomUUID()}`;
|
|
881
1029
|
const fallbackQueueOwnerProcessId = process.pid > 0 ? process.pid : 1;
|
|
882
1030
|
const createAbortController = deps.createAbortController ?? (() => new AbortController());
|
|
@@ -890,6 +1038,13 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
890
1038
|
processBirthId: `${fallbackQueueOwnerProcessId}:${fallbackQueueOwnerInstanceId}`,
|
|
891
1039
|
sessionGeneration: generation,
|
|
892
1040
|
});
|
|
1041
|
+
const isQueueOwnerIdentityCurrent = (expected) => {
|
|
1042
|
+
if (!deps.getQueueOwnerIdentity)
|
|
1043
|
+
return true;
|
|
1044
|
+
const live = deps.getQueueOwnerIdentity(expected.ctx), captured = expected.queueOwnerIdentity;
|
|
1045
|
+
return !!live && live.instanceId === captured.instanceId && live.processId === captured.processId &&
|
|
1046
|
+
live.processBirthId === captured.processBirthId && live.sessionGeneration === captured.sessionGeneration;
|
|
1047
|
+
};
|
|
893
1048
|
const state = {
|
|
894
1049
|
phase: "stopped",
|
|
895
1050
|
generation: 0,
|
|
@@ -903,8 +1058,11 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
903
1058
|
unsettledExecutionCount: 0,
|
|
904
1059
|
};
|
|
905
1060
|
const claims = new Map();
|
|
1061
|
+
const deferredSources = new Map();
|
|
906
1062
|
const unsettledExecutionsByUpdateId = new Map();
|
|
907
1063
|
const committedQueueReceipts = new Map();
|
|
1064
|
+
const requiredScopedQueueReceipts = new Set();
|
|
1065
|
+
const scopedQueueCompletionAttempts = new Map();
|
|
908
1066
|
const unsettledExecutions = new Set();
|
|
909
1067
|
let owner;
|
|
910
1068
|
let drainPromise;
|
|
@@ -914,6 +1072,8 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
914
1072
|
let retryTimer;
|
|
915
1073
|
let retryTimerAtMs;
|
|
916
1074
|
let retryTimerToken;
|
|
1075
|
+
let routingTimer;
|
|
1076
|
+
let routingTimerToken;
|
|
917
1077
|
let launchDrain = () => { };
|
|
918
1078
|
const recordRuntimeEvent = (error, details) => {
|
|
919
1079
|
try {
|
|
@@ -934,14 +1094,28 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
934
1094
|
const updateClaimCounts = () => {
|
|
935
1095
|
let deferredClaimCount = 0;
|
|
936
1096
|
let queuedClaimCount = 0;
|
|
1097
|
+
let abandoningClaimCount = 0;
|
|
1098
|
+
let historicalClaimCount = 0;
|
|
937
1099
|
for (const claim of claims.values()) {
|
|
938
1100
|
if (claim === "queued")
|
|
939
1101
|
queuedClaimCount += 1;
|
|
940
1102
|
else
|
|
941
1103
|
deferredClaimCount += 1;
|
|
1104
|
+
if (claim === "abandoning")
|
|
1105
|
+
abandoningClaimCount += 1;
|
|
1106
|
+
if (claim === "historical" || claim === "retained")
|
|
1107
|
+
historicalClaimCount += 1;
|
|
942
1108
|
}
|
|
943
1109
|
state.deferredClaimCount = deferredClaimCount;
|
|
944
1110
|
state.queuedClaimCount = queuedClaimCount;
|
|
1111
|
+
if (abandoningClaimCount > 0)
|
|
1112
|
+
state.abandoningClaimCount = abandoningClaimCount;
|
|
1113
|
+
else
|
|
1114
|
+
delete state.abandoningClaimCount;
|
|
1115
|
+
if (historicalClaimCount > 0)
|
|
1116
|
+
state.historicalClaimCount = historicalClaimCount;
|
|
1117
|
+
else
|
|
1118
|
+
delete state.historicalClaimCount;
|
|
945
1119
|
};
|
|
946
1120
|
const releaseDeferredClaims = (updateIds) => {
|
|
947
1121
|
let changed = false;
|
|
@@ -949,6 +1123,7 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
949
1123
|
if (claims.get(updateId) !== "deferred")
|
|
950
1124
|
continue;
|
|
951
1125
|
claims.delete(updateId);
|
|
1126
|
+
deferredSources.delete(updateId);
|
|
952
1127
|
changed = true;
|
|
953
1128
|
}
|
|
954
1129
|
if (!changed)
|
|
@@ -992,7 +1167,13 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
992
1167
|
? { journalBindingKey: deps.getJournalBindingKey() }
|
|
993
1168
|
: {}),
|
|
994
1169
|
});
|
|
995
|
-
const
|
|
1170
|
+
const inspectQueueReceipt = deps.journal.isQueueReceiptCurrent?.bind(deps.journal);
|
|
1171
|
+
const inspectQueuedSources = deps.journal.inspectQueuedReceipt?.bind(deps.journal);
|
|
1172
|
+
const prepareQueueReceipt = deps.beforeQueueReceiptPublished;
|
|
1173
|
+
const pendingQueuePublications = new Map();
|
|
1174
|
+
const publishCommittedQueueReceipt = async (receipt, queueOwner, expectedOwner) => {
|
|
1175
|
+
const ctx = expectedOwner.ctx;
|
|
1176
|
+
let sourceCompletions;
|
|
996
1177
|
const normalized = bindQueueReceiptToJournal(receipt);
|
|
997
1178
|
const existing = committedQueueReceipts.get(receipt.receiptId);
|
|
998
1179
|
if (existing) {
|
|
@@ -1002,28 +1183,90 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1002
1183
|
}
|
|
1003
1184
|
return false;
|
|
1004
1185
|
}
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1186
|
+
if (prepareQueueReceipt) {
|
|
1187
|
+
const pending = pendingQueuePublications.get(normalized.receiptId);
|
|
1188
|
+
if (pending) {
|
|
1189
|
+
if (pending.owner !== expectedOwner || !areTelegramQueueAdmissionReceiptsEqual(pending.receipt, normalized) ||
|
|
1190
|
+
!areTelegramUpdateJournalQueueOwnersEqual(pending.queueOwner, queueOwner)) {
|
|
1191
|
+
throw new TelegramUpdateAdmissionOutcomeError("Telegram queue publication has conflicting in-flight authority.");
|
|
1192
|
+
}
|
|
1193
|
+
await pending.task;
|
|
1194
|
+
return false;
|
|
1195
|
+
}
|
|
1196
|
+
const journalBindingKey = deps.getJournalBindingKey?.();
|
|
1197
|
+
const current = () => owner === expectedOwner && !expectedOwner.controller.signal.aborted && deps.hasAuthority(ctx) && deps.isContextCurrent?.(ctx) !== false &&
|
|
1198
|
+
isQueueOwnerIdentityCurrent(expectedOwner) && deps.getJournalBindingKey?.() === journalBindingKey;
|
|
1199
|
+
if (!journalBindingKey || !current() || !inspectQueueReceipt ||
|
|
1200
|
+
inspectQueueReceipt(normalizeQueueReceipt(normalized), { ...queueOwner }) !== true || !current()) {
|
|
1201
|
+
throw new TelegramUpdateAdmissionOutcomeError("Telegram queue publication requires exact current receipt inspection.");
|
|
1202
|
+
}
|
|
1203
|
+
const task = Promise.resolve().then(async () => {
|
|
1204
|
+
if (!current())
|
|
1205
|
+
throw new TelegramUpdateAdmissionOutcomeError("Telegram queue publication authority ended before acceptance.");
|
|
1206
|
+
const prepared = await prepareQueueReceipt(normalizeQueueReceipt(normalized), { ...queueOwner }, ctx, current);
|
|
1207
|
+
if (prepared !== undefined) {
|
|
1208
|
+
sourceCompletions = normalizeTelegramQueueSourceCompletions(prepared, new Set(normalized.sourceUpdateIds), journalBindingKey, false);
|
|
1209
|
+
if (!completeQueuedExact || !inspectCompletion)
|
|
1210
|
+
throw new Error("Telegram scoped queue terminal capabilities are unavailable.");
|
|
1211
|
+
if (sourceCompletions.length !== normalized.sourceUpdateIds.length) {
|
|
1212
|
+
if (!current() || !inspectQueuedSources)
|
|
1213
|
+
throw new Error("Telegram partial queue scopes require strict current whole-receipt origin inspection.");
|
|
1214
|
+
const expected = { queueKind: normalized.queueKind, receiptId: normalized.receiptId,
|
|
1215
|
+
sourceUpdateIds: [...normalized.sourceUpdateIds], queueOwner: { ...queueOwner } };
|
|
1216
|
+
const proof = inspectQueuedSources(structuredClone(expected));
|
|
1217
|
+
if (!current() || !proof || !isDeepStrictEqual(proof.receipt, expected) ||
|
|
1218
|
+
!Array.isArray(proof.sources) || proof.sources.length !== expected.sourceUpdateIds.length ||
|
|
1219
|
+
proof.sources.some((source, index) => source.updateId !== expected.sourceUpdateIds[index] || typeof source.sourceSha256 !== "string" || !/^[a-f0-9]{64}$/u.test(source.sourceSha256)) ||
|
|
1220
|
+
sourceCompletions.some(scope => proof.sources.find(source => source.updateId === scope.updateId)?.sourceSha256 !== scope.sourceSha256)) {
|
|
1221
|
+
throw new Error("Telegram partial queue scope origin was not confirmed under whole-receipt authority.");
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
for (const { journalBindingKey: _binding, ...completion } of sourceCompletions) {
|
|
1225
|
+
if (!current() || inspectCompletion({ ...completion }) !== undefined || !current()) {
|
|
1226
|
+
throw new Error("Telegram queued source completion authority is contradictory.");
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
if (!current() || inspectQueueReceipt(normalizeQueueReceipt(normalized), { ...queueOwner }) !== true || !current()) {
|
|
1231
|
+
throw new TelegramUpdateAdmissionOutcomeError("Telegram queue publication authority changed after acceptance.");
|
|
1232
|
+
}
|
|
1233
|
+
publishReady();
|
|
1234
|
+
}).finally(() => { if (pendingQueuePublications.get(normalized.receiptId)?.task === task)
|
|
1235
|
+
pendingQueuePublications.delete(normalized.receiptId); });
|
|
1236
|
+
pendingQueuePublications.set(normalized.receiptId, { receipt: normalizeQueueReceipt(normalized), queueOwner: { ...queueOwner }, owner: expectedOwner, task });
|
|
1237
|
+
await task;
|
|
1238
|
+
return true;
|
|
1011
1239
|
}
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1240
|
+
publishReady();
|
|
1241
|
+
return true;
|
|
1242
|
+
function publishReady() {
|
|
1243
|
+
committedQueueReceipts.set(receipt.receiptId, {
|
|
1244
|
+
receipt: normalized,
|
|
1245
|
+
queueOwner: { ...queueOwner },
|
|
1246
|
+
...(sourceCompletions ? { sourceCompletions: sourceCompletions.map(value => ({ ...value })) } : {}),
|
|
1016
1247
|
});
|
|
1248
|
+
if (sourceCompletions)
|
|
1249
|
+
requiredScopedQueueReceipts.add(receipt.receiptId);
|
|
1250
|
+
try {
|
|
1251
|
+
deps.onQueueReceiptCommitted?.(normalizeQueueReceipt(normalized), ctx);
|
|
1252
|
+
}
|
|
1253
|
+
catch (error) {
|
|
1254
|
+
recordRuntimeEvent(error, {
|
|
1255
|
+
phase: "queue-receipt-observer",
|
|
1256
|
+
receiptId: normalized.receiptId,
|
|
1257
|
+
});
|
|
1258
|
+
}
|
|
1017
1259
|
}
|
|
1018
|
-
return true;
|
|
1019
1260
|
};
|
|
1020
1261
|
const getCurrentQueueReceipt = (receipt) => {
|
|
1262
|
+
if (scopedQueueCompletionAttempts.has(receipt.receiptId))
|
|
1263
|
+
return undefined;
|
|
1021
1264
|
const committed = committedQueueReceipts.get(receipt.receiptId);
|
|
1022
1265
|
if (!committed || !areTelegramQueueAdmissionReceiptsEqual(committed.receipt, normalizeQueueReceipt(receipt)))
|
|
1023
1266
|
return undefined;
|
|
1024
1267
|
try {
|
|
1025
|
-
if (
|
|
1026
|
-
|
|
1268
|
+
if (inspectQueueReceipt &&
|
|
1269
|
+
inspectQueueReceipt(normalizeQueueReceipt(committed.receipt), { ...committed.queueOwner }) !== true) {
|
|
1027
1270
|
return undefined;
|
|
1028
1271
|
}
|
|
1029
1272
|
}
|
|
@@ -1066,7 +1309,7 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1066
1309
|
launchDrain();
|
|
1067
1310
|
}, Math.max(0, nextRetryAtMs - getNowMs()));
|
|
1068
1311
|
};
|
|
1069
|
-
const refreshJournalState = (snapshot, expectedOwner) => {
|
|
1312
|
+
const refreshJournalState = async (snapshot, expectedOwner) => {
|
|
1070
1313
|
// Validate the whole snapshot before reconstructing any queue authority.
|
|
1071
1314
|
if ((snapshot.version !== TELEGRAM_UPDATE_JOURNAL_VERSION &&
|
|
1072
1315
|
snapshot.version !== TELEGRAM_UPDATE_JOURNAL_EXCLUSION_VERSION &&
|
|
@@ -1095,7 +1338,7 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1095
1338
|
oldestAdmittedAtMs === undefined
|
|
1096
1339
|
? entry.admittedAtMs
|
|
1097
1340
|
: Math.min(oldestAdmittedAtMs, entry.admittedAtMs);
|
|
1098
|
-
if (entry.state === "retry-wait" && entry.nextRetryAtMs !== undefined) {
|
|
1341
|
+
if (entry.state === "retry-wait" && entry.nextRetryAtMs !== undefined && !claims.has(entry.updateId)) {
|
|
1099
1342
|
scheduledRetryAtMs =
|
|
1100
1343
|
scheduledRetryAtMs === undefined
|
|
1101
1344
|
? entry.nextRetryAtMs
|
|
@@ -1156,17 +1399,22 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1156
1399
|
}
|
|
1157
1400
|
}
|
|
1158
1401
|
for (const [updateId, claim] of claims) {
|
|
1159
|
-
if (!availableUpdateIds.has(updateId) ||
|
|
1402
|
+
if ((claim !== "abandoning" && !availableUpdateIds.has(updateId)) ||
|
|
1160
1403
|
(claim === "queued" && !locallyOwnedQueuedUpdateIds.has(updateId))) {
|
|
1161
1404
|
claims.delete(updateId);
|
|
1162
1405
|
}
|
|
1163
1406
|
}
|
|
1407
|
+
for (const updateId of deferredSources.keys()) {
|
|
1408
|
+
const claim = claims.get(updateId);
|
|
1409
|
+
if (claim !== "deferred" && claim !== "abandoning" && claim !== "historical" && claim !== "retained")
|
|
1410
|
+
deferredSources.delete(updateId);
|
|
1411
|
+
}
|
|
1164
1412
|
for (const [receiptId, receipt] of queuedReceiptEntries) {
|
|
1165
|
-
publishCommittedQueueReceipt({
|
|
1413
|
+
await publishCommittedQueueReceipt({
|
|
1166
1414
|
queueKind: receipt.queueKind,
|
|
1167
1415
|
receiptId,
|
|
1168
1416
|
sourceUpdateIds: receipt.sourceUpdateIds,
|
|
1169
|
-
}, receipt.queueOwner, expectedOwner
|
|
1417
|
+
}, receipt.queueOwner, expectedOwner);
|
|
1170
1418
|
}
|
|
1171
1419
|
for (const [receiptId] of committedQueueReceipts) {
|
|
1172
1420
|
if (!queuedReceiptEntries.has(receiptId)) {
|
|
@@ -1307,13 +1555,7 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1307
1555
|
for (const sourceUpdateId of normalized.sourceUpdateIds) {
|
|
1308
1556
|
claims.set(sourceUpdateId, "queued");
|
|
1309
1557
|
}
|
|
1310
|
-
|
|
1311
|
-
publishCommittedQueueReceipt(normalized, queueOwner, expectedOwner.ctx);
|
|
1312
|
-
}
|
|
1313
|
-
catch (error) {
|
|
1314
|
-
return blockWithFailure("invalid-outcome", "queue-receipt-publish", error, currentUpdateId);
|
|
1315
|
-
}
|
|
1316
|
-
return "committed";
|
|
1558
|
+
return publishCommittedQueueReceipt(normalized, queueOwner, expectedOwner).then(() => "committed", error => blockWithFailure("invalid-outcome", "queue-receipt-publish", error, currentUpdateId));
|
|
1317
1559
|
};
|
|
1318
1560
|
const persistExecutionFailure = (expectedOwner, entry, error) => {
|
|
1319
1561
|
const authorityResult = checkAuthority(expectedOwner, entry.updateId);
|
|
@@ -1372,14 +1614,52 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1372
1614
|
transition(disposition, entry.updateId);
|
|
1373
1615
|
return disposition;
|
|
1374
1616
|
};
|
|
1375
|
-
const commitCompletedBatch = (expectedOwner,
|
|
1617
|
+
const commitCompletedBatch = (expectedOwner, completions) => {
|
|
1618
|
+
const updateIds = completions.map(value => value.updateId);
|
|
1376
1619
|
if (updateIds.length === 0)
|
|
1377
1620
|
return undefined;
|
|
1378
1621
|
const commitAuthority = checkAuthority(expectedOwner, updateIds[updateIds.length - 1]);
|
|
1379
1622
|
if (commitAuthority)
|
|
1380
1623
|
return commitAuthority;
|
|
1624
|
+
const journalBindingKey = deps.getJournalBindingKey?.();
|
|
1381
1625
|
try {
|
|
1382
|
-
const
|
|
1626
|
+
const expectedSources = completions.flatMap(value => value.expectedSource ? [value.expectedSource] : []);
|
|
1627
|
+
if (expectedSources.some(value => value.journalBindingKey !== journalBindingKey) ||
|
|
1628
|
+
(expectedSources.length > 0 && (deps.isContextCurrent?.(expectedOwner.ctx) === false || !isQueueOwnerIdentityCurrent(expectedOwner)))) {
|
|
1629
|
+
throw new Error("Telegram guarded completion source authority changed.");
|
|
1630
|
+
}
|
|
1631
|
+
const scoped = expectedSources.flatMap(({ updateId, sourceSha256, completionSha256 }) => completionSha256 !== undefined
|
|
1632
|
+
? [{ updateId, sourceSha256, completionSha256 }] : []).sort((a, b) => a.updateId - b.updateId);
|
|
1633
|
+
if (scoped.length > 0 && !inspectCompletion)
|
|
1634
|
+
throw new Error("Telegram source completion inspection is unavailable.");
|
|
1635
|
+
let removed;
|
|
1636
|
+
if (expectedSources.length > 0) {
|
|
1637
|
+
if (!completeExact)
|
|
1638
|
+
throw new Error("Telegram exact source completion is unavailable.");
|
|
1639
|
+
removed = completeExact(updateIds, expectedSources.map(({ updateId, sourceSha256 }) => ({ updateId, sourceSha256 })), scoped.length > 0 ? scoped.map(completion => ({ ...completion })) : undefined);
|
|
1640
|
+
}
|
|
1641
|
+
else
|
|
1642
|
+
removed = deps.journal.removeCompleted(updateIds);
|
|
1643
|
+
if (scoped.length > 0) {
|
|
1644
|
+
const afterCommit = checkAuthority(expectedOwner, updateIds.at(-1));
|
|
1645
|
+
if (afterCommit)
|
|
1646
|
+
return afterCommit;
|
|
1647
|
+
if (deps.isContextCurrent?.(expectedOwner.ctx) === false || !isQueueOwnerIdentityCurrent(expectedOwner) ||
|
|
1648
|
+
deps.getJournalBindingKey?.() !== journalBindingKey)
|
|
1649
|
+
throw new Error("Telegram source completion ACK authority changed.");
|
|
1650
|
+
if (!isDeepStrictEqual(removed.sourceCompletions, scoped))
|
|
1651
|
+
throw new Error("Telegram source completion ACK was not confirmed.");
|
|
1652
|
+
for (const completion of scoped) {
|
|
1653
|
+
if (!isDeepStrictEqual(inspectCompletion({ ...completion }), completion))
|
|
1654
|
+
throw new Error("Telegram source completion ACK was not retained.");
|
|
1655
|
+
const afterRead = checkAuthority(expectedOwner, completion.updateId);
|
|
1656
|
+
if (afterRead)
|
|
1657
|
+
return afterRead;
|
|
1658
|
+
if (deps.isContextCurrent?.(expectedOwner.ctx) === false || !isQueueOwnerIdentityCurrent(expectedOwner) ||
|
|
1659
|
+
deps.getJournalBindingKey?.() !== journalBindingKey)
|
|
1660
|
+
throw new Error("Telegram source completion ACK authority changed.");
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1383
1663
|
const removedIds = new Set(removed.removedUpdateIds);
|
|
1384
1664
|
if (updateIds.some((updateId) => !removedIds.has(updateId))) {
|
|
1385
1665
|
throw new Error("Telegram update batch changed before completion commit.");
|
|
@@ -1389,12 +1669,16 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1389
1669
|
return blockWithFailure("journal-write", "completion-commit", error, updateIds[updateIds.length - 1]);
|
|
1390
1670
|
}
|
|
1391
1671
|
const completedAtMs = getNowMs();
|
|
1392
|
-
for (const updateId of
|
|
1672
|
+
for (const { updateId, spent } of completions) {
|
|
1393
1673
|
claims.delete(updateId);
|
|
1674
|
+
deferredSources.delete(updateId);
|
|
1675
|
+
// Spending is disposition, not completion: no task, cleanup or session observer runs.
|
|
1676
|
+
if (spent)
|
|
1677
|
+
continue;
|
|
1394
1678
|
state.lastCompletedUpdateId = updateId;
|
|
1395
1679
|
state.lastCompletedAtMs = completedAtMs;
|
|
1396
1680
|
try {
|
|
1397
|
-
deps.onUpdateCompleted?.(updateId, expectedOwner.ctx);
|
|
1681
|
+
deps.onUpdateCompleted?.(updateId, expectedOwner.ctx, journalBindingKey);
|
|
1398
1682
|
}
|
|
1399
1683
|
catch (error) {
|
|
1400
1684
|
recordRuntimeEvent(error, {
|
|
@@ -1405,6 +1689,111 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1405
1689
|
}
|
|
1406
1690
|
return undefined;
|
|
1407
1691
|
};
|
|
1692
|
+
const clearRoutingTimer = () => {
|
|
1693
|
+
if (routingTimer !== undefined)
|
|
1694
|
+
cancelRetry(routingTimer);
|
|
1695
|
+
routingTimer = undefined;
|
|
1696
|
+
routingTimerToken = undefined;
|
|
1697
|
+
};
|
|
1698
|
+
const reconcileRoutingInputs = async (snapshot, expectedOwner) => {
|
|
1699
|
+
clearRoutingTimer();
|
|
1700
|
+
if (!isTelegramUpdateJournalLegacyFamilyVersion(snapshot.version) || !expireRouting || !expireRoutingObserver)
|
|
1701
|
+
return false;
|
|
1702
|
+
let nextAtMs, changed = false;
|
|
1703
|
+
const binding = deps.getJournalBindingKey?.(), snapshotIds = new Set(snapshot.entries.map(entry => entry.updateId));
|
|
1704
|
+
// Lost removal ACKs reconcile only the exact held original through the retained copy/tombstone.
|
|
1705
|
+
const candidates = [...snapshot.entries, ...[...deferredSources.values()]
|
|
1706
|
+
.filter(source => source.journalBindingKey === binding && !snapshotIds.has(source.entry.updateId))
|
|
1707
|
+
.map(source => source.entry)];
|
|
1708
|
+
for (const entry of candidates) {
|
|
1709
|
+
const lifetime = entry.routingInput;
|
|
1710
|
+
if (claims.get(entry.updateId) === "retained" || (expectedOwner.startupUpdateIds?.has(entry.updateId) &&
|
|
1711
|
+
!claims.has(entry.updateId) && deps.shouldReviewHistoricalInput))
|
|
1712
|
+
continue;
|
|
1713
|
+
if (!lifetime || lifetime.phase !== "waiting" || entry.state !== "pending" || !binding || unsettledExecutionsByUpdateId.get(entry.updateId)?.size)
|
|
1714
|
+
continue;
|
|
1715
|
+
if (getNowMs() < lifetime.expiresAtMs) {
|
|
1716
|
+
nextAtMs = Math.min(nextAtMs ?? Infinity, lifetime.expiresAtMs);
|
|
1717
|
+
continue;
|
|
1718
|
+
}
|
|
1719
|
+
const existing = deferredSources.get(entry.updateId);
|
|
1720
|
+
if (existing && !isDeepStrictEqual(existing.entry, entry))
|
|
1721
|
+
continue;
|
|
1722
|
+
const source = existing ?? { entry: structuredClone(entry), journalBindingKey: binding, historical: true };
|
|
1723
|
+
deferredSources.set(entry.updateId, source);
|
|
1724
|
+
if (!claims.has(entry.updateId))
|
|
1725
|
+
claims.set(entry.updateId, "historical");
|
|
1726
|
+
const current = () => owner === expectedOwner && !expectedOwner.controller.signal.aborted &&
|
|
1727
|
+
deps.isContextCurrent?.(expectedOwner.ctx) !== false && deps.getJournalBindingKey?.() === binding &&
|
|
1728
|
+
deps.hasAuthority(expectedOwner.ctx) && isQueueOwnerIdentityCurrent(expectedOwner) && deferredSources.get(entry.updateId) === source &&
|
|
1729
|
+
isDeepStrictEqual(source.entry, entry) && claims.get(entry.updateId) !== "queued";
|
|
1730
|
+
let committed;
|
|
1731
|
+
try {
|
|
1732
|
+
if (current())
|
|
1733
|
+
await expireRoutingObserver({ original: structuredClone(entry), journalBindingKey: binding, isCurrent: current,
|
|
1734
|
+
expire() {
|
|
1735
|
+
if (!current())
|
|
1736
|
+
return undefined;
|
|
1737
|
+
if (committed)
|
|
1738
|
+
return { ...committed, duplicate: true };
|
|
1739
|
+
claims.set(entry.updateId, "abandoning");
|
|
1740
|
+
committed = expireRouting({ entry: source.entry, journalBindingKey: binding, operatorUserId: lifetime.operatorUserId, isCurrent: current });
|
|
1741
|
+
claims.delete(entry.updateId);
|
|
1742
|
+
deferredSources.delete(entry.updateId);
|
|
1743
|
+
state.journalEntryCount = committed.entryCount;
|
|
1744
|
+
state.journalSerializedBytes = committed.serializedBytes;
|
|
1745
|
+
updateClaimCounts();
|
|
1746
|
+
notifyStateChange();
|
|
1747
|
+
changed = true;
|
|
1748
|
+
return committed;
|
|
1749
|
+
} }, expectedOwner.ctx, expectedOwner.controller.signal);
|
|
1750
|
+
}
|
|
1751
|
+
catch (error) {
|
|
1752
|
+
recordRuntimeEvent(error, { phase: "routing-input-expiry", updateId: entry.updateId });
|
|
1753
|
+
}
|
|
1754
|
+
if (!committed)
|
|
1755
|
+
nextAtMs = Math.min(nextAtMs ?? Infinity, getNowMs() + 60_000);
|
|
1756
|
+
}
|
|
1757
|
+
if (nextAtMs !== undefined && owner === expectedOwner && !expectedOwner.controller.signal.aborted) {
|
|
1758
|
+
const token = {};
|
|
1759
|
+
routingTimerToken = token;
|
|
1760
|
+
routingTimer = scheduleRetry(() => {
|
|
1761
|
+
if (routingTimerToken !== token || owner !== expectedOwner || expectedOwner.controller.signal.aborted)
|
|
1762
|
+
return;
|
|
1763
|
+
routingTimer = undefined;
|
|
1764
|
+
routingTimerToken = undefined;
|
|
1765
|
+
pendingSignal = true;
|
|
1766
|
+
launchDrain();
|
|
1767
|
+
}, Math.max(1, nextAtMs - getNowMs()));
|
|
1768
|
+
}
|
|
1769
|
+
return changed;
|
|
1770
|
+
};
|
|
1771
|
+
const mutateRoutingSource = (input, select) => {
|
|
1772
|
+
const expectedOwner = owner, ids = [...input.sourceUpdateIds];
|
|
1773
|
+
if (!ids.length || !ids.includes(input.updateId) || new Set(ids).size !== ids.length || ids.some(id => !Number.isSafeInteger(id) || id < 0))
|
|
1774
|
+
return undefined;
|
|
1775
|
+
const sources = ids.map(id => deferredSources.get(id)), binding = sources[0]?.journalBindingKey;
|
|
1776
|
+
const current = () => !!expectedOwner && owner === expectedOwner && expectedOwner.controller.signal === input.signal && !input.signal.aborted &&
|
|
1777
|
+
!!binding && sources.every((source, index) => !!source && deferredSources.get(ids[index]) === source && !source.historical &&
|
|
1778
|
+
source.journalBindingKey === binding && claims.get(ids[index]) !== "queued" && claims.get(ids[index]) !== "abandoning") &&
|
|
1779
|
+
deps.getJournalBindingKey?.() === binding && deps.isContextCurrent?.(expectedOwner.ctx) !== false &&
|
|
1780
|
+
deps.hasAuthority(expectedOwner.ctx) && isQueueOwnerIdentityCurrent(expectedOwner);
|
|
1781
|
+
if (!current() || !armRouting || !selectRouting || !expireRoutingObserver)
|
|
1782
|
+
return undefined;
|
|
1783
|
+
const authority = { journalBindingKey: binding, entries: sources.map(source => structuredClone(source.entry)), operatorUserId: input.operatorUserId, isCurrent: current };
|
|
1784
|
+
const result = select ? selectRouting(authority) : { issued: false, entries: armRouting({ ...authority, publishedAtMs: getNowMs() }) };
|
|
1785
|
+
if (result.entries.length !== ids.length || result.entries.some((entry, index) => entry.updateId !== ids[index] || !entry.routingInput) || !current()) {
|
|
1786
|
+
throw new Error("Telegram routing input lifetime ACK was not confirmed.");
|
|
1787
|
+
}
|
|
1788
|
+
const retained = deps.journal.read().entries;
|
|
1789
|
+
if (result.entries.some(entry => !isDeepStrictEqual(retained.find(value => value.updateId === entry.updateId), entry)) || !current()) {
|
|
1790
|
+
throw new Error("Telegram routing input lifetime ACK was not retained.");
|
|
1791
|
+
}
|
|
1792
|
+
result.entries.forEach((entry, index) => { sources[index].entry = structuredClone(entry); });
|
|
1793
|
+
pendingSignal = true;
|
|
1794
|
+
launchDrain();
|
|
1795
|
+
return { issued: result.issued, lifetime: { ...result.entries[0].routingInput } };
|
|
1796
|
+
};
|
|
1408
1797
|
const drain = async (expectedOwner) => {
|
|
1409
1798
|
while (owner === expectedOwner && !expectedOwner.controller.signal.aborted) {
|
|
1410
1799
|
const authorityResult = checkAuthority(expectedOwner);
|
|
@@ -1414,11 +1803,18 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1414
1803
|
let scheduledRetryAtMs;
|
|
1415
1804
|
try {
|
|
1416
1805
|
snapshot = deps.journal.read();
|
|
1417
|
-
scheduledRetryAtMs = refreshJournalState(snapshot, expectedOwner);
|
|
1806
|
+
scheduledRetryAtMs = await refreshJournalState(snapshot, expectedOwner);
|
|
1807
|
+
// One complete validated baseline per generation, before batching or execution.
|
|
1808
|
+
// Replayed v1 entries do not prove that a prior process never forwarded them.
|
|
1809
|
+
expectedOwner.startupUpdateIds ??= new Set(snapshot.entries.map(entry => entry.updateId));
|
|
1418
1810
|
}
|
|
1419
1811
|
catch (error) {
|
|
1420
1812
|
return blockWithFailure("journal-read", "journal-read", error);
|
|
1421
1813
|
}
|
|
1814
|
+
if (await reconcileRoutingInputs(snapshot, expectedOwner))
|
|
1815
|
+
continue;
|
|
1816
|
+
if (owner !== expectedOwner || expectedOwner.controller.signal.aborted)
|
|
1817
|
+
return "aborted";
|
|
1422
1818
|
const nowMs = getNowMs();
|
|
1423
1819
|
const entries = [];
|
|
1424
1820
|
let hasMoreEntries = false;
|
|
@@ -1489,9 +1885,105 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1489
1885
|
break;
|
|
1490
1886
|
}
|
|
1491
1887
|
if (entry.preApprovalExcluded === true) {
|
|
1492
|
-
completedUpdateIds.push(entry.updateId);
|
|
1888
|
+
completedUpdateIds.push({ updateId: entry.updateId });
|
|
1493
1889
|
continue;
|
|
1494
1890
|
}
|
|
1891
|
+
if (isTelegramUpdateJournalLegacyFamilyVersion(snapshot.version) && entry.state === "pending" && deps.journal.inspectPendingRetention) {
|
|
1892
|
+
const authorityResult = checkAuthority(expectedOwner, entry.updateId);
|
|
1893
|
+
if (authorityResult)
|
|
1894
|
+
return authorityResult;
|
|
1895
|
+
let retained;
|
|
1896
|
+
let unverifiable = false;
|
|
1897
|
+
try {
|
|
1898
|
+
retained = deps.journal.inspectPendingRetention(entry);
|
|
1899
|
+
}
|
|
1900
|
+
catch (error) {
|
|
1901
|
+
unverifiable = true;
|
|
1902
|
+
recordRuntimeEvent(error, { phase: "pending-retention-inspection", updateId: entry.updateId });
|
|
1903
|
+
}
|
|
1904
|
+
if (retained || unverifiable) {
|
|
1905
|
+
// A copy is protection evidence only. A fresh owner must explicitly retry
|
|
1906
|
+
// cancellation; neither startup nor a malformed copy may dispatch the source.
|
|
1907
|
+
claims.set(entry.updateId, "abandoning");
|
|
1908
|
+
const journalBindingKey = retained?.journalBindingKey ?? deps.getJournalBindingKey?.();
|
|
1909
|
+
if (journalBindingKey)
|
|
1910
|
+
deferredSources.set(entry.updateId, { entry, journalBindingKey });
|
|
1911
|
+
transition("deferred", entry.updateId);
|
|
1912
|
+
continue;
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
if (isTelegramUpdateJournalLegacyFamilyVersion(snapshot.version) && (entry.state === "pending" || entry.state === "retry-wait")) {
|
|
1916
|
+
const historical = expectedOwner.startupUpdateIds.has(entry.updateId);
|
|
1917
|
+
const journalBindingKey = deps.getJournalBindingKey?.();
|
|
1918
|
+
// Classifiers and handler projections receive no mutable alias to CAS evidence.
|
|
1919
|
+
if (journalBindingKey)
|
|
1920
|
+
deferredSources.set(entry.updateId, { entry: structuredClone(entry), journalBindingKey,
|
|
1921
|
+
...(historical ? { historical: true } : {}) });
|
|
1922
|
+
const classifyPending = historical && entry.state === "pending" ? deps.shouldReviewHistoricalInput : deps.shouldHoldPendingInput;
|
|
1923
|
+
if (classifyPending) {
|
|
1924
|
+
const checkHistoricalAuthority = () => {
|
|
1925
|
+
if (owner !== expectedOwner || expectedOwner.controller.signal.aborted)
|
|
1926
|
+
return "aborted";
|
|
1927
|
+
if (deps.getJournalBindingKey?.() !== journalBindingKey || deps.isContextCurrent?.(expectedOwner.ctx) === false) {
|
|
1928
|
+
transition("blocked", entry.updateId, "authority-lost");
|
|
1929
|
+
return "blocked";
|
|
1930
|
+
}
|
|
1931
|
+
return checkAuthority(expectedOwner, entry.updateId);
|
|
1932
|
+
};
|
|
1933
|
+
const beforeClassification = checkHistoricalAuthority();
|
|
1934
|
+
if (beforeClassification)
|
|
1935
|
+
return beforeClassification;
|
|
1936
|
+
let review;
|
|
1937
|
+
try {
|
|
1938
|
+
review = await classifyPending(structuredClone(entry), expectedOwner.ctx, expectedOwner.controller.signal);
|
|
1939
|
+
}
|
|
1940
|
+
catch (error) {
|
|
1941
|
+
const authorityResult = checkHistoricalAuthority();
|
|
1942
|
+
if (authorityResult)
|
|
1943
|
+
return authorityResult;
|
|
1944
|
+
return blockWithFailure("execution", "historical-review-classification", error, entry.updateId);
|
|
1945
|
+
}
|
|
1946
|
+
const authorityResult = checkHistoricalAuthority();
|
|
1947
|
+
if (authorityResult)
|
|
1948
|
+
return authorityResult;
|
|
1949
|
+
if (typeof review !== "boolean" && !(historical && entry.state === "pending" && review === "retain"))
|
|
1950
|
+
return blockWithFailure("execution", "historical-review-classification", new TelegramUpdateAdmissionOutcomeError("Historical classification must return a boolean or retain verdict."), entry.updateId);
|
|
1951
|
+
// Classification may await a domain read; accepted/changed source evidence wins.
|
|
1952
|
+
try {
|
|
1953
|
+
const current = deps.journal.read();
|
|
1954
|
+
if (current.version !== snapshot.version || JSON.stringify(current.entries.find(candidate => candidate.updateId === entry.updateId)) !== JSON.stringify(entry)) {
|
|
1955
|
+
snapshotInvalidated = true;
|
|
1956
|
+
break;
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
catch (error) {
|
|
1960
|
+
return blockWithFailure("journal-read", "historical-source-recheck", error, entry.updateId);
|
|
1961
|
+
}
|
|
1962
|
+
if (review === "retain") {
|
|
1963
|
+
claims.set(entry.updateId, "retained");
|
|
1964
|
+
transition("deferred", entry.updateId);
|
|
1965
|
+
continue;
|
|
1966
|
+
}
|
|
1967
|
+
if (review && historical && spendHistorical && classifyPending === deps.shouldReviewHistoricalInput) {
|
|
1968
|
+
completedUpdateIds.push({ updateId: entry.updateId, spent: true });
|
|
1969
|
+
continue;
|
|
1970
|
+
}
|
|
1971
|
+
if (review) {
|
|
1972
|
+
claims.set(entry.updateId, "historical");
|
|
1973
|
+
transition("deferred", entry.updateId);
|
|
1974
|
+
continue;
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
if (entry.state === "pending" && entry.routingInput) {
|
|
1978
|
+
if (historical && spendHistorical) {
|
|
1979
|
+
completedUpdateIds.push({ updateId: entry.updateId, spent: true });
|
|
1980
|
+
continue;
|
|
1981
|
+
}
|
|
1982
|
+
claims.set(entry.updateId, "historical");
|
|
1983
|
+
transition("deferred", entry.updateId);
|
|
1984
|
+
continue;
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1495
1987
|
clearRetryTimer();
|
|
1496
1988
|
transition("executing", entry.updateId);
|
|
1497
1989
|
const execution = await executeWithinOwner(expectedOwner, entry.update);
|
|
@@ -1532,7 +2024,7 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1532
2024
|
for (const sourceUpdateId of receipt.sourceUpdateIds)
|
|
1533
2025
|
claims.set(sourceUpdateId, "queued");
|
|
1534
2026
|
try {
|
|
1535
|
-
publishCommittedQueueReceipt(receipt, outcome.queueReceipt.queueOwner, expectedOwner
|
|
2027
|
+
await publishCommittedQueueReceipt(receipt, outcome.queueReceipt.queueOwner, expectedOwner);
|
|
1536
2028
|
}
|
|
1537
2029
|
catch (error) {
|
|
1538
2030
|
return blockWithFailure("invalid-outcome", "queue-receipt-publish", error, entry.updateId);
|
|
@@ -1547,7 +2039,7 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1547
2039
|
return postExecutionAuthority;
|
|
1548
2040
|
const claimableUpdateIds = new Set([
|
|
1549
2041
|
entry.updateId,
|
|
1550
|
-
...claims.
|
|
2042
|
+
...[...claims].filter(([, claim]) => claim !== "abandoning" && claim !== "historical" && claim !== "retained").map(([id]) => id),
|
|
1551
2043
|
]);
|
|
1552
2044
|
let outcome;
|
|
1553
2045
|
try {
|
|
@@ -1557,7 +2049,10 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1557
2049
|
return blockWithFailure("invalid-outcome", "invalid-outcome", error, entry.updateId);
|
|
1558
2050
|
}
|
|
1559
2051
|
if (outcome.kind === "deferred") {
|
|
1560
|
-
|
|
2052
|
+
if (outcome.routingReview && !deferredSources.get(entry.updateId)?.historical) {
|
|
2053
|
+
return blockWithFailure("invalid-outcome", "historical-review-source", new TelegramUpdateAdmissionOutcomeError("Historical review needs an exact startup source."), entry.updateId);
|
|
2054
|
+
}
|
|
2055
|
+
claims.set(entry.updateId, outcome.routingReview ? "historical" : "deferred");
|
|
1561
2056
|
transition("deferred", entry.updateId);
|
|
1562
2057
|
continue;
|
|
1563
2058
|
}
|
|
@@ -1565,7 +2060,7 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1565
2060
|
const completionResult = commitCompletedBatch(expectedOwner, completedUpdateIds);
|
|
1566
2061
|
if (completionResult)
|
|
1567
2062
|
return completionResult;
|
|
1568
|
-
const queuedResult = commitQueuedOutcome(expectedOwner, entry.updateId, outcome);
|
|
2063
|
+
const queuedResult = await commitQueuedOutcome(expectedOwner, entry.updateId, outcome);
|
|
1569
2064
|
if (queuedResult === "blocked" || queuedResult === "aborted") {
|
|
1570
2065
|
return queuedResult;
|
|
1571
2066
|
}
|
|
@@ -1573,7 +2068,7 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1573
2068
|
snapshotInvalidated = true;
|
|
1574
2069
|
break;
|
|
1575
2070
|
}
|
|
1576
|
-
completedUpdateIds.push(entry.updateId);
|
|
2071
|
+
completedUpdateIds.push({ updateId: entry.updateId, ...(outcome.expectedSource ? { expectedSource: outcome.expectedSource } : {}) });
|
|
1577
2072
|
}
|
|
1578
2073
|
const completionResult = commitCompletedBatch(expectedOwner, completedUpdateIds);
|
|
1579
2074
|
if (completionResult)
|
|
@@ -1592,6 +2087,14 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1592
2087
|
const expectedOwner = owner;
|
|
1593
2088
|
if (!expectedOwner || drainPromise)
|
|
1594
2089
|
return;
|
|
2090
|
+
let startupKey;
|
|
2091
|
+
if (onHeldSourcesPrepared) {
|
|
2092
|
+
try {
|
|
2093
|
+
startupKey = deps.getJournalBindingKey?.();
|
|
2094
|
+
}
|
|
2095
|
+
catch { /* Missing startup identity grants no notification. */ }
|
|
2096
|
+
}
|
|
2097
|
+
let lastResult;
|
|
1595
2098
|
const run = async () => {
|
|
1596
2099
|
while (pendingSignal &&
|
|
1597
2100
|
owner === expectedOwner &&
|
|
@@ -1599,7 +2102,7 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1599
2102
|
pendingSignal = false;
|
|
1600
2103
|
// Keep newer wakes in this run so waitForDrain also covers them. A failure
|
|
1601
2104
|
// observed after signal() still keeps its latch; this loop never clears it.
|
|
1602
|
-
await drain(expectedOwner);
|
|
2105
|
+
lastResult = await drain(expectedOwner);
|
|
1603
2106
|
}
|
|
1604
2107
|
};
|
|
1605
2108
|
const operation = run();
|
|
@@ -1610,6 +2113,30 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1610
2113
|
if (pendingSignal && !expectedOwner.controller.signal.aborted) {
|
|
1611
2114
|
launchDrain();
|
|
1612
2115
|
}
|
|
2116
|
+
else if (lastResult === "idle" && startupKey && onHeldSourcesPrepared && !expectedOwner.heldSourcesPreparedIssued) {
|
|
2117
|
+
try {
|
|
2118
|
+
const ctx = expectedOwner.ctx, key = startupKey;
|
|
2119
|
+
const isCurrent = () => owner === expectedOwner && !blocked && !expectedOwner.controller.signal.aborted &&
|
|
2120
|
+
deps.getJournalBindingKey?.() === key && deps.isContextCurrent?.(ctx) !== false && deps.hasAuthority(ctx);
|
|
2121
|
+
if (!isCurrent())
|
|
2122
|
+
return;
|
|
2123
|
+
expectedOwner.heldSourcesPreparedIssued = true;
|
|
2124
|
+
// Do not couple lifecycle/stop or the worker drain to a held controller/API reply.
|
|
2125
|
+
void Promise.resolve().then(() => isCurrent() ? onHeldSourcesPrepared({ ctx,
|
|
2126
|
+
journalBindingKey: key, signal: expectedOwner.controller.signal, isCurrent }) : undefined).catch(error => {
|
|
2127
|
+
try {
|
|
2128
|
+
deps.recordRuntimeEvent?.("inbound-worker", error, { phase: "held-source-preparation" });
|
|
2129
|
+
}
|
|
2130
|
+
catch { /* Diagnostic only. */ }
|
|
2131
|
+
});
|
|
2132
|
+
}
|
|
2133
|
+
catch (error) {
|
|
2134
|
+
try {
|
|
2135
|
+
deps.recordRuntimeEvent?.("inbound-worker", error, { phase: "held-source-preparation" });
|
|
2136
|
+
}
|
|
2137
|
+
catch { /* Diagnostic only. */ }
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
1613
2140
|
}
|
|
1614
2141
|
};
|
|
1615
2142
|
tracked = operation.then(() => finish(), (error) => {
|
|
@@ -1621,6 +2148,76 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1621
2148
|
});
|
|
1622
2149
|
drainPromise = tracked;
|
|
1623
2150
|
};
|
|
2151
|
+
const abandonSource = (input, historicalSource) => {
|
|
2152
|
+
const expectedOwner = owner;
|
|
2153
|
+
const source = deferredSources.get(input.updateId);
|
|
2154
|
+
const abandonPending = deps.journal.abandonPending;
|
|
2155
|
+
const isCurrent = () => {
|
|
2156
|
+
if (!expectedOwner || owner !== expectedOwner || input.signal.aborted ||
|
|
2157
|
+
expectedOwner.controller.signal !== input.signal || !source ||
|
|
2158
|
+
source.entry.routingInput?.phase === "selected" || (source.historical && historicalSource !== source) ||
|
|
2159
|
+
(claims.get(input.updateId) !== "deferred" && claims.get(input.updateId) !== "abandoning" &&
|
|
2160
|
+
!(claims.get(input.updateId) === "historical" && historicalSource === source)))
|
|
2161
|
+
return false;
|
|
2162
|
+
return input.isCurrent() && deps.isContextCurrent?.(expectedOwner.ctx) !== false &&
|
|
2163
|
+
deps.getJournalBindingKey?.() === source.journalBindingKey && deps.hasAuthority(expectedOwner.ctx);
|
|
2164
|
+
};
|
|
2165
|
+
if (!source || !abandonPending || !isCurrent())
|
|
2166
|
+
return undefined;
|
|
2167
|
+
// Retain this claim on failure, including unknown commit: never reopen dispatch.
|
|
2168
|
+
claims.set(input.updateId, "abandoning");
|
|
2169
|
+
try {
|
|
2170
|
+
const result = abandonPending({ ...input, entry: source.entry,
|
|
2171
|
+
journalBindingKey: source.journalBindingKey, isCurrent });
|
|
2172
|
+
claims.delete(input.updateId);
|
|
2173
|
+
deferredSources.delete(input.updateId);
|
|
2174
|
+
state.journalEntryCount = result.entryCount;
|
|
2175
|
+
state.journalSerializedBytes = result.serializedBytes;
|
|
2176
|
+
transition("idle");
|
|
2177
|
+
pendingSignal = true;
|
|
2178
|
+
launchDrain();
|
|
2179
|
+
return result;
|
|
2180
|
+
}
|
|
2181
|
+
catch (error) {
|
|
2182
|
+
recordRuntimeEvent(error, { phase: "pending-abandonment", updateId: input.updateId });
|
|
2183
|
+
updateClaimCounts();
|
|
2184
|
+
notifyStateChange();
|
|
2185
|
+
throw error;
|
|
2186
|
+
}
|
|
2187
|
+
};
|
|
2188
|
+
const inspectRecovery = (input, kind) => {
|
|
2189
|
+
const expectedOwner = owner;
|
|
2190
|
+
if (!expectedOwner)
|
|
2191
|
+
return undefined;
|
|
2192
|
+
const { signal, journalBindingKey, isCurrent: callerIsCurrent, afterUpdateId } = input;
|
|
2193
|
+
const isOwnerCurrent = () => owner === expectedOwner && expectedOwner.controller.signal === signal && !signal.aborted;
|
|
2194
|
+
const isCurrent = () => isOwnerCurrent() && callerIsCurrent() && deps.isContextCurrent?.(expectedOwner.ctx) !== false &&
|
|
2195
|
+
deps.getJournalBindingKey?.() === journalBindingKey && deps.hasAuthority(expectedOwner.ctx);
|
|
2196
|
+
if (!isCurrent() || !deps.journal.abandonPending || !deps.journal.inspectPendingRetention ||
|
|
2197
|
+
(afterUpdateId !== undefined && (!Number.isSafeInteger(afterUpdateId) || afterUpdateId < 0)))
|
|
2198
|
+
return undefined;
|
|
2199
|
+
const matches = [...deferredSources].filter(([id, source]) => claims.get(id) === kind &&
|
|
2200
|
+
source.journalBindingKey === journalBindingKey && (afterUpdateId === undefined || id > afterUpdateId))
|
|
2201
|
+
.sort(([a], [b]) => a - b);
|
|
2202
|
+
const sources = matches.slice(0, TELEGRAM_HELD_SOURCE_INSPECTION_LIMIT).map(([updateId, source]) => {
|
|
2203
|
+
let committed;
|
|
2204
|
+
return { original: structuredClone(source.entry), retry(authority) {
|
|
2205
|
+
const authorized = () => isOwnerCurrent() && authority.isCurrent() && isCurrent();
|
|
2206
|
+
if (!authorized())
|
|
2207
|
+
return undefined;
|
|
2208
|
+
if (committed)
|
|
2209
|
+
return { ...committed, duplicate: true };
|
|
2210
|
+
const stillProtected = () => authorized() && (claims.get(updateId) === kind || claims.get(updateId) === "abandoning") &&
|
|
2211
|
+
deferredSources.get(updateId) === source;
|
|
2212
|
+
if (!stillProtected())
|
|
2213
|
+
return undefined;
|
|
2214
|
+
committed = abandonSource({ ...authority, updateId, signal, isCurrent: stillProtected }, source);
|
|
2215
|
+
return committed;
|
|
2216
|
+
} };
|
|
2217
|
+
});
|
|
2218
|
+
return isCurrent() ? { sources,
|
|
2219
|
+
...(matches.length > TELEGRAM_HELD_SOURCE_INSPECTION_LIMIT ? { nextAfterUpdateId: matches[TELEGRAM_HELD_SOURCE_INSPECTION_LIMIT - 1][0] } : {}) } : undefined;
|
|
2220
|
+
};
|
|
1624
2221
|
return {
|
|
1625
2222
|
start(ctx) {
|
|
1626
2223
|
if (owner) {
|
|
@@ -1631,6 +2228,7 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1631
2228
|
return;
|
|
1632
2229
|
}
|
|
1633
2230
|
clearRetryTimer();
|
|
2231
|
+
clearRoutingTimer();
|
|
1634
2232
|
const nowMs = getNowMs();
|
|
1635
2233
|
const generation = ++nextGeneration;
|
|
1636
2234
|
const queueOwnerIdentity = resolveQueueOwnerIdentity(ctx, generation);
|
|
@@ -1641,6 +2239,7 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1641
2239
|
queueOwnerIdentity,
|
|
1642
2240
|
};
|
|
1643
2241
|
claims.clear();
|
|
2242
|
+
deferredSources.clear();
|
|
1644
2243
|
committedQueueReceipts.clear();
|
|
1645
2244
|
blocked = false;
|
|
1646
2245
|
pendingSignal = true;
|
|
@@ -1653,6 +2252,8 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1653
2252
|
state.journalSerializedBytes = 0;
|
|
1654
2253
|
state.oldestAdmittedAtMs = undefined;
|
|
1655
2254
|
state.deferredClaimCount = 0;
|
|
2255
|
+
delete state.abandoningClaimCount;
|
|
2256
|
+
delete state.historicalClaimCount;
|
|
1656
2257
|
state.queuedClaimCount = 0;
|
|
1657
2258
|
state.foreignQueuedCount = 0;
|
|
1658
2259
|
delete state.foreignQueuedOwner;
|
|
@@ -1683,7 +2284,52 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1683
2284
|
pendingSignal = true;
|
|
1684
2285
|
launchDrain();
|
|
1685
2286
|
},
|
|
1686
|
-
|
|
2287
|
+
armRoutingInput(input) { return mutateRoutingSource(input, false)?.lifetime; },
|
|
2288
|
+
selectRoutingInput(input) { return mutateRoutingSource(input, true)?.issued === true; },
|
|
2289
|
+
inspectDeferredSource(input) {
|
|
2290
|
+
const expectedOwner = owner;
|
|
2291
|
+
const source = deferredSources.get(input.updateId);
|
|
2292
|
+
const current = () => {
|
|
2293
|
+
if (!expectedOwner || owner !== expectedOwner || input.signal.aborted || expectedOwner.controller.signal !== input.signal ||
|
|
2294
|
+
!source || deferredSources.get(input.updateId) !== source || claims.get(input.updateId) !== "deferred" ||
|
|
2295
|
+
deps.getJournalBindingKey?.() !== source.journalBindingKey || deps.isContextCurrent?.(expectedOwner.ctx) === false ||
|
|
2296
|
+
!deps.hasAuthority(expectedOwner.ctx))
|
|
2297
|
+
return false;
|
|
2298
|
+
return isQueueOwnerIdentityCurrent(expectedOwner);
|
|
2299
|
+
};
|
|
2300
|
+
if (!current())
|
|
2301
|
+
return undefined;
|
|
2302
|
+
const snapshot = deps.journal.read();
|
|
2303
|
+
const entry = snapshot.entries.find(value => value.updateId === input.updateId);
|
|
2304
|
+
if (!isTelegramUpdateJournalLegacyFamilyVersion(snapshot.version) || !entry ||
|
|
2305
|
+
JSON.stringify(entry) !== JSON.stringify(source.entry) || !current())
|
|
2306
|
+
return undefined;
|
|
2307
|
+
return { journalBindingKey: source.journalBindingKey, ...createTelegramUpdateJournalEntryDigest(entry) };
|
|
2308
|
+
},
|
|
2309
|
+
isHistoricalSource(input) {
|
|
2310
|
+
const source = deferredSources.get(input.updateId);
|
|
2311
|
+
const isCurrent = () => owner?.controller.signal === input.signal && !input.signal.aborted &&
|
|
2312
|
+
source?.historical === true && deferredSources.get(input.updateId) === source;
|
|
2313
|
+
if (!isCurrent())
|
|
2314
|
+
return false;
|
|
2315
|
+
// Domain eligibility observes original evidence, never mutable handler projections.
|
|
2316
|
+
return (!input.matchesOriginal || input.matchesOriginal(structuredClone(source.entry)) === true) && isCurrent();
|
|
2317
|
+
},
|
|
2318
|
+
supportsDeferredAbandonment(input) {
|
|
2319
|
+
const expectedOwner = owner;
|
|
2320
|
+
const source = deferredSources.get(input.updateId);
|
|
2321
|
+
if (!expectedOwner || input.signal.aborted || expectedOwner.controller.signal !== input.signal ||
|
|
2322
|
+
!source || source.historical || input.journalBindingKey !== source.journalBindingKey ||
|
|
2323
|
+
!deps.journal.abandonPending || !deps.journal.inspectPendingRetention ||
|
|
2324
|
+
claims.get(input.updateId) === "queued")
|
|
2325
|
+
return false;
|
|
2326
|
+
return deps.isContextCurrent?.(expectedOwner.ctx) !== false &&
|
|
2327
|
+
deps.getJournalBindingKey?.() === source.journalBindingKey && deps.hasAuthority(expectedOwner.ctx);
|
|
2328
|
+
},
|
|
2329
|
+
abandonDeferred(input) { return abandonSource(input); },
|
|
2330
|
+
inspectAbandoning(input) { return inspectRecovery(input, "abandoning"); },
|
|
2331
|
+
inspectHistorical(input) { return inspectRecovery(input, "historical"); },
|
|
2332
|
+
async settleDeferred(input) {
|
|
1687
2333
|
const expectedOwner = owner;
|
|
1688
2334
|
if (!expectedOwner ||
|
|
1689
2335
|
expectedOwner.controller.signal !== input.signal ||
|
|
@@ -1700,11 +2346,24 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1700
2346
|
return;
|
|
1701
2347
|
}
|
|
1702
2348
|
const claim = claims.get(input.updateId);
|
|
2349
|
+
if (claim === "abandoning" || claim === "historical" || claim === "retained")
|
|
2350
|
+
return;
|
|
1703
2351
|
if (input.outcome.kind === "complete") {
|
|
1704
2352
|
// Expiry may retire only a still-deferred source, never accepted queue work.
|
|
1705
2353
|
if (claim !== "deferred")
|
|
1706
2354
|
return;
|
|
1707
|
-
|
|
2355
|
+
let completion;
|
|
2356
|
+
try {
|
|
2357
|
+
completion = validateTelegramUpdateAdmissionOutcome(input.outcome, input.updateId, new Set([input.updateId]));
|
|
2358
|
+
}
|
|
2359
|
+
catch (error) {
|
|
2360
|
+
blockWithFailure("invalid-outcome", "late-invalid-completion", error, input.updateId);
|
|
2361
|
+
return;
|
|
2362
|
+
}
|
|
2363
|
+
if (completion.kind !== "complete")
|
|
2364
|
+
return;
|
|
2365
|
+
const result = commitCompletedBatch(expectedOwner, [{ updateId: input.updateId,
|
|
2366
|
+
...(completion.expectedSource ? { expectedSource: completion.expectedSource } : {}) }]);
|
|
1708
2367
|
if (!result)
|
|
1709
2368
|
transition("idle", input.updateId);
|
|
1710
2369
|
return;
|
|
@@ -1721,7 +2380,7 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1721
2380
|
}
|
|
1722
2381
|
let outcome;
|
|
1723
2382
|
try {
|
|
1724
|
-
outcome = validateTelegramUpdateAdmissionOutcome(input.outcome, input.updateId, new Set([input.updateId, ...claims.
|
|
2383
|
+
outcome = validateTelegramUpdateAdmissionOutcome(input.outcome, input.updateId, new Set([input.updateId, ...[...claims].filter(([, claim]) => claim !== "abandoning" && claim !== "historical" && claim !== "retained").map(([id]) => id)]));
|
|
1725
2384
|
}
|
|
1726
2385
|
catch (error) {
|
|
1727
2386
|
blockWithFailure("invalid-outcome", "late-invalid-outcome", error, input.updateId);
|
|
@@ -1737,7 +2396,8 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1737
2396
|
releaseDeferredClaims(outcome.sourceUpdateIds);
|
|
1738
2397
|
return;
|
|
1739
2398
|
}
|
|
1740
|
-
const
|
|
2399
|
+
const publication = commitQueuedOutcome(expectedOwner, input.updateId, outcome);
|
|
2400
|
+
const result = typeof publication === "string" ? publication : await publication;
|
|
1741
2401
|
if (result === "blocked") {
|
|
1742
2402
|
releaseDeferredClaims(outcome.sourceUpdateIds);
|
|
1743
2403
|
return;
|
|
@@ -1746,7 +2406,7 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1746
2406
|
return;
|
|
1747
2407
|
transition("queued", input.updateId);
|
|
1748
2408
|
},
|
|
1749
|
-
settleCustodied(input) {
|
|
2409
|
+
async settleCustodied(input) {
|
|
1750
2410
|
const expectedOwner = owner;
|
|
1751
2411
|
if (!expectedOwner || expectedOwner.controller.signal !== input.signal || input.signal.aborted)
|
|
1752
2412
|
return;
|
|
@@ -1791,7 +2451,7 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1791
2451
|
for (const sourceUpdateId of receipt.sourceUpdateIds)
|
|
1792
2452
|
claims.set(sourceUpdateId, "queued");
|
|
1793
2453
|
try {
|
|
1794
|
-
publishCommittedQueueReceipt(receipt, input.result.queueReceipt.queueOwner, expectedOwner
|
|
2454
|
+
await publishCommittedQueueReceipt(receipt, input.result.queueReceipt.queueOwner, expectedOwner);
|
|
1795
2455
|
}
|
|
1796
2456
|
catch (error) {
|
|
1797
2457
|
blockWithFailure("invalid-outcome", "queue-receipt-publish", error, input.updateId);
|
|
@@ -1804,16 +2464,32 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1804
2464
|
const committed = getCurrentQueueReceipt(receipt);
|
|
1805
2465
|
return committed ? { ...committed.queueOwner } : undefined;
|
|
1806
2466
|
},
|
|
1807
|
-
|
|
2467
|
+
getQueueReceiptSettlementOwner(receipt, ctx, reason) {
|
|
2468
|
+
const expectedOwner = owner, committed = committedQueueReceipts.get(receipt.receiptId);
|
|
2469
|
+
if (!expectedOwner || expectedOwner.controller.signal.aborted ||
|
|
2470
|
+
!(deps.isContextCurrent?.(ctx) ?? expectedOwner.ctx === ctx) || !isQueueOwnerIdentityCurrent(expectedOwner) ||
|
|
2471
|
+
!committed || !areTelegramQueueAdmissionReceiptsEqual(committed.receipt, normalizeQueueReceipt(receipt)) ||
|
|
2472
|
+
(deps.getJournalBindingKey && committed.receipt.journalBindingKey !== deps.getJournalBindingKey()) ||
|
|
2473
|
+
!isTelegramUpdateJournalQueueOwnerProcess(committed.queueOwner, expectedOwner.queueOwnerIdentity))
|
|
2474
|
+
return undefined;
|
|
2475
|
+
const attempt = scopedQueueCompletionAttempts.get(receipt.receiptId);
|
|
2476
|
+
if (attempt ? attempt.owner !== expectedOwner || attempt.reason !== reason || attempt.journalBindingKey !== deps.getJournalBindingKey?.()
|
|
2477
|
+
: !getCurrentQueueReceipt(receipt))
|
|
2478
|
+
return undefined;
|
|
2479
|
+
return { ...committed.queueOwner };
|
|
2480
|
+
},
|
|
2481
|
+
completeQueueReceipts: function completeQueueReceipts(input) {
|
|
1808
2482
|
const expectedOwner = owner;
|
|
1809
2483
|
if (!expectedOwner ||
|
|
1810
2484
|
expectedOwner.controller.signal.aborted ||
|
|
1811
2485
|
!(deps.isContextCurrent?.(input.ctx) ?? expectedOwner.ctx === input.ctx)) {
|
|
1812
2486
|
return false;
|
|
1813
2487
|
}
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
2488
|
+
// An ACK permits only local completion reconciliation, never execution readiness.
|
|
2489
|
+
const receipts = input.sourceCompletions === undefined ? input.receipts.filter(receipt => !isQueueReceiptCompletionAcknowledged(receipt)) : input.receipts;
|
|
2490
|
+
if (receipts.length === 0)
|
|
2491
|
+
return input.sourceCompletions === undefined;
|
|
2492
|
+
const normalizedReceipts = receipts.map(normalizeQueueReceipt);
|
|
1817
2493
|
const receiptIds = new Set();
|
|
1818
2494
|
const sourceUpdateIds = new Set();
|
|
1819
2495
|
const queuedCompletions = [];
|
|
@@ -1837,9 +2513,91 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1837
2513
|
sourceUpdateIds.add(updateId);
|
|
1838
2514
|
}
|
|
1839
2515
|
}
|
|
2516
|
+
const completionBinding = deps.getJournalBindingKey?.();
|
|
2517
|
+
const ordinaryObserverBound = normalizedReceipts.every(receipt => committedQueueReceipts.get(receipt.receiptId).receipt.journalBindingKey === completionBinding);
|
|
2518
|
+
const ordinaryCompletionCurrent = () => ordinaryObserverBound && owner === expectedOwner &&
|
|
2519
|
+
!expectedOwner.controller.signal.aborted && (deps.isContextCurrent?.(input.ctx) ?? expectedOwner.ctx === input.ctx) &&
|
|
2520
|
+
isQueueOwnerIdentityCurrent(expectedOwner) && deps.getJournalBindingKey?.() === completionBinding;
|
|
1840
2521
|
let removedUpdateIds;
|
|
2522
|
+
let scopedCompletionCurrent;
|
|
1841
2523
|
try {
|
|
1842
|
-
|
|
2524
|
+
const prepared = normalizedReceipts.flatMap(receipt => committedQueueReceipts.get(receipt.receiptId).sourceCompletions ?? []);
|
|
2525
|
+
const sourceCompletions = input.sourceCompletions === undefined ? (prepared.length ? prepared : undefined) : input.sourceCompletions;
|
|
2526
|
+
if (input.sourceCompletions !== undefined && prepared.some(expected => !input.sourceCompletions.some(value => isDeepStrictEqual(value, expected)))) {
|
|
2527
|
+
throw new Error("Telegram queued completion conflicts with its prepared source scopes.");
|
|
2528
|
+
}
|
|
2529
|
+
if (input.sourceCompletions === undefined && prepared.length && receipts.some(receipt => !requiredScopedQueueReceipts.has(receipt.receiptId))) {
|
|
2530
|
+
const scoped = receipts.filter(receipt => requiredScopedQueueReceipts.has(receipt.receiptId));
|
|
2531
|
+
const ordinary = receipts.filter(receipt => !requiredScopedQueueReceipts.has(receipt.receiptId));
|
|
2532
|
+
if (!scoped.length || !ordinary.length)
|
|
2533
|
+
throw new Error("Telegram mixed queue completion has incomplete scoped authority.");
|
|
2534
|
+
const binding = deps.getJournalBindingKey?.();
|
|
2535
|
+
const current = () => owner === expectedOwner && !expectedOwner.controller.signal.aborted &&
|
|
2536
|
+
(deps.isContextCurrent?.(input.ctx) ?? expectedOwner.ctx === input.ctx) && isQueueOwnerIdentityCurrent(expectedOwner) &&
|
|
2537
|
+
!!binding && deps.getJournalBindingKey?.() === binding;
|
|
2538
|
+
// Independent whole receipts need independent ACKs. Ordinary siblings must not acquire invented scopes.
|
|
2539
|
+
if (!current() || !completeQueueReceipts({ receipts: scoped, ctx: input.ctx, reason: input.reason }) || !current())
|
|
2540
|
+
return false;
|
|
2541
|
+
return completeQueueReceipts({ receipts: ordinary, ctx: input.ctx, reason: input.reason });
|
|
2542
|
+
}
|
|
2543
|
+
if (sourceCompletions === undefined) {
|
|
2544
|
+
if (queuedCompletions.some(receipt => requiredScopedQueueReceipts.has(receipt.receiptId))) {
|
|
2545
|
+
throw new Error("Telegram scoped queue completion cannot downgrade required source scopes.");
|
|
2546
|
+
}
|
|
2547
|
+
removedUpdateIds = deps.journal.completeQueued(queuedCompletions).removedUpdateIds;
|
|
2548
|
+
}
|
|
2549
|
+
else {
|
|
2550
|
+
for (const receipt of queuedCompletions)
|
|
2551
|
+
requiredScopedQueueReceipts.add(receipt.receiptId);
|
|
2552
|
+
queuedCompletions.sort((a, b) => a.receiptId < b.receiptId ? -1 : a.receiptId > b.receiptId ? 1 : 0);
|
|
2553
|
+
const journalBindingKey = deps.getJournalBindingKey?.();
|
|
2554
|
+
const current = () => owner === expectedOwner && !expectedOwner.controller.signal.aborted &&
|
|
2555
|
+
(deps.isContextCurrent?.(input.ctx) ?? expectedOwner.ctx === input.ctx) && isQueueOwnerIdentityCurrent(expectedOwner) &&
|
|
2556
|
+
deps.getJournalBindingKey?.() === journalBindingKey;
|
|
2557
|
+
if (!journalBindingKey || !current() || !completeQueuedExact || !inspectCompletion ||
|
|
2558
|
+
!Array.isArray(sourceCompletions) || input.sourceCompletions !== undefined && sourceCompletions.length !== sourceUpdateIds.size) {
|
|
2559
|
+
throw new Error("Telegram scoped queue completion requires full current source authority.");
|
|
2560
|
+
}
|
|
2561
|
+
scopedCompletionCurrent = current;
|
|
2562
|
+
const completions = normalizeTelegramQueueSourceCompletions(sourceCompletions, sourceUpdateIds, journalBindingKey, input.sourceCompletions !== undefined)
|
|
2563
|
+
.map(({ journalBindingKey: _binding, ...completion }) => completion);
|
|
2564
|
+
if (queuedCompletions.some(receipt => !completions.some(scope => receipt.sourceUpdateIds.includes(scope.updateId)))) {
|
|
2565
|
+
throw new Error("Telegram scoped queue completion requires a proven origin for every whole receipt.");
|
|
2566
|
+
}
|
|
2567
|
+
const previous = queuedCompletions.map(receipt => scopedQueueCompletionAttempts.get(receipt.receiptId));
|
|
2568
|
+
const retained = previous.find(attempt => attempt !== undefined);
|
|
2569
|
+
if (retained && (previous.some(attempt => attempt !== retained) || retained.owner !== expectedOwner ||
|
|
2570
|
+
retained.journalBindingKey !== journalBindingKey || retained.reason !== input.reason ||
|
|
2571
|
+
!isDeepStrictEqual(retained.receipts, queuedCompletions) || !isDeepStrictEqual(retained.completions, completions))) {
|
|
2572
|
+
throw new Error("Telegram scoped queue completion has conflicting issued authority.");
|
|
2573
|
+
}
|
|
2574
|
+
if (!retained) {
|
|
2575
|
+
const attempt = { owner: expectedOwner, journalBindingKey, reason: input.reason,
|
|
2576
|
+
receipts: queuedCompletions.map(receipt => ({ ...receipt, sourceUpdateIds: [...receipt.sourceUpdateIds], queueOwner: { ...receipt.queueOwner } })),
|
|
2577
|
+
completions: completions.map(completion => ({ ...completion })) };
|
|
2578
|
+
if (!current())
|
|
2579
|
+
throw new Error("Telegram scoped queue completion authority changed before disposal.");
|
|
2580
|
+
// Mark before issuance: an exception may follow a committed rename, and cannot license another disposal.
|
|
2581
|
+
for (const receipt of queuedCompletions)
|
|
2582
|
+
scopedQueueCompletionAttempts.set(receipt.receiptId, attempt);
|
|
2583
|
+
const result = completeQueuedExact(queuedCompletions.map(receipt => ({ ...receipt, sourceUpdateIds: [...receipt.sourceUpdateIds], queueOwner: { ...receipt.queueOwner } })), completions.map(completion => ({ ...completion })));
|
|
2584
|
+
if (!current() || !isDeepStrictEqual(result.sourceCompletions, completions)) {
|
|
2585
|
+
throw new Error("Telegram scoped queue completion ACK was not confirmed.");
|
|
2586
|
+
}
|
|
2587
|
+
const removed = new Set(result.removedUpdateIds);
|
|
2588
|
+
if (removed.size !== sourceUpdateIds.size || [...sourceUpdateIds].some(id => !removed.has(id))) {
|
|
2589
|
+
throw new Error("Telegram scoped queue completion did not remove every source.");
|
|
2590
|
+
}
|
|
2591
|
+
}
|
|
2592
|
+
for (const completion of completions) {
|
|
2593
|
+
if (!current() || !isDeepStrictEqual(inspectCompletion({ ...completion }), completion) || !current()) {
|
|
2594
|
+
throw new Error("Telegram scoped queue completion ACK was not retained under current authority.");
|
|
2595
|
+
}
|
|
2596
|
+
}
|
|
2597
|
+
// Every immutable whole receipt has a scoped queued-origin witness; native queued ACK continuity proves its complete removal.
|
|
2598
|
+
// Unscoped siblings receive only receipt acknowledgement, never a source marker or absence-based proof.
|
|
2599
|
+
removedUpdateIds = [...sourceUpdateIds];
|
|
2600
|
+
}
|
|
1843
2601
|
}
|
|
1844
2602
|
catch (error) {
|
|
1845
2603
|
blockWithFailure("journal-write", "queue-receipt-completion", error);
|
|
@@ -1851,13 +2609,15 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1851
2609
|
blockWithFailure("journal-write", "queue-receipt-completion", new Error(`Telegram ${input.reason} did not complete every receipt source.`));
|
|
1852
2610
|
return false;
|
|
1853
2611
|
}
|
|
1854
|
-
|
|
2612
|
+
receipts.forEach((receipt, index) => {
|
|
1855
2613
|
acknowledgedQueueReceiptCompletions.set(receipt, normalizedReceipts[index]);
|
|
1856
2614
|
});
|
|
1857
2615
|
for (const updateId of sourceUpdateIds)
|
|
1858
2616
|
claims.delete(updateId);
|
|
1859
2617
|
for (const receiptId of receiptIds) {
|
|
1860
2618
|
committedQueueReceipts.delete(receiptId);
|
|
2619
|
+
requiredScopedQueueReceipts.delete(receiptId);
|
|
2620
|
+
scopedQueueCompletionAttempts.delete(receiptId);
|
|
1861
2621
|
}
|
|
1862
2622
|
const completedUpdateIds = [...sourceUpdateIds];
|
|
1863
2623
|
state.lastCompletedUpdateId = Math.max(state.lastCompletedUpdateId ?? -1, ...completedUpdateIds);
|
|
@@ -1865,8 +2625,33 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1865
2625
|
state.journalEntryCount = Math.max(0, state.journalEntryCount - completedUpdateIds.length);
|
|
1866
2626
|
updateClaimCounts();
|
|
1867
2627
|
notifyStateChange();
|
|
1868
|
-
|
|
1869
|
-
|
|
2628
|
+
if (scopedCompletionCurrent)
|
|
2629
|
+
for (const receipt of normalizedReceipts) {
|
|
2630
|
+
if (!scopedCompletionCurrent())
|
|
2631
|
+
break;
|
|
2632
|
+
try {
|
|
2633
|
+
observeQueuedCompletion?.(normalizeQueueReceipt(receipt), input.ctx);
|
|
2634
|
+
}
|
|
2635
|
+
catch (error) {
|
|
2636
|
+
recordRuntimeEvent(error, { phase: "queue-receipt-completion-observer", receiptId: receipt.receiptId });
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
2639
|
+
// Ordinary queued disposal is source completion too; notify only after a whole positive ACK and fresh origin authority.
|
|
2640
|
+
if (!scopedCompletionCurrent)
|
|
2641
|
+
for (const updateId of completedUpdateIds) {
|
|
2642
|
+
try {
|
|
2643
|
+
if (!ordinaryCompletionCurrent())
|
|
2644
|
+
break;
|
|
2645
|
+
deps.onUpdateCompleted?.(updateId, input.ctx, completionBinding);
|
|
2646
|
+
}
|
|
2647
|
+
catch (error) {
|
|
2648
|
+
recordRuntimeEvent(error, { phase: "queue-source-completion-observer", updateId });
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
if (!scopedCompletionCurrent || scopedCompletionCurrent()) {
|
|
2652
|
+
pendingSignal = true;
|
|
2653
|
+
launchDrain();
|
|
2654
|
+
}
|
|
1870
2655
|
return true;
|
|
1871
2656
|
},
|
|
1872
2657
|
async stop() {
|
|
@@ -1875,6 +2660,7 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1875
2660
|
return;
|
|
1876
2661
|
pendingSignal = false;
|
|
1877
2662
|
clearRetryTimer();
|
|
2663
|
+
clearRoutingTimer();
|
|
1878
2664
|
expectedOwner.controller.abort();
|
|
1879
2665
|
await drainPromise?.catch(() => undefined);
|
|
1880
2666
|
if (owner !== expectedOwner)
|
|
@@ -1882,6 +2668,7 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1882
2668
|
owner = undefined;
|
|
1883
2669
|
drainPromise = undefined;
|
|
1884
2670
|
claims.clear();
|
|
2671
|
+
deferredSources.clear();
|
|
1885
2672
|
committedQueueReceipts.clear();
|
|
1886
2673
|
blocked = false;
|
|
1887
2674
|
state.phase = "stopped";
|
|
@@ -1889,6 +2676,8 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1889
2676
|
state.currentUpdateId = undefined;
|
|
1890
2677
|
state.blockedReason = undefined;
|
|
1891
2678
|
state.deferredClaimCount = 0;
|
|
2679
|
+
delete state.abandoningClaimCount;
|
|
2680
|
+
delete state.historicalClaimCount;
|
|
1892
2681
|
state.queuedClaimCount = 0;
|
|
1893
2682
|
state.foreignQueuedCount = 0;
|
|
1894
2683
|
delete state.foreignQueuedOwner;
|
|
@@ -1907,8 +2696,9 @@ export function createTelegramUpdateWorkerRuntime(deps) {
|
|
|
1907
2696
|
state.unsettledExecutionCount = unsettledExecutions.size;
|
|
1908
2697
|
notifyStateChange();
|
|
1909
2698
|
},
|
|
1910
|
-
waitForDrain() {
|
|
1911
|
-
|
|
2699
|
+
async waitForDrain() {
|
|
2700
|
+
await (drainPromise ?? Promise.resolve());
|
|
2701
|
+
await Promise.allSettled([...pendingQueuePublications.values()].map(value => value.task));
|
|
1912
2702
|
},
|
|
1913
2703
|
getState() {
|
|
1914
2704
|
return getTelegramUpdateWorkerStateSnapshot(state);
|
|
@@ -2019,8 +2809,21 @@ function mergeTelegramReportedAdmissionOutcome(current, next, updateId) {
|
|
|
2019
2809
|
return next;
|
|
2020
2810
|
if (next.kind === "deferred")
|
|
2021
2811
|
return current;
|
|
2022
|
-
if (current.kind === "complete" && next.kind === "complete")
|
|
2023
|
-
|
|
2812
|
+
if (current.kind === "complete" && next.kind === "complete") {
|
|
2813
|
+
if (!current.expectedSource)
|
|
2814
|
+
return next;
|
|
2815
|
+
if (!next.expectedSource)
|
|
2816
|
+
return current;
|
|
2817
|
+
if (current.expectedSource.updateId === next.expectedSource.updateId &&
|
|
2818
|
+
current.expectedSource.journalBindingKey === next.expectedSource.journalBindingKey &&
|
|
2819
|
+
current.expectedSource.sourceSha256 === next.expectedSource.sourceSha256) {
|
|
2820
|
+
if (!current.expectedSource.completionSha256)
|
|
2821
|
+
return next;
|
|
2822
|
+
if (!next.expectedSource.completionSha256 || current.expectedSource.completionSha256 === next.expectedSource.completionSha256)
|
|
2823
|
+
return current;
|
|
2824
|
+
}
|
|
2825
|
+
throw new TelegramUpdateAdmissionOutcomeError(`Telegram update ${updateId} reported conflicting completion sources.`);
|
|
2826
|
+
}
|
|
2024
2827
|
if (current.kind === "queued" && next.kind === "queued" &&
|
|
2025
2828
|
areTelegramQueueAdmissionReceiptsEqual(current, next))
|
|
2026
2829
|
return current;
|
|
@@ -2736,13 +3539,21 @@ export function createTelegramUpdateAdmissionHandle(deps) {
|
|
|
2736
3539
|
let nextExecutionGeneration = 0;
|
|
2737
3540
|
return async (update, ctx, signal) => {
|
|
2738
3541
|
const generation = ++nextExecutionGeneration;
|
|
3542
|
+
let suspended = false;
|
|
3543
|
+
let routingClaims = 0;
|
|
3544
|
+
let abandoned;
|
|
3545
|
+
let routingLifetime;
|
|
3546
|
+
let routingSourceIds;
|
|
3547
|
+
let routingClockUnknown = false;
|
|
3548
|
+
let routingSelectionAttempted = false;
|
|
3549
|
+
let routingSelectionConfirmed = false;
|
|
2739
3550
|
const execution = {
|
|
2740
3551
|
generation,
|
|
2741
3552
|
updateId: update.update_id,
|
|
2742
3553
|
signal,
|
|
2743
|
-
isCurrent: () => !signal.aborted,
|
|
3554
|
+
isCurrent: () => !signal.aborted && !suspended,
|
|
2744
3555
|
assertCurrent() {
|
|
2745
|
-
if (signal.aborted) {
|
|
3556
|
+
if (signal.aborted || suspended) {
|
|
2746
3557
|
throw signal.reason ?? new DOMException("Aborted", "AbortError");
|
|
2747
3558
|
}
|
|
2748
3559
|
},
|
|
@@ -2753,8 +3564,19 @@ export function createTelegramUpdateAdmissionHandle(deps) {
|
|
|
2753
3564
|
if (verdict === "consume")
|
|
2754
3565
|
return { kind: "complete" };
|
|
2755
3566
|
let immediate = true;
|
|
3567
|
+
let settlementReported = false;
|
|
2756
3568
|
let outcome;
|
|
2757
3569
|
const boundUpdate = bindTelegramUpdateExecutionFence(bindTelegramUpdateAdmissionSource(update, (next) => {
|
|
3570
|
+
if (suspended || signal.aborted)
|
|
3571
|
+
return;
|
|
3572
|
+
if (next.kind === "deferred" && next.routingReview) {
|
|
3573
|
+
if (!immediate || settlementReported || routingClaims > 0 || !deps.isHistoricalSource?.({ updateId: update.update_id, signal })) {
|
|
3574
|
+
throw new TelegramUpdateAdmissionOutcomeError("Historical review cannot override accepted or selected work.");
|
|
3575
|
+
}
|
|
3576
|
+
suspended = true;
|
|
3577
|
+
}
|
|
3578
|
+
if (next.kind !== "deferred")
|
|
3579
|
+
settlementReported = true;
|
|
2758
3580
|
if (immediate) {
|
|
2759
3581
|
outcome = mergeTelegramReportedAdmissionOutcome(outcome, next, update.update_id);
|
|
2760
3582
|
return;
|
|
@@ -2763,11 +3585,15 @@ export function createTelegramUpdateAdmissionHandle(deps) {
|
|
|
2763
3585
|
throw new TelegramUpdateAdmissionOutcomeError(`Telegram update ${update.update_id} reported a late outcome without an owner.`);
|
|
2764
3586
|
}
|
|
2765
3587
|
void Promise.resolve()
|
|
2766
|
-
.then(() =>
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
3588
|
+
.then(() => {
|
|
3589
|
+
if (suspended || signal.aborted)
|
|
3590
|
+
return;
|
|
3591
|
+
return deps.onLateOutcome(next, {
|
|
3592
|
+
updateId: update.update_id,
|
|
3593
|
+
ctx,
|
|
3594
|
+
signal,
|
|
3595
|
+
});
|
|
3596
|
+
})
|
|
2771
3597
|
.catch((error) => {
|
|
2772
3598
|
try {
|
|
2773
3599
|
deps.onLateOutcomeError?.(error, update.update_id);
|
|
@@ -2776,11 +3602,81 @@ export function createTelegramUpdateAdmissionHandle(deps) {
|
|
|
2776
3602
|
// Diagnostic sinks must not create an unhandled late Promise.
|
|
2777
3603
|
}
|
|
2778
3604
|
});
|
|
2779
|
-
}
|
|
3605
|
+
}, { abandon: (authority) => {
|
|
3606
|
+
const isCurrent = () => !signal.aborted && routingClaims === 0 && !settlementReported && authority.isCurrent();
|
|
3607
|
+
if (immediate || outcome?.kind !== "deferred" || !deps.abandonDeferred || !isCurrent())
|
|
3608
|
+
return undefined;
|
|
3609
|
+
if (abandoned)
|
|
3610
|
+
return { ...abandoned, duplicate: true };
|
|
3611
|
+
const wasSuspended = suspended;
|
|
3612
|
+
suspended = true;
|
|
3613
|
+
// Failed/unknown publication keeps dispatch suspended for exact cancellation retry.
|
|
3614
|
+
// An ineligible request that attempted no publication remains inert.
|
|
3615
|
+
const result = deps.abandonDeferred({ ...authority, updateId: update.update_id, signal, isCurrent });
|
|
3616
|
+
if (result)
|
|
3617
|
+
abandoned = result;
|
|
3618
|
+
else
|
|
3619
|
+
suspended = wasSuspended;
|
|
3620
|
+
return result;
|
|
3621
|
+
}, inspectAbandoning: request => execution.isCurrent()
|
|
3622
|
+
? deps.inspectAbandoning?.({ ...request, signal }) : undefined,
|
|
3623
|
+
inspectHistorical: request => execution.isCurrent() ? deps.inspectHistorical?.({ ...request, signal }) : undefined,
|
|
3624
|
+
inspectSource: () => execution.isCurrent() && !immediate && !settlementReported
|
|
3625
|
+
? deps.inspectDeferredSource?.({ updateId: update.update_id, signal }) : undefined,
|
|
3626
|
+
isHistorical: matchesOriginal => !signal.aborted && deps.isHistoricalSource?.({ updateId: update.update_id, signal,
|
|
3627
|
+
...(matchesOriginal ? { matchesOriginal } : {}) }) === true,
|
|
3628
|
+
isHistoricalReviewHeld: () => !signal.aborted && suspended && outcome?.kind === "deferred" && outcome.routingReview === true,
|
|
3629
|
+
supportsAbandonment: journalBindingKey => !signal.aborted && !settlementReported &&
|
|
3630
|
+
deps.supportsDeferredAbandonment?.({ updateId: update.update_id, signal, journalBindingKey }) === true,
|
|
3631
|
+
armRoutingInput: (operatorUserId, sourceUpdateIds) => {
|
|
3632
|
+
execution.assertCurrent();
|
|
3633
|
+
if (settlementReported || routingClaims || suspended)
|
|
3634
|
+
return undefined;
|
|
3635
|
+
if (routingClockUnknown)
|
|
3636
|
+
throw new TelegramUpdateAdmissionOutcomeError("Telegram routing input clock publication is unconfirmed.");
|
|
3637
|
+
const ids = [...sourceUpdateIds];
|
|
3638
|
+
if (routingSourceIds && !isDeepStrictEqual(ids, routingSourceIds))
|
|
3639
|
+
throw new TelegramUpdateAdmissionOutcomeError("Telegram routing input source membership changed.");
|
|
3640
|
+
try {
|
|
3641
|
+
routingLifetime = deps.armRoutingInput?.({ updateId: update.update_id, signal, operatorUserId, sourceUpdateIds: ids });
|
|
3642
|
+
}
|
|
3643
|
+
catch (error) {
|
|
3644
|
+
routingClockUnknown = true;
|
|
3645
|
+
throw error;
|
|
3646
|
+
}
|
|
3647
|
+
if (routingLifetime)
|
|
3648
|
+
routingSourceIds = ids;
|
|
3649
|
+
return routingLifetime ? { ...routingLifetime } : undefined;
|
|
3650
|
+
},
|
|
3651
|
+
acquireRouting: (select = false, sourceUpdateIds) => {
|
|
3652
|
+
execution.assertCurrent();
|
|
3653
|
+
if (select && routingClockUnknown)
|
|
3654
|
+
throw new TelegramUpdateAdmissionOutcomeError("Telegram routing input clock publication is unconfirmed; selection is protected.");
|
|
3655
|
+
if (select && routingLifetime && !routingSelectionConfirmed) {
|
|
3656
|
+
if (routingSelectionAttempted)
|
|
3657
|
+
throw new TelegramUpdateAdmissionOutcomeError("Telegram routing selection is unconfirmed; no replay is permitted.");
|
|
3658
|
+
if (!isDeepStrictEqual(sourceUpdateIds ?? [update.update_id], routingSourceIds))
|
|
3659
|
+
throw new TelegramUpdateAdmissionOutcomeError("Telegram routing input selection changed its source membership.");
|
|
3660
|
+
routingSelectionAttempted = true;
|
|
3661
|
+
routingSelectionConfirmed = deps.selectRoutingInput?.({ updateId: update.update_id, signal,
|
|
3662
|
+
operatorUserId: routingLifetime.operatorUserId, sourceUpdateIds: routingSourceIds }) === true;
|
|
3663
|
+
if (!routingSelectionConfirmed)
|
|
3664
|
+
throw new TelegramUpdateAdmissionOutcomeError("Telegram routing input choice is expired or unavailable.");
|
|
3665
|
+
}
|
|
3666
|
+
routingClaims += 1;
|
|
3667
|
+
let released = false;
|
|
3668
|
+
return () => {
|
|
3669
|
+
if (released)
|
|
3670
|
+
return;
|
|
3671
|
+
released = true;
|
|
3672
|
+
routingClaims -= 1;
|
|
3673
|
+
};
|
|
3674
|
+
} }), execution);
|
|
2780
3675
|
try {
|
|
2781
3676
|
execution.assertCurrent();
|
|
2782
3677
|
await deps.defaultHandle(boundUpdate, ctx, execution);
|
|
2783
|
-
|
|
3678
|
+
if (outcome?.kind !== "deferred" || !outcome.routingReview || signal.aborted)
|
|
3679
|
+
execution.assertCurrent();
|
|
2784
3680
|
}
|
|
2785
3681
|
finally {
|
|
2786
3682
|
immediate = false;
|
|
@@ -2813,17 +3709,21 @@ export function createTelegramQueueAdmissionSettlementMuxRuntime(runtimes) {
|
|
|
2813
3709
|
// Plan the whole request before mutation; unready is not an acknowledgement of completion.
|
|
2814
3710
|
const complete = (items, ctx, kind) => {
|
|
2815
3711
|
const plan = new Map();
|
|
3712
|
+
const reason = kind === "prompt" ? "prompt-handoff" : kind === "control" ? "control-settlement" : "discard";
|
|
3713
|
+
const settlementOwner = (runtime, receipt) => runtime.getQueueReceiptSettlementOwner ? runtime.getQueueReceiptSettlementOwner(receipt, ctx, reason) :
|
|
3714
|
+
runtime.isItemReady({ admissionReceipts: [receipt] }) ? runtime.getQueueReceiptOwner(receipt) : undefined;
|
|
2816
3715
|
for (const receipt of items.flatMap(item => item.admissionReceipts ?? [])) {
|
|
2817
|
-
if (
|
|
3716
|
+
if (isQueueReceiptCompletionAcknowledged(receipt))
|
|
2818
3717
|
continue;
|
|
2819
|
-
const candidates = runtimes.filter(runtime => runtime.
|
|
3718
|
+
const candidates = runtimes.filter(runtime => runtime.getQueueReceiptSettlementOwner ? !!settlementOwner(runtime, receipt)
|
|
3719
|
+
: runtime.isItemReady({ admissionReceipts: [receipt] }));
|
|
2820
3720
|
const selected = candidates[0];
|
|
2821
3721
|
if (!selected)
|
|
2822
3722
|
return false;
|
|
2823
3723
|
if (candidates.length > 1) {
|
|
2824
|
-
const owner = selected
|
|
3724
|
+
const owner = settlementOwner(selected, receipt);
|
|
2825
3725
|
if (!owner || candidates.some(runtime => {
|
|
2826
|
-
const candidate = runtime
|
|
3726
|
+
const candidate = settlementOwner(runtime, receipt);
|
|
2827
3727
|
return !candidate || !areTelegramUpdateJournalQueueOwnersEqual(owner, candidate);
|
|
2828
3728
|
}))
|
|
2829
3729
|
return false;
|
|
@@ -2868,7 +3768,7 @@ export function createTelegramQueueAdmissionSettlementRuntime(worker) {
|
|
|
2868
3768
|
const receipts = [];
|
|
2869
3769
|
for (const item of items) {
|
|
2870
3770
|
for (const receipt of item.admissionReceipts ?? []) {
|
|
2871
|
-
if (
|
|
3771
|
+
if (!isQueueReceiptCompletionAcknowledged(receipt))
|
|
2872
3772
|
receipts.push(receipt);
|
|
2873
3773
|
}
|
|
2874
3774
|
}
|
|
@@ -2877,6 +3777,7 @@ export function createTelegramQueueAdmissionSettlementRuntime(worker) {
|
|
|
2877
3777
|
return {
|
|
2878
3778
|
isItemReady: (item) => (item.admissionReceipts ?? []).every(worker.isQueueReceiptCommitted),
|
|
2879
3779
|
getQueueReceiptOwner: worker.getQueueReceiptOwner,
|
|
3780
|
+
...(worker.getQueueReceiptSettlementOwner ? { getQueueReceiptSettlementOwner: worker.getQueueReceiptSettlementOwner } : {}),
|
|
2880
3781
|
onPromptHandedOff: (item, ctx) => complete([item], ctx, "prompt-handoff"),
|
|
2881
3782
|
onControlSettled: (item, ctx) => complete([item], ctx, "control-settlement"),
|
|
2882
3783
|
onItemsDiscarded: (items, ctx) => complete(items, ctx, "discard"),
|
|
@@ -3048,9 +3949,9 @@ export function createTelegramQueueHandoffReconciliationRuntimeAssembly(deps) {
|
|
|
3048
3949
|
canHandoffWithLeader: deps.canHandoffWithLeader,
|
|
3049
3950
|
listFollowers: deps.listFollowers,
|
|
3050
3951
|
createRecipientJournalBindingKey(recipient) {
|
|
3051
|
-
if (!recipient.profileKey)
|
|
3952
|
+
if (!recipient.profileKey || !recipient.sessionId)
|
|
3052
3953
|
return undefined;
|
|
3053
|
-
return deps.createRecipientJournalResolver(recipient.profileKey)()
|
|
3954
|
+
return deps.createRecipientJournalResolver(recipient.profileKey, recipient.sessionId)()
|
|
3054
3955
|
?.recoveryKey;
|
|
3055
3956
|
},
|
|
3056
3957
|
getQueuedItems: deps.queueStore.getQueuedItems,
|
|
@@ -3228,17 +4129,18 @@ export function createTelegramUpdateWorkerOwnerRuntime(deps) {
|
|
|
3228
4129
|
sessionGeneration: deps.getSessionGeneration(),
|
|
3229
4130
|
};
|
|
3230
4131
|
},
|
|
3231
|
-
onQueueReceiptCommitted(
|
|
4132
|
+
onQueueReceiptCommitted(receipt, ctx) {
|
|
3232
4133
|
if (!deps.isContextCurrent(ctx))
|
|
3233
4134
|
return;
|
|
4135
|
+
deps.afterQueueReceiptCommitted?.(receipt, ctx);
|
|
3234
4136
|
deps.dispatchNext(ctx);
|
|
3235
4137
|
deps.requestQueueHandoffReconciliation(ctx);
|
|
3236
4138
|
},
|
|
3237
|
-
onUpdateCompleted(updateId, ctx) {
|
|
4139
|
+
onUpdateCompleted(updateId, ctx, journalBindingKey) {
|
|
3238
4140
|
if (!deps.isContextCurrent(ctx))
|
|
3239
4141
|
return;
|
|
3240
4142
|
deps.dispatchNext(ctx);
|
|
3241
|
-
deps.afterUpdateCompleted?.(updateId);
|
|
4143
|
+
deps.afterUpdateCompleted?.(updateId, ctx, journalBindingKey);
|
|
3242
4144
|
},
|
|
3243
4145
|
};
|
|
3244
4146
|
}
|
|
@@ -3247,6 +4149,7 @@ export function createTelegramUpdateAdmissionRuntimeBinding(deps) {
|
|
|
3247
4149
|
let follower;
|
|
3248
4150
|
let settlement;
|
|
3249
4151
|
let inputCustodyBus;
|
|
4152
|
+
const getActive = () => deps.isFollowerRegistered() ? follower : leader;
|
|
3250
4153
|
return {
|
|
3251
4154
|
bind(input) {
|
|
3252
4155
|
leader = input.leader;
|
|
@@ -3259,7 +4162,7 @@ export function createTelegramUpdateAdmissionRuntimeBinding(deps) {
|
|
|
3259
4162
|
},
|
|
3260
4163
|
getLeader: () => leader,
|
|
3261
4164
|
getFollower: () => follower,
|
|
3262
|
-
getActive
|
|
4165
|
+
getActive,
|
|
3263
4166
|
getSettlement: () => settlement,
|
|
3264
4167
|
getInputCustodyBus: () => inputCustodyBus,
|
|
3265
4168
|
getLifecycleForJournalBinding(journalBindingKey) {
|
|
@@ -3358,6 +4261,7 @@ export function createTelegramUpdateAdmissionLifecycleRuntime(deps) {
|
|
|
3358
4261
|
binding.recoveryKey !== recoveryKey) {
|
|
3359
4262
|
throw new Error("Telegram update source binding changed during startup.");
|
|
3360
4263
|
}
|
|
4264
|
+
deps.prepareBinding?.(binding);
|
|
3361
4265
|
const release = deps.acquireSourceReference?.(binding);
|
|
3362
4266
|
const inputCustody = binding.journal.inputCustody;
|
|
3363
4267
|
try {
|
|
@@ -3580,6 +4484,14 @@ export function createTelegramUpdateAdmissionWorkerRuntime(deps) {
|
|
|
3580
4484
|
const executeUpdate = createTelegramUpdateAdmissionHandle({
|
|
3581
4485
|
defaultHandle: deps.defaultHandle,
|
|
3582
4486
|
registry: deps.registry,
|
|
4487
|
+
abandonDeferred: input => worker?.abandonDeferred?.(input),
|
|
4488
|
+
armRoutingInput: input => worker?.armRoutingInput?.(input),
|
|
4489
|
+
selectRoutingInput: input => worker?.selectRoutingInput?.(input) === true,
|
|
4490
|
+
supportsDeferredAbandonment: input => worker?.supportsDeferredAbandonment?.(input) === true,
|
|
4491
|
+
inspectAbandoning: input => worker?.inspectAbandoning?.(input),
|
|
4492
|
+
inspectHistorical: input => worker?.inspectHistorical?.(input),
|
|
4493
|
+
inspectDeferredSource: input => worker?.inspectDeferredSource?.(input),
|
|
4494
|
+
isHistoricalSource: input => worker?.isHistoricalSource?.(input) === true,
|
|
3583
4495
|
onLateOutcome(outcome, details) {
|
|
3584
4496
|
worker?.settleDeferred({
|
|
3585
4497
|
updateId: details.updateId,
|
|
@@ -3647,6 +4559,7 @@ export function createTelegramUpdateAdmissionLifecycleAssembly(deps) {
|
|
|
3647
4559
|
const follower = createTelegramUpdateAdmissionLifecycleRuntime({
|
|
3648
4560
|
getQueueOwnerIdentity: deps.worker.getQueueOwnerIdentity,
|
|
3649
4561
|
...(deps.acquireSourceReference ? { acquireSourceReference: binding => deps.acquireSourceReference("follower", binding) } : {}),
|
|
4562
|
+
...(deps.follower.prepareBinding ? { prepareBinding: deps.follower.prepareBinding } : {}),
|
|
3650
4563
|
resolveBinding() {
|
|
3651
4564
|
const generation = deps.follower.getGeneration();
|
|
3652
4565
|
if (!deps.follower.isRegistered() || !generation)
|
|
@@ -3671,6 +4584,11 @@ export function createTelegramUpdateAdmissionLifecycleAssembly(deps) {
|
|
|
3671
4584
|
getJournalBindingKey: () => binding.recoveryKey,
|
|
3672
4585
|
getRecipientBindingKey: () => binding.recipientBindingKey,
|
|
3673
4586
|
hasAuthority: () => binding.hasAuthority?.() ?? false,
|
|
4587
|
+
// Recipient custody is not an orphaned direct-bot source.
|
|
4588
|
+
shouldReviewHistoricalInput: undefined,
|
|
4589
|
+
shouldHoldPendingInput: undefined,
|
|
4590
|
+
spendHistoricalInput: undefined,
|
|
4591
|
+
onHeldSourcesPrepared: undefined,
|
|
3674
4592
|
prepareUpdateForExecution: deps.follower.prepareUpdateForExecution,
|
|
3675
4593
|
});
|
|
3676
4594
|
},
|