@llblab/pi-telegram 0.46.0 → 0.48.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +4 -2
- package/BACKLOG.md +0 -6
- package/CHANGELOG.md +14 -0
- package/README.md +5 -2
- package/dist/api/activity.d.ts +6 -0
- package/dist/api/activity.js +6 -0
- package/dist/api/commands.d.ts +6 -0
- package/dist/api/commands.js +6 -0
- package/dist/api/delivery.d.ts +6 -0
- package/dist/api/delivery.js +6 -0
- package/dist/api/inbound.d.ts +6 -0
- package/dist/api/inbound.js +6 -0
- package/dist/api/keyboard.d.ts +6 -0
- package/dist/api/keyboard.js +6 -0
- package/dist/api/outbound.d.ts +6 -0
- package/dist/api/outbound.js +6 -0
- package/dist/api/sections.d.ts +7 -0
- package/dist/api/sections.js +6 -0
- package/dist/api/status.d.ts +6 -0
- package/dist/api/status.js +6 -0
- package/dist/api/updates.d.ts +6 -0
- package/dist/api/updates.js +6 -0
- package/dist/api/voice.d.ts +6 -0
- package/dist/api/voice.js +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/lib/activity-verbosity.d.ts +52 -0
- package/dist/lib/activity-verbosity.js +596 -0
- package/dist/lib/activity.d.ts +220 -0
- package/dist/lib/activity.js +574 -0
- package/dist/lib/agent-messages.d.ts +28 -0
- package/dist/lib/agent-messages.js +86 -0
- package/dist/lib/bindings.d.ts +250 -0
- package/dist/lib/bindings.js +936 -0
- package/dist/lib/bus-api.d.ts +18 -0
- package/dist/lib/bus-api.js +254 -0
- package/dist/lib/bus-follower.d.ts +464 -0
- package/dist/lib/bus-follower.js +1686 -0
- package/dist/lib/bus-leader.d.ts +292 -0
- package/dist/lib/bus-leader.js +2242 -0
- package/dist/lib/bus-transport.d.ts +64 -0
- package/dist/lib/bus-transport.js +140 -0
- package/dist/lib/bus.d.ts +518 -0
- package/dist/lib/bus.js +2055 -0
- package/dist/lib/channel-posts.d.ts +170 -0
- package/dist/lib/channel-posts.js +611 -0
- package/dist/lib/command-templates.d.ts +70 -0
- package/dist/lib/command-templates.js +744 -0
- package/dist/lib/commands.d.ts +541 -0
- package/dist/lib/commands.js +1155 -0
- package/dist/lib/config.d.ts +252 -0
- package/dist/lib/config.js +889 -0
- package/dist/lib/delivery.d.ts +163 -0
- package/dist/lib/delivery.js +542 -0
- package/dist/lib/extension.d.ts +7 -0
- package/dist/lib/extension.js +1604 -0
- package/dist/lib/generative-app-worker.mjs +104 -0
- package/dist/lib/generative-apps.d.ts +212 -0
- package/dist/lib/generative-apps.js +1006 -0
- package/dist/lib/inbound.d.ts +91 -0
- package/dist/lib/inbound.js +502 -0
- package/dist/lib/journal.d.ts +665 -0
- package/dist/lib/journal.js +3701 -0
- package/dist/lib/keyboard.d.ts +23 -0
- package/dist/lib/keyboard.js +37 -0
- package/dist/lib/lifecycle.d.ts +157 -0
- package/dist/lib/lifecycle.js +395 -0
- package/dist/lib/locks.d.ts +164 -0
- package/dist/lib/locks.js +1208 -0
- package/dist/lib/logging.d.ts +50 -0
- package/dist/lib/logging.js +274 -0
- package/dist/lib/media.d.ts +181 -0
- package/dist/lib/media.js +602 -0
- package/dist/lib/menu-model.d.ts +217 -0
- package/dist/lib/menu-model.js +663 -0
- package/dist/lib/menu-queue.d.ts +37 -0
- package/dist/lib/menu-queue.js +408 -0
- package/dist/lib/menu-settings.d.ts +115 -0
- package/dist/lib/menu-settings.js +595 -0
- package/dist/lib/menu-status.d.ts +32 -0
- package/dist/lib/menu-status.js +112 -0
- package/dist/lib/menu-thinking.d.ts +29 -0
- package/dist/lib/menu-thinking.js +82 -0
- package/dist/lib/menu.d.ts +171 -0
- package/dist/lib/menu.js +323 -0
- package/dist/lib/model.d.ts +127 -0
- package/dist/lib/model.js +409 -0
- package/dist/lib/outbound-attachments.d.ts +241 -0
- package/dist/lib/outbound-attachments.js +639 -0
- package/dist/lib/outbound-buttons.d.ts +69 -0
- package/dist/lib/outbound-buttons.js +248 -0
- package/dist/lib/outbound-markup.d.ts +42 -0
- package/dist/lib/outbound-markup.js +678 -0
- package/dist/lib/outbound-voice.d.ts +55 -0
- package/dist/lib/outbound-voice.js +152 -0
- package/dist/lib/outbound.d.ts +185 -0
- package/dist/lib/outbound.js +516 -0
- package/dist/lib/ownership.d.ts +77 -0
- package/dist/lib/ownership.js +174 -0
- package/dist/lib/paths.d.ts +40 -0
- package/dist/lib/paths.js +94 -0
- package/dist/lib/pi.d.ts +72 -0
- package/dist/lib/pi.js +121 -0
- package/dist/lib/polling.d.ts +351 -0
- package/dist/lib/polling.js +1196 -0
- package/dist/lib/preview.d.ts +192 -0
- package/dist/lib/preview.js +614 -0
- package/dist/lib/prompt-templates.d.ts +24 -0
- package/dist/lib/prompt-templates.js +118 -0
- package/dist/lib/prompts.d.ts +57 -0
- package/dist/lib/prompts.js +167 -0
- package/dist/lib/queue.d.ts +766 -0
- package/dist/lib/queue.js +1965 -0
- package/dist/lib/recovery.d.ts +86 -0
- package/dist/lib/recovery.js +285 -0
- package/dist/lib/rendering.d.ts +20 -0
- package/dist/lib/rendering.js +983 -0
- package/dist/lib/replies.d.ts +214 -0
- package/dist/lib/replies.js +737 -0
- package/dist/lib/routing.d.ts +207 -0
- package/dist/lib/routing.js +2282 -0
- package/dist/lib/runtime.d.ts +172 -0
- package/dist/lib/runtime.js +402 -0
- package/dist/lib/sections.d.ts +165 -0
- package/dist/lib/sections.js +327 -0
- package/dist/lib/setup.d.ts +82 -0
- package/dist/lib/setup.js +150 -0
- package/dist/lib/skills.d.ts +8 -0
- package/dist/lib/skills.js +12 -0
- package/dist/lib/status.d.ts +442 -0
- package/dist/lib/status.js +1008 -0
- package/dist/lib/sync.d.ts +179 -0
- package/dist/lib/sync.js +782 -0
- package/dist/lib/target.d.ts +20 -0
- package/dist/lib/target.js +27 -0
- package/dist/lib/telegram-api.d.ts +541 -0
- package/dist/lib/telegram-api.js +1159 -0
- package/dist/lib/text-groups.d.ts +89 -0
- package/dist/lib/text-groups.js +317 -0
- package/dist/lib/thread-cleanup-manager.d.ts +288 -0
- package/dist/lib/thread-cleanup-manager.js +560 -0
- package/dist/lib/thread-display.d.ts +46 -0
- package/dist/lib/thread-display.js +257 -0
- package/dist/lib/thread-naming.d.ts +46 -0
- package/dist/lib/thread-naming.js +78 -0
- package/dist/lib/thread-reconciler.d.ts +239 -0
- package/dist/lib/thread-reconciler.js +644 -0
- package/dist/lib/threads.d.ts +621 -0
- package/dist/lib/threads.js +3679 -0
- package/dist/lib/time-injection.d.ts +15 -0
- package/dist/lib/time-injection.js +56 -0
- package/dist/lib/turns.d.ts +109 -0
- package/dist/lib/turns.js +500 -0
- package/dist/lib/updates.d.ts +1290 -0
- package/dist/lib/updates.js +3634 -0
- package/dist/lib/voice.d.ts +117 -0
- package/dist/lib/voice.js +174 -0
- package/dist/lib/workspace-admission.d.ts +264 -0
- package/dist/lib/workspace-admission.js +1136 -0
- package/dist/lib/workspace-retirement.d.ts +219 -0
- package/dist/lib/workspace-retirement.js +587 -0
- package/dist/lib/workspace-slots.d.ts +30 -0
- package/dist/lib/workspace-slots.js +54 -0
- package/dist/package.json +126 -0
- package/dist/pi-telegram/index.js +1 -0
- package/dist/skills/generated-control-surface/SKILL.md +107 -0
- package/dist/skills/generated-control-surface/references/capability-adapters.md +27 -0
- package/dist/skills/generated-control-surface/references/layout-and-state.md +37 -0
- package/dist/skills/generative-apps/SKILL.md +115 -0
- package/dist/skills/show-me/SKILL.md +166 -0
- package/dist/skills/show-me/references/telegram-surfaces.md +43 -0
- package/dist/skills/telegram-bridge/SKILL.md +129 -0
- package/dist/skills/telegram-bridge/references/configuration.md +15 -0
- package/dist/skills/telegram-bridge/references/delivery-and-threads.md +27 -0
- package/dist/skills/telegram-bridge/references/diagnosis.md +18 -0
- package/docs/README.md +2 -0
- package/docs/architecture.md +1 -1
- package/docs/compact-matrix-literal.md +10 -3
- package/docs/generative-apps.md +15 -13
- package/docs/multi-instance-bus.md +6 -4
- package/docs/outbound.md +1 -1
- package/docs/public-api.md +3 -3
- package/lib/bindings.ts +89 -6
- package/lib/bus-follower.ts +7 -3
- package/lib/bus-leader.ts +44 -19
- package/lib/bus.ts +4 -1
- package/lib/commands.ts +2 -2
- package/lib/config.ts +14 -7
- package/lib/delivery.ts +38 -6
- package/lib/extension.ts +7 -0
- package/lib/generative-apps.ts +379 -13
- package/lib/menu-settings.ts +20 -6
- package/lib/outbound-markup.ts +16 -7
- package/lib/status.ts +15 -12
- package/lib/telegram-api.ts +12 -1
- package/lib/thread-display.ts +96 -18
- package/package.json +56 -13
- package/scripts/build-dist.mjs +44 -0
- package/scripts/measure-bus.mjs +8 -1
- package/scripts/measure-workspace.mjs +8 -1
- package/skills/generative-apps/SKILL.md +1 -1
- package/skills/show-me/SKILL.md +1 -1
- package/skills/show-me/references/telegram-surfaces.md +1 -1
- package/skills/telegram-bridge/SKILL.md +1 -1
|
@@ -0,0 +1,3634 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram updates domain helpers
|
|
3
|
+
* Zones: telegram inbound, authorization, routing plans
|
|
4
|
+
* Owns update extraction, authorization, execution planning, generation-fenced journal draining, and the public update-handler registry
|
|
5
|
+
*/
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
7
|
+
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";
|
|
9
|
+
import { areTelegramQueueAdmissionReceiptsEqual, createTelegramQueueHandoff, removeTelegramQueueItemByReceipt, } from "./queue.js";
|
|
10
|
+
import { createTelegramUserPairingRuntime, getTelegramAuthorizationState, } from "./config.js";
|
|
11
|
+
export const TELEGRAM_PRIORITY_REACTIONS = [
|
|
12
|
+
{ id: 10, name: "like", emoji: "👍" },
|
|
13
|
+
{ id: 11, name: "lightning", emoji: "⚡" },
|
|
14
|
+
{ id: 12, name: "heart", emoji: "❤" },
|
|
15
|
+
{ id: 13, name: "dove", emoji: "🕊" },
|
|
16
|
+
{ id: 14, name: "fire", emoji: "🔥" },
|
|
17
|
+
];
|
|
18
|
+
export const TELEGRAM_REMOVAL_REACTIONS = [
|
|
19
|
+
{ id: 20, name: "dislike", emoji: "👎" },
|
|
20
|
+
{ id: 21, name: "ghost", emoji: "👻" },
|
|
21
|
+
{ id: 22, name: "broken-heart", emoji: "💔" },
|
|
22
|
+
{ id: 23, name: "poop", emoji: "💩" },
|
|
23
|
+
{ id: 24, name: "wastebasket", emoji: "🗑" },
|
|
24
|
+
];
|
|
25
|
+
export const TELEGRAM_PRIORITY_REACTION_EMOJIS = TELEGRAM_PRIORITY_REACTIONS.map((reaction) => reaction.emoji);
|
|
26
|
+
export const TELEGRAM_REMOVAL_REACTION_EMOJIS = TELEGRAM_REMOVAL_REACTIONS.map((reaction) => reaction.emoji);
|
|
27
|
+
function isTelegramMessageIdList(value) {
|
|
28
|
+
return Array.isArray(value) && value.every((item) => Number.isInteger(item));
|
|
29
|
+
}
|
|
30
|
+
export function normalizeTelegramReactionEmoji(emoji) {
|
|
31
|
+
return emoji.replace(/\uFE0F/g, "");
|
|
32
|
+
}
|
|
33
|
+
export function collectTelegramReactionEmojis(reactions) {
|
|
34
|
+
const emojis = new Set();
|
|
35
|
+
for (const reaction of reactions) {
|
|
36
|
+
if (reaction.type === "emoji") {
|
|
37
|
+
const emojiReaction = reaction;
|
|
38
|
+
emojis.add(normalizeTelegramReactionEmoji(emojiReaction.emoji));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return emojis;
|
|
42
|
+
}
|
|
43
|
+
function getTelegramReactionEmoji(emojis, candidates) {
|
|
44
|
+
return candidates.find((emoji) => emojis.has(emoji));
|
|
45
|
+
}
|
|
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
|
+
function getTelegramQueueReactionTransition(oldReactions, newReactions) {
|
|
64
|
+
const oldEmojis = collectTelegramReactionEmojis(oldReactions);
|
|
65
|
+
const newEmojis = collectTelegramReactionEmojis(newReactions);
|
|
66
|
+
const oldPriorityEmoji = getTelegramReactionEmoji(oldEmojis, TELEGRAM_PRIORITY_REACTION_EMOJIS);
|
|
67
|
+
const newPriorityEmoji = getTelegramReactionEmoji(newEmojis, TELEGRAM_PRIORITY_REACTION_EMOJIS);
|
|
68
|
+
const oldSuppressionEmoji = getTelegramReactionEmoji(oldEmojis, TELEGRAM_REMOVAL_REACTION_EMOJIS);
|
|
69
|
+
const newSuppressionEmoji = getTelegramReactionEmoji(newEmojis, TELEGRAM_REMOVAL_REACTION_EMOJIS);
|
|
70
|
+
if (oldPriorityEmoji === newPriorityEmoji &&
|
|
71
|
+
oldSuppressionEmoji === newSuppressionEmoji) {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
const transition = { kind: "reaction-transition" };
|
|
75
|
+
if (oldPriorityEmoji !== newPriorityEmoji) {
|
|
76
|
+
transition.priorityEmoji = newPriorityEmoji ?? null;
|
|
77
|
+
}
|
|
78
|
+
if (oldSuppressionEmoji !== newSuppressionEmoji) {
|
|
79
|
+
transition.suppressionEmoji = newSuppressionEmoji ?? null;
|
|
80
|
+
}
|
|
81
|
+
return transition;
|
|
82
|
+
}
|
|
83
|
+
export function extractDeletedTelegramMessageIds(update) {
|
|
84
|
+
const deletedBusinessMessageIds = update.deleted_business_messages?.message_ids;
|
|
85
|
+
if (isTelegramMessageIdList(deletedBusinessMessageIds)) {
|
|
86
|
+
return deletedBusinessMessageIds;
|
|
87
|
+
}
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
export function getTelegramTopicLifecycleUpdate(message) {
|
|
91
|
+
if (!message ||
|
|
92
|
+
typeof message.chat.id !== "number" ||
|
|
93
|
+
typeof message.message_thread_id !== "number") {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
const target = {
|
|
97
|
+
...createTelegramThreadTarget(message.chat.id, message.message_thread_id),
|
|
98
|
+
threadId: message.message_thread_id,
|
|
99
|
+
};
|
|
100
|
+
if (message.forum_topic_created !== undefined) {
|
|
101
|
+
return { kind: "created", message, target };
|
|
102
|
+
}
|
|
103
|
+
if (message.forum_topic_closed !== undefined) {
|
|
104
|
+
return { kind: "closed", message, target };
|
|
105
|
+
}
|
|
106
|
+
if (message.forum_topic_reopened !== undefined) {
|
|
107
|
+
return { kind: "reopened", message, target };
|
|
108
|
+
}
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
export function getTelegramMessageTarget(message) {
|
|
112
|
+
if (typeof message.chat.id !== "number")
|
|
113
|
+
return undefined;
|
|
114
|
+
return typeof message.message_thread_id === "number"
|
|
115
|
+
? createTelegramThreadTarget(message.chat.id, message.message_thread_id)
|
|
116
|
+
: createTelegramPrivateTarget(message.chat.id);
|
|
117
|
+
}
|
|
118
|
+
export function getAuthorizedTelegramCallbackQuery(update, allowedUserId) {
|
|
119
|
+
const query = update.callback_query;
|
|
120
|
+
if (!query || query.from.is_bot)
|
|
121
|
+
return undefined;
|
|
122
|
+
const message = query.message;
|
|
123
|
+
if (!message)
|
|
124
|
+
return undefined;
|
|
125
|
+
if (message.chat.type === "private")
|
|
126
|
+
return query;
|
|
127
|
+
return query.from.id === allowedUserId ? query : undefined;
|
|
128
|
+
}
|
|
129
|
+
export function getAuthorizedTelegramMessage(update, allowedUserId) {
|
|
130
|
+
const message = update.message;
|
|
131
|
+
if (!message || !message.from || message.from.is_bot)
|
|
132
|
+
return undefined;
|
|
133
|
+
if (message.chat.type === "private")
|
|
134
|
+
return message;
|
|
135
|
+
return message.from.id === allowedUserId ? message : undefined;
|
|
136
|
+
}
|
|
137
|
+
export function getAuthorizedTelegramEditedMessage(update, allowedUserId) {
|
|
138
|
+
const message = update.edited_message;
|
|
139
|
+
if (!message || !message.from || message.from.is_bot)
|
|
140
|
+
return undefined;
|
|
141
|
+
if (message.chat.type === "private")
|
|
142
|
+
return message;
|
|
143
|
+
return message.from.id === allowedUserId ? message : undefined;
|
|
144
|
+
}
|
|
145
|
+
export function getAuthorizedTelegramGuestMessage(update) {
|
|
146
|
+
const guestMessage = update.guest_message;
|
|
147
|
+
if (!guestMessage || !guestMessage.from || guestMessage.from.is_bot) {
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
return guestMessage;
|
|
151
|
+
}
|
|
152
|
+
export class TelegramForeignUpdateSettlementError extends Error {
|
|
153
|
+
settlement;
|
|
154
|
+
constructor(operation, settlement) {
|
|
155
|
+
super(`Telegram ${operation} forwarding did not settle: ${settlement.failureClass}.`);
|
|
156
|
+
this.name = "TelegramForeignUpdateSettlementError";
|
|
157
|
+
this.settlement = settlement;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function rejectTelegramForeignUpdateSettlement(settlement, operation, source) {
|
|
161
|
+
const sourceUpdateId = source && typeof source === "object"
|
|
162
|
+
? Reflect.get(source, "pi_telegram_source_update_id")
|
|
163
|
+
: undefined;
|
|
164
|
+
const failure = settlement && settlement.status !== "accepted"
|
|
165
|
+
? settlement
|
|
166
|
+
: {
|
|
167
|
+
status: "terminal-rejected",
|
|
168
|
+
failureClass: "forwarder-unavailable",
|
|
169
|
+
message: `Telegram ${operation} forwarding is unavailable.`,
|
|
170
|
+
...(Number.isSafeInteger(sourceUpdateId) && sourceUpdateId >= 0
|
|
171
|
+
? { sourceUpdateId }
|
|
172
|
+
: {}),
|
|
173
|
+
};
|
|
174
|
+
throw new TelegramForeignUpdateSettlementError(operation, failure);
|
|
175
|
+
}
|
|
176
|
+
export const TELEGRAM_INTERNAL_AGENT_MESSAGE = Symbol("telegram.internalAgentMessage");
|
|
177
|
+
const TELEGRAM_UPDATE_ADMISSION_BINDING = Symbol("telegram.update-admission.binding");
|
|
178
|
+
function bindTelegramUpdateAdmissionCarrier(value, binding) {
|
|
179
|
+
if (!value || typeof value !== "object")
|
|
180
|
+
return value;
|
|
181
|
+
return {
|
|
182
|
+
...value,
|
|
183
|
+
pi_telegram_source_update_id: binding.sourceUpdateId,
|
|
184
|
+
[TELEGRAM_UPDATE_ADMISSION_BINDING]: binding,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
function getTelegramUpdateAdmissionBinding(value) {
|
|
188
|
+
if (!value || typeof value !== "object")
|
|
189
|
+
return undefined;
|
|
190
|
+
const binding = Reflect.get(value, TELEGRAM_UPDATE_ADMISSION_BINDING);
|
|
191
|
+
return binding &&
|
|
192
|
+
Number.isSafeInteger(binding.sourceUpdateId) &&
|
|
193
|
+
binding.sourceUpdateId >= 0 &&
|
|
194
|
+
typeof binding.report === "function"
|
|
195
|
+
? binding
|
|
196
|
+
: undefined;
|
|
197
|
+
}
|
|
198
|
+
export function bindTelegramUpdateAdmissionSource(update, report) {
|
|
199
|
+
if (!Number.isSafeInteger(update.update_id) || update.update_id < 0) {
|
|
200
|
+
throw new TelegramUpdateAdmissionOutcomeError("Telegram update admission requires a safe update_id.");
|
|
201
|
+
}
|
|
202
|
+
const binding = {
|
|
203
|
+
sourceUpdateId: update.update_id,
|
|
204
|
+
report,
|
|
205
|
+
};
|
|
206
|
+
const callbackQuery = update.callback_query
|
|
207
|
+
? bindTelegramUpdateAdmissionCarrier({
|
|
208
|
+
...update.callback_query,
|
|
209
|
+
message: bindTelegramUpdateAdmissionCarrier(update.callback_query.message, binding),
|
|
210
|
+
}, binding)
|
|
211
|
+
: undefined;
|
|
212
|
+
return {
|
|
213
|
+
...update,
|
|
214
|
+
...(update.message
|
|
215
|
+
? { message: bindTelegramUpdateAdmissionCarrier(update.message, binding) }
|
|
216
|
+
: {}),
|
|
217
|
+
...(update.edited_message
|
|
218
|
+
? {
|
|
219
|
+
edited_message: bindTelegramUpdateAdmissionCarrier(update.edited_message, binding),
|
|
220
|
+
}
|
|
221
|
+
: {}),
|
|
222
|
+
...(callbackQuery ? { callback_query: callbackQuery } : {}),
|
|
223
|
+
...(update.guest_message
|
|
224
|
+
? {
|
|
225
|
+
guest_message: bindTelegramUpdateAdmissionCarrier(update.guest_message, binding),
|
|
226
|
+
}
|
|
227
|
+
: {}),
|
|
228
|
+
...(update.message_reaction
|
|
229
|
+
? {
|
|
230
|
+
message_reaction: bindTelegramUpdateAdmissionCarrier(update.message_reaction, binding),
|
|
231
|
+
}
|
|
232
|
+
: {}),
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
export function collectTelegramAdmissionSourceUpdateIds(values) {
|
|
236
|
+
const sourceUpdateIds = new Set();
|
|
237
|
+
for (const value of values) {
|
|
238
|
+
const binding = getTelegramUpdateAdmissionBinding(value);
|
|
239
|
+
if (binding)
|
|
240
|
+
sourceUpdateIds.add(binding.sourceUpdateId);
|
|
241
|
+
}
|
|
242
|
+
return [...sourceUpdateIds].sort((left, right) => left - right);
|
|
243
|
+
}
|
|
244
|
+
/** Report source completion; true means reported, not a durable settlement acknowledgement. */
|
|
245
|
+
export function reportTelegramUpdateCompleted(value) {
|
|
246
|
+
const binding = getTelegramUpdateAdmissionBinding(value);
|
|
247
|
+
if (!binding)
|
|
248
|
+
return false;
|
|
249
|
+
const execution = getTelegramUpdateExecutionFence(value);
|
|
250
|
+
if (execution && !execution.isCurrent())
|
|
251
|
+
return false;
|
|
252
|
+
binding.report({ kind: "complete" });
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
export function reportTelegramUpdateDeferred(value) {
|
|
256
|
+
const binding = getTelegramUpdateAdmissionBinding(value);
|
|
257
|
+
if (!binding)
|
|
258
|
+
return false;
|
|
259
|
+
binding.report({ kind: "deferred" });
|
|
260
|
+
return true;
|
|
261
|
+
}
|
|
262
|
+
export function reportTelegramQueueAdmission(values, receipts) {
|
|
263
|
+
const bindings = new Map();
|
|
264
|
+
for (const value of values) {
|
|
265
|
+
const binding = getTelegramUpdateAdmissionBinding(value);
|
|
266
|
+
if (!binding)
|
|
267
|
+
continue;
|
|
268
|
+
const existing = bindings.get(binding.sourceUpdateId);
|
|
269
|
+
if (existing && existing !== binding) {
|
|
270
|
+
throw new TelegramUpdateAdmissionOutcomeError(`Telegram update ${binding.sourceUpdateId} has conflicting admission bindings.`);
|
|
271
|
+
}
|
|
272
|
+
bindings.set(binding.sourceUpdateId, binding);
|
|
273
|
+
}
|
|
274
|
+
if (bindings.size === 0)
|
|
275
|
+
return false;
|
|
276
|
+
const receiptsByUpdateId = new Map();
|
|
277
|
+
for (const receipt of receipts) {
|
|
278
|
+
for (const sourceUpdateId of new Set(receipt.sourceUpdateIds)) {
|
|
279
|
+
const existing = receiptsByUpdateId.get(sourceUpdateId);
|
|
280
|
+
if (existing)
|
|
281
|
+
existing.count += 1;
|
|
282
|
+
else
|
|
283
|
+
receiptsByUpdateId.set(sourceUpdateId, { receipt, count: 1 });
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
const reports = [...bindings].map(([sourceUpdateId, binding]) => {
|
|
287
|
+
const match = receiptsByUpdateId.get(sourceUpdateId);
|
|
288
|
+
if (!match || match.count !== 1) {
|
|
289
|
+
throw new TelegramUpdateAdmissionOutcomeError(`Telegram update ${sourceUpdateId} requires one exact queue receipt.`);
|
|
290
|
+
}
|
|
291
|
+
return {
|
|
292
|
+
binding,
|
|
293
|
+
outcome: {
|
|
294
|
+
kind: "queued",
|
|
295
|
+
queueKind: match.receipt.queueKind,
|
|
296
|
+
receiptId: match.receipt.receiptId,
|
|
297
|
+
sourceUpdateIds: [...match.receipt.sourceUpdateIds],
|
|
298
|
+
},
|
|
299
|
+
};
|
|
300
|
+
});
|
|
301
|
+
for (const report of reports)
|
|
302
|
+
report.binding.report(report.outcome);
|
|
303
|
+
return true;
|
|
304
|
+
}
|
|
305
|
+
export function buildTelegramUpdateFlowAction(update, allowedUserId) {
|
|
306
|
+
const deletedMessageIds = extractDeletedTelegramMessageIds(update);
|
|
307
|
+
if (deletedMessageIds.length > 0) {
|
|
308
|
+
return { kind: "deleted", messageIds: deletedMessageIds };
|
|
309
|
+
}
|
|
310
|
+
if (update.message_reaction) {
|
|
311
|
+
return { kind: "reaction", reactionUpdate: update.message_reaction };
|
|
312
|
+
}
|
|
313
|
+
const topicLifecycle = getTelegramTopicLifecycleUpdate(update.message);
|
|
314
|
+
if (topicLifecycle) {
|
|
315
|
+
return { kind: "topic-lifecycle", lifecycle: topicLifecycle };
|
|
316
|
+
}
|
|
317
|
+
const query = getAuthorizedTelegramCallbackQuery(update, allowedUserId);
|
|
318
|
+
if (query) {
|
|
319
|
+
return {
|
|
320
|
+
kind: "callback",
|
|
321
|
+
query: query,
|
|
322
|
+
authorization: getTelegramAuthorizationState(query.from.id, allowedUserId),
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
const message = getAuthorizedTelegramMessage(update, allowedUserId);
|
|
326
|
+
if (message?.from) {
|
|
327
|
+
return {
|
|
328
|
+
kind: "message",
|
|
329
|
+
message: message,
|
|
330
|
+
authorization: getTelegramAuthorizationState(message.from.id, allowedUserId),
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
const editedMessage = getAuthorizedTelegramEditedMessage(update, allowedUserId);
|
|
334
|
+
if (editedMessage?.from) {
|
|
335
|
+
return {
|
|
336
|
+
kind: "edited-message",
|
|
337
|
+
message: editedMessage,
|
|
338
|
+
authorization: getTelegramAuthorizationState(editedMessage.from.id, allowedUserId),
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
const guestMessage = getAuthorizedTelegramGuestMessage(update);
|
|
342
|
+
if (guestMessage?.from) {
|
|
343
|
+
return {
|
|
344
|
+
kind: "guest",
|
|
345
|
+
guestMessage: guestMessage,
|
|
346
|
+
authorization: getTelegramAuthorizationState(guestMessage.from.id, allowedUserId),
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
return { kind: "ignore" };
|
|
350
|
+
}
|
|
351
|
+
export function buildTelegramUpdateExecutionPlan(action) {
|
|
352
|
+
switch (action.kind) {
|
|
353
|
+
case "ignore":
|
|
354
|
+
return { kind: "ignore" };
|
|
355
|
+
case "deleted":
|
|
356
|
+
return { kind: "deleted", messageIds: action.messageIds };
|
|
357
|
+
case "reaction":
|
|
358
|
+
return { kind: "reaction", reactionUpdate: action.reactionUpdate };
|
|
359
|
+
case "topic-lifecycle":
|
|
360
|
+
return { kind: "topic-lifecycle", lifecycle: action.lifecycle };
|
|
361
|
+
case "callback":
|
|
362
|
+
return {
|
|
363
|
+
kind: "callback",
|
|
364
|
+
query: action.query,
|
|
365
|
+
shouldPair: action.authorization.kind === "pair",
|
|
366
|
+
shouldDeny: action.authorization.kind === "deny",
|
|
367
|
+
};
|
|
368
|
+
case "message":
|
|
369
|
+
return {
|
|
370
|
+
kind: "message",
|
|
371
|
+
message: action.message,
|
|
372
|
+
shouldPair: action.authorization.kind === "pair",
|
|
373
|
+
shouldNotifyPaired: action.authorization.kind === "pair",
|
|
374
|
+
shouldDeny: action.authorization.kind === "deny",
|
|
375
|
+
};
|
|
376
|
+
case "edited-message":
|
|
377
|
+
return {
|
|
378
|
+
kind: "edited-message",
|
|
379
|
+
message: action.message,
|
|
380
|
+
shouldPair: action.authorization.kind === "pair",
|
|
381
|
+
shouldDeny: action.authorization.kind === "deny",
|
|
382
|
+
};
|
|
383
|
+
case "guest":
|
|
384
|
+
return {
|
|
385
|
+
kind: "guest",
|
|
386
|
+
guestMessage: action.guestMessage,
|
|
387
|
+
// Guest mode is an extension of an already paired bridge, not a pairing surface.
|
|
388
|
+
shouldDeny: action.authorization.kind !== "allow",
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
export function buildTelegramUpdateExecutionPlanFromUpdate(update, allowedUserId) {
|
|
393
|
+
return buildTelegramUpdateExecutionPlan(buildTelegramUpdateFlowAction(update, allowedUserId));
|
|
394
|
+
}
|
|
395
|
+
const TELEGRAM_UNAUTHORIZED_DENIAL_COPY = "Access denied.";
|
|
396
|
+
function formatTelegramUnauthorizedDenial(format) {
|
|
397
|
+
return format === "html"
|
|
398
|
+
? `<b>🚫 ${TELEGRAM_UNAUTHORIZED_DENIAL_COPY}</b>`
|
|
399
|
+
: `🚫 ${TELEGRAM_UNAUTHORIZED_DENIAL_COPY}`;
|
|
400
|
+
}
|
|
401
|
+
function getTelegramCallbackQueryId(query) {
|
|
402
|
+
return typeof query.id === "string" ? query.id : undefined;
|
|
403
|
+
}
|
|
404
|
+
function getTelegramMessageReplyTarget(message) {
|
|
405
|
+
if (typeof message.chat.id !== "number" ||
|
|
406
|
+
typeof message.message_id !== "number") {
|
|
407
|
+
return undefined;
|
|
408
|
+
}
|
|
409
|
+
return {
|
|
410
|
+
chatId: message.chat.id,
|
|
411
|
+
messageId: message.message_id,
|
|
412
|
+
...(typeof message.message_thread_id === "number"
|
|
413
|
+
? { threadId: message.message_thread_id }
|
|
414
|
+
: {}),
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
function getForeignTelegramMessageOwnership(target, deps) {
|
|
418
|
+
if (!target || !deps.getMessageOwnership || !deps.getCurrentInstanceId) {
|
|
419
|
+
return undefined;
|
|
420
|
+
}
|
|
421
|
+
const currentInstanceId = deps.getCurrentInstanceId();
|
|
422
|
+
if (!currentInstanceId)
|
|
423
|
+
return undefined;
|
|
424
|
+
const ownership = deps.getMessageOwnership(target.chatId, target.messageId);
|
|
425
|
+
return ownership && ownership.instanceId !== currentInstanceId
|
|
426
|
+
? ownership
|
|
427
|
+
: undefined;
|
|
428
|
+
}
|
|
429
|
+
function getForeignTelegramCallbackOwnership(query, deps) {
|
|
430
|
+
const messageOwnership = getForeignTelegramMessageOwnership(getTelegramCallbackMessageTarget(query), deps);
|
|
431
|
+
const targetOwnership = getForeignTelegramTargetOwnership(query.message ? getTelegramMessageTarget(query.message) : undefined, deps);
|
|
432
|
+
if (!messageOwnership)
|
|
433
|
+
return targetOwnership;
|
|
434
|
+
if (targetOwnership &&
|
|
435
|
+
messageOwnership.recipientBindingKey &&
|
|
436
|
+
messageOwnership.recipientBindingKey === targetOwnership.recipientBindingKey) {
|
|
437
|
+
return targetOwnership;
|
|
438
|
+
}
|
|
439
|
+
return messageOwnership;
|
|
440
|
+
}
|
|
441
|
+
function getTelegramCallbackMessageTarget(query) {
|
|
442
|
+
return query.message
|
|
443
|
+
? getTelegramMessageReplyTarget(query.message)
|
|
444
|
+
: undefined;
|
|
445
|
+
}
|
|
446
|
+
function getTelegramReactionMessageTarget(reactionUpdate) {
|
|
447
|
+
return typeof reactionUpdate.chat.id === "number"
|
|
448
|
+
? { chatId: reactionUpdate.chat.id, messageId: reactionUpdate.message_id }
|
|
449
|
+
: undefined;
|
|
450
|
+
}
|
|
451
|
+
function getForeignTelegramTargetOwnership(target, deps) {
|
|
452
|
+
if (!target || !deps.getTargetOwnership || !deps.getCurrentInstanceId) {
|
|
453
|
+
return undefined;
|
|
454
|
+
}
|
|
455
|
+
const currentInstanceId = deps.getCurrentInstanceId();
|
|
456
|
+
if (!currentInstanceId)
|
|
457
|
+
return undefined;
|
|
458
|
+
const ownership = deps.getTargetOwnership(target);
|
|
459
|
+
return ownership && ownership.instanceId !== currentInstanceId
|
|
460
|
+
? ownership
|
|
461
|
+
: undefined;
|
|
462
|
+
}
|
|
463
|
+
export async function executeTelegramUpdate(update, allowedUserId, deps) {
|
|
464
|
+
const runtimeDeps = update[TELEGRAM_INTERNAL_AGENT_MESSAGE]
|
|
465
|
+
? { ...deps, getMessageOwnership: undefined }
|
|
466
|
+
: deps;
|
|
467
|
+
await executeTelegramUpdatePlan(buildTelegramUpdateExecutionPlanFromUpdate(update, allowedUserId), runtimeDeps);
|
|
468
|
+
}
|
|
469
|
+
export function createTelegramPairedUpdateRuntime(deps) {
|
|
470
|
+
return createTelegramUpdateRuntime({
|
|
471
|
+
getAllowedUserId: deps.getAllowedUserId,
|
|
472
|
+
getCurrentInstanceId: deps.getCurrentInstanceId,
|
|
473
|
+
getMessageOwnership: deps.getMessageOwnership,
|
|
474
|
+
getTargetOwnership: deps.getTargetOwnership,
|
|
475
|
+
recordMessageOwnership: deps.recordMessageOwnership,
|
|
476
|
+
handleTelegramTopicLifecycleUpdate: deps.handleTelegramTopicLifecycleUpdate,
|
|
477
|
+
foreignOwnedUpdateForwarder: deps.foreignOwnedUpdateForwarder,
|
|
478
|
+
removePendingMediaGroupMessages: deps.removePendingMediaGroupMessages,
|
|
479
|
+
flushPendingMediaGroupMessage: deps.flushPendingMediaGroupMessage,
|
|
480
|
+
flushPendingTextGroupMessage: deps.flushPendingTextGroupMessage,
|
|
481
|
+
removeQueuedTelegramTurnsByMessageIds: deps.removeQueuedTelegramTurnsByMessageIds,
|
|
482
|
+
applyQueuedTelegramTurnReactionByMessageId: deps.applyQueuedTelegramTurnReactionByMessageId,
|
|
483
|
+
pairTelegramUserIfNeeded: (userId, ctx, assertExecutionCurrent) => createTelegramUserPairingRuntime({
|
|
484
|
+
getAllowedUserId: deps.getAllowedUserId,
|
|
485
|
+
persistAllowedUserId: deps.persistAllowedUserId,
|
|
486
|
+
updateStatus: deps.updateStatus,
|
|
487
|
+
}).pairIfNeeded(userId, ctx, assertExecutionCurrent),
|
|
488
|
+
answerCallbackQuery: deps.answerCallbackQuery,
|
|
489
|
+
answerGuestQuery: deps.answerGuestQuery,
|
|
490
|
+
handleAuthorizedTelegramCallbackQuery: deps.handleAuthorizedTelegramCallbackQuery,
|
|
491
|
+
sendTextReply: deps.sendTextReply,
|
|
492
|
+
handleAuthorizedTelegramMessage: deps.handleAuthorizedTelegramMessage,
|
|
493
|
+
handleAuthorizedTelegramEditedMessage: deps.handleAuthorizedTelegramEditedMessage,
|
|
494
|
+
handleAuthorizedTelegramGuestMessage: deps.handleAuthorizedTelegramGuestMessage,
|
|
495
|
+
handleUnboundTelegramTopicMessage: deps.handleUnboundTelegramTopicMessage,
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
export function createTelegramUpdateRuntime(deps) {
|
|
499
|
+
const handleAuthorizedReactionUpdate = async (reactionUpdate, ctx) => {
|
|
500
|
+
await handleAuthorizedTelegramReactionUpdate(reactionUpdate, {
|
|
501
|
+
allowedUserId: deps.getAllowedUserId(),
|
|
502
|
+
ctx,
|
|
503
|
+
flushPendingMediaGroupMessage: deps.flushPendingMediaGroupMessage,
|
|
504
|
+
flushPendingTextGroupMessage: deps.flushPendingTextGroupMessage,
|
|
505
|
+
getCurrentInstanceId: deps.getCurrentInstanceId,
|
|
506
|
+
getMessageOwnership: deps.getMessageOwnership,
|
|
507
|
+
foreignOwnedUpdateForwarder: deps.foreignOwnedUpdateForwarder,
|
|
508
|
+
assertExecutionCurrent: createTelegramUpdateExecutionFenceGuard(reactionUpdate),
|
|
509
|
+
applyQueuedTelegramTurnReactionByMessageId: deps.applyQueuedTelegramTurnReactionByMessageId,
|
|
510
|
+
});
|
|
511
|
+
};
|
|
512
|
+
return {
|
|
513
|
+
handleAuthorizedReactionUpdate,
|
|
514
|
+
handleUpdate: (update, ctx, execution) => executeTelegramUpdate(update, deps.getAllowedUserId(), {
|
|
515
|
+
ctx,
|
|
516
|
+
execution,
|
|
517
|
+
getCurrentInstanceId: deps.getCurrentInstanceId,
|
|
518
|
+
getMessageOwnership: deps.getMessageOwnership,
|
|
519
|
+
getTargetOwnership: deps.getTargetOwnership,
|
|
520
|
+
recordMessageOwnership: deps.recordMessageOwnership,
|
|
521
|
+
foreignOwnedUpdateForwarder: deps.foreignOwnedUpdateForwarder,
|
|
522
|
+
removePendingMediaGroupMessages: deps.removePendingMediaGroupMessages,
|
|
523
|
+
removeQueuedTelegramTurnsByMessageIds: deps.removeQueuedTelegramTurnsByMessageIds,
|
|
524
|
+
handleAuthorizedTelegramReactionUpdate: handleAuthorizedReactionUpdate,
|
|
525
|
+
handleTelegramTopicLifecycleUpdate: deps.handleTelegramTopicLifecycleUpdate,
|
|
526
|
+
pairTelegramUserIfNeeded: deps.pairTelegramUserIfNeeded,
|
|
527
|
+
answerCallbackQuery: deps.answerCallbackQuery,
|
|
528
|
+
answerGuestQuery: deps.answerGuestQuery,
|
|
529
|
+
handleAuthorizedTelegramCallbackQuery: deps.handleAuthorizedTelegramCallbackQuery,
|
|
530
|
+
sendTextReply: deps.sendTextReply,
|
|
531
|
+
handleAuthorizedTelegramMessage: deps.handleAuthorizedTelegramMessage,
|
|
532
|
+
handleAuthorizedTelegramEditedMessage: deps.handleAuthorizedTelegramEditedMessage,
|
|
533
|
+
handleAuthorizedTelegramGuestMessage: deps.handleAuthorizedTelegramGuestMessage,
|
|
534
|
+
handleUnboundTelegramTopicMessage: deps.handleUnboundTelegramTopicMessage,
|
|
535
|
+
}),
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
export async function handleAuthorizedTelegramReactionUpdate(reactionUpdate, deps) {
|
|
539
|
+
const reactionUser = reactionUpdate.user;
|
|
540
|
+
const allowedUserId = deps.allowedUserId;
|
|
541
|
+
if (allowedUserId === undefined || !Number.isSafeInteger(allowedUserId) || allowedUserId <= 0 ||
|
|
542
|
+
!reactionUser || reactionUser.is_bot || reactionUser.id !== allowedUserId ||
|
|
543
|
+
reactionUpdate.actor_chat !== undefined)
|
|
544
|
+
return;
|
|
545
|
+
const foreignOwnership = getForeignTelegramMessageOwnership(getTelegramReactionMessageTarget(reactionUpdate), deps);
|
|
546
|
+
if (foreignOwnership) {
|
|
547
|
+
deps.assertExecutionCurrent?.();
|
|
548
|
+
const settlement = await deps.foreignOwnedUpdateForwarder?.forwardReaction?.({
|
|
549
|
+
reactionUpdate,
|
|
550
|
+
ownership: foreignOwnership,
|
|
551
|
+
ctx: deps.ctx,
|
|
552
|
+
});
|
|
553
|
+
deps.assertExecutionCurrent?.();
|
|
554
|
+
if (settlement?.status !== "accepted") {
|
|
555
|
+
rejectTelegramForeignUpdateSettlement(settlement, "reaction", reactionUpdate);
|
|
556
|
+
}
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
const reactionScope = typeof reactionUpdate.chat.id === "number"
|
|
560
|
+
? { chatId: reactionUpdate.chat.id }
|
|
561
|
+
: undefined;
|
|
562
|
+
const reactionTransition = getTelegramQueueReactionTransition(reactionUpdate.old_reaction, reactionUpdate.new_reaction);
|
|
563
|
+
if (!reactionTransition)
|
|
564
|
+
return;
|
|
565
|
+
deps.assertExecutionCurrent?.();
|
|
566
|
+
await deps.flushPendingMediaGroupMessage?.(reactionUpdate.message_id);
|
|
567
|
+
deps.assertExecutionCurrent?.();
|
|
568
|
+
await deps.flushPendingTextGroupMessage?.(reactionUpdate.message_id);
|
|
569
|
+
deps.assertExecutionCurrent?.();
|
|
570
|
+
deps.applyQueuedTelegramTurnReactionByMessageId(reactionUpdate.message_id, reactionTransition, deps.ctx, reactionScope);
|
|
571
|
+
}
|
|
572
|
+
function isTelegramStaleContextError(error) {
|
|
573
|
+
return (error instanceof Error &&
|
|
574
|
+
(error.message.includes("stale after session") ||
|
|
575
|
+
error.message.includes("stale ctx")));
|
|
576
|
+
}
|
|
577
|
+
export async function executeTelegramUpdatePlan(plan, deps) {
|
|
578
|
+
try {
|
|
579
|
+
const assertExecutionCurrent = () => deps.execution?.assertCurrent();
|
|
580
|
+
if (plan.kind === "ignore")
|
|
581
|
+
return;
|
|
582
|
+
if (plan.kind === "deleted") {
|
|
583
|
+
assertExecutionCurrent();
|
|
584
|
+
deps.removePendingMediaGroupMessages(plan.messageIds);
|
|
585
|
+
deps.removeQueuedTelegramTurnsByMessageIds(plan.messageIds, deps.ctx);
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
if (plan.kind === "reaction") {
|
|
589
|
+
assertExecutionCurrent();
|
|
590
|
+
await deps.handleAuthorizedTelegramReactionUpdate(plan.reactionUpdate, deps.ctx);
|
|
591
|
+
assertExecutionCurrent();
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
if (plan.kind === "topic-lifecycle") {
|
|
595
|
+
assertExecutionCurrent();
|
|
596
|
+
await deps.handleTelegramTopicLifecycleUpdate?.(plan.lifecycle, deps.ctx);
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
if (plan.kind === "callback") {
|
|
600
|
+
let pairingAllowed = true;
|
|
601
|
+
if (plan.shouldPair) {
|
|
602
|
+
assertExecutionCurrent();
|
|
603
|
+
pairingAllowed = await deps.pairTelegramUserIfNeeded(plan.query.from.id, deps.ctx, assertExecutionCurrent);
|
|
604
|
+
}
|
|
605
|
+
if (plan.shouldDeny || !pairingAllowed) {
|
|
606
|
+
const callbackQueryId = getTelegramCallbackQueryId(plan.query);
|
|
607
|
+
if (callbackQueryId) {
|
|
608
|
+
assertExecutionCurrent();
|
|
609
|
+
await deps.answerCallbackQuery(callbackQueryId, formatTelegramUnauthorizedDenial("plain"));
|
|
610
|
+
}
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
const foreignOwnership = getForeignTelegramCallbackOwnership(plan.query, deps);
|
|
614
|
+
if (foreignOwnership) {
|
|
615
|
+
assertExecutionCurrent();
|
|
616
|
+
const settlement = await deps.foreignOwnedUpdateForwarder?.forwardCallback?.({
|
|
617
|
+
query: plan.query,
|
|
618
|
+
ownership: foreignOwnership,
|
|
619
|
+
ctx: deps.ctx,
|
|
620
|
+
});
|
|
621
|
+
if (settlement?.status !== "accepted") {
|
|
622
|
+
const callbackQueryId = getTelegramCallbackQueryId(plan.query);
|
|
623
|
+
try {
|
|
624
|
+
if (callbackQueryId) {
|
|
625
|
+
assertExecutionCurrent();
|
|
626
|
+
await deps.answerCallbackQuery(callbackQueryId, "This Telegram message belongs to another Pi instance.");
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
finally {
|
|
630
|
+
rejectTelegramForeignUpdateSettlement(settlement, "callback", plan.query);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
assertExecutionCurrent();
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
assertExecutionCurrent();
|
|
637
|
+
await deps.handleAuthorizedTelegramCallbackQuery(plan.query, deps.ctx);
|
|
638
|
+
assertExecutionCurrent();
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
if (plan.kind === "guest") {
|
|
642
|
+
if (plan.shouldDeny) {
|
|
643
|
+
assertExecutionCurrent();
|
|
644
|
+
await deps.answerGuestQuery(plan.guestMessage.guest_query_id, formatTelegramUnauthorizedDenial("html"), { parseMode: "HTML" });
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
if (deps.handleAuthorizedTelegramGuestMessage) {
|
|
648
|
+
assertExecutionCurrent();
|
|
649
|
+
await deps.handleAuthorizedTelegramGuestMessage(plan.guestMessage, deps.ctx);
|
|
650
|
+
assertExecutionCurrent();
|
|
651
|
+
}
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
if (plan.shouldPair)
|
|
655
|
+
assertExecutionCurrent();
|
|
656
|
+
const pairedNow = plan.shouldPair
|
|
657
|
+
? await deps.pairTelegramUserIfNeeded(plan.message.from.id, deps.ctx, assertExecutionCurrent)
|
|
658
|
+
: false;
|
|
659
|
+
const replyTarget = getTelegramMessageReplyTarget(plan.message);
|
|
660
|
+
if (plan.shouldDeny || (plan.shouldPair && !pairedNow)) {
|
|
661
|
+
if (replyTarget) {
|
|
662
|
+
assertExecutionCurrent();
|
|
663
|
+
await deps.sendTextReply(replyTarget.chatId, replyTarget.messageId, formatTelegramUnauthorizedDenial("html"), { parseMode: "HTML", target: replyTarget });
|
|
664
|
+
}
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
if (plan.kind === "message" &&
|
|
668
|
+
pairedNow &&
|
|
669
|
+
plan.shouldNotifyPaired &&
|
|
670
|
+
replyTarget) {
|
|
671
|
+
assertExecutionCurrent();
|
|
672
|
+
await deps.sendTextReply(replyTarget.chatId, replyTarget.messageId, "Telegram bridge paired with this account.", { target: replyTarget });
|
|
673
|
+
assertExecutionCurrent();
|
|
674
|
+
}
|
|
675
|
+
const foreignMessageOwnership = getForeignTelegramMessageOwnership(replyTarget, deps);
|
|
676
|
+
if (foreignMessageOwnership) {
|
|
677
|
+
assertExecutionCurrent();
|
|
678
|
+
const settlement = plan.kind === "edited-message"
|
|
679
|
+
? await deps.foreignOwnedUpdateForwarder?.forwardEditedMessage?.({
|
|
680
|
+
message: plan.message,
|
|
681
|
+
ownership: foreignMessageOwnership,
|
|
682
|
+
ctx: deps.ctx,
|
|
683
|
+
})
|
|
684
|
+
: await deps.foreignOwnedUpdateForwarder?.forwardMessage?.({
|
|
685
|
+
message: plan.message,
|
|
686
|
+
ownership: foreignMessageOwnership,
|
|
687
|
+
ctx: deps.ctx,
|
|
688
|
+
});
|
|
689
|
+
if (settlement?.status !== "accepted") {
|
|
690
|
+
rejectTelegramForeignUpdateSettlement(settlement, plan.kind, plan.message);
|
|
691
|
+
}
|
|
692
|
+
assertExecutionCurrent();
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
const messageTarget = getTelegramMessageTarget(plan.message);
|
|
696
|
+
const foreignTargetOwnership = getForeignTelegramTargetOwnership(messageTarget, deps);
|
|
697
|
+
if (foreignTargetOwnership) {
|
|
698
|
+
if (typeof plan.message.message_id === "number") {
|
|
699
|
+
assertExecutionCurrent();
|
|
700
|
+
deps.recordMessageOwnership?.({
|
|
701
|
+
chatId: messageTarget.chatId,
|
|
702
|
+
messageId: plan.message.message_id,
|
|
703
|
+
target: messageTarget,
|
|
704
|
+
instanceId: foreignTargetOwnership.instanceId,
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
assertExecutionCurrent();
|
|
708
|
+
const settlement = plan.kind === "edited-message"
|
|
709
|
+
? await deps.foreignOwnedUpdateForwarder?.forwardEditedMessage?.({
|
|
710
|
+
message: plan.message,
|
|
711
|
+
ownership: foreignTargetOwnership,
|
|
712
|
+
ctx: deps.ctx,
|
|
713
|
+
})
|
|
714
|
+
: await deps.foreignOwnedUpdateForwarder?.forwardMessage?.({
|
|
715
|
+
message: plan.message,
|
|
716
|
+
ownership: foreignTargetOwnership,
|
|
717
|
+
ctx: deps.ctx,
|
|
718
|
+
});
|
|
719
|
+
if (settlement?.status !== "accepted") {
|
|
720
|
+
rejectTelegramForeignUpdateSettlement(settlement, plan.kind, plan.message);
|
|
721
|
+
}
|
|
722
|
+
assertExecutionCurrent();
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
if (plan.kind === "message" &&
|
|
726
|
+
messageTarget?.threadId != null &&
|
|
727
|
+
deps.handleUnboundTelegramTopicMessage) {
|
|
728
|
+
assertExecutionCurrent();
|
|
729
|
+
await deps.handleUnboundTelegramTopicMessage(plan.message, deps.ctx);
|
|
730
|
+
assertExecutionCurrent();
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
if (plan.kind === "edited-message") {
|
|
734
|
+
assertExecutionCurrent();
|
|
735
|
+
await deps.handleAuthorizedTelegramEditedMessage(plan.message, deps.ctx);
|
|
736
|
+
assertExecutionCurrent();
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
assertExecutionCurrent();
|
|
740
|
+
await deps.handleAuthorizedTelegramMessage(plan.message, deps.ctx);
|
|
741
|
+
assertExecutionCurrent();
|
|
742
|
+
}
|
|
743
|
+
catch (error) {
|
|
744
|
+
if (!isTelegramStaleContextError(error))
|
|
745
|
+
throw error;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
// --- Durable update worker ---
|
|
749
|
+
export const TELEGRAM_UPDATE_RETRY_BASE_DELAY_MS = 1_000;
|
|
750
|
+
export const TELEGRAM_UPDATE_RETRY_MAX_DELAY_MS = 60_000;
|
|
751
|
+
export const TELEGRAM_UPDATE_WORKER_BATCH_SIZE = 64;
|
|
752
|
+
export class TelegramUpdateAdmissionOutcomeError extends Error {
|
|
753
|
+
constructor(message) {
|
|
754
|
+
super(message);
|
|
755
|
+
this.name = "TelegramUpdateAdmissionOutcomeError";
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
const TELEGRAM_UPDATE_WORKER_EXECUTION_ABORTED = Symbol("telegram.update-worker.execution-aborted");
|
|
759
|
+
function getTelegramUpdateWorkerStateSnapshot(state) {
|
|
760
|
+
return { ...state };
|
|
761
|
+
}
|
|
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
|
+
function validateTelegramUpdateAdmissionOutcome(value, currentUpdateId, claimableUpdateIds) {
|
|
769
|
+
if (!isTelegramUpdateAdmissionRecord(value)) {
|
|
770
|
+
throw new TelegramUpdateAdmissionOutcomeError(`Telegram update ${currentUpdateId} returned no admission outcome.`);
|
|
771
|
+
}
|
|
772
|
+
if (value.kind === "complete")
|
|
773
|
+
return { kind: "complete" };
|
|
774
|
+
if (value.kind === "deferred")
|
|
775
|
+
return { kind: "deferred" };
|
|
776
|
+
if (value.kind === "queued") {
|
|
777
|
+
if ((value.queueKind !== "prompt" && value.queueKind !== "control") ||
|
|
778
|
+
!isTelegramUpdateAdmissionString(value.receiptId) ||
|
|
779
|
+
!Array.isArray(value.sourceUpdateIds) ||
|
|
780
|
+
value.sourceUpdateIds.length === 0 ||
|
|
781
|
+
!value.sourceUpdateIds.every((updateId) => Number.isSafeInteger(updateId) &&
|
|
782
|
+
updateId >= 0 &&
|
|
783
|
+
claimableUpdateIds.has(updateId))) {
|
|
784
|
+
throw new TelegramUpdateAdmissionOutcomeError(`Telegram update ${currentUpdateId} returned an invalid queue receipt.`);
|
|
785
|
+
}
|
|
786
|
+
const sourceUpdateIds = [...new Set(value.sourceUpdateIds)];
|
|
787
|
+
if (sourceUpdateIds.length !== value.sourceUpdateIds.length ||
|
|
788
|
+
!sourceUpdateIds.includes(currentUpdateId)) {
|
|
789
|
+
throw new TelegramUpdateAdmissionOutcomeError(`Telegram update ${currentUpdateId} returned a mismatched queue receipt.`);
|
|
790
|
+
}
|
|
791
|
+
return {
|
|
792
|
+
kind: "queued",
|
|
793
|
+
queueKind: value.queueKind,
|
|
794
|
+
receiptId: value.receiptId,
|
|
795
|
+
sourceUpdateIds,
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
throw new TelegramUpdateAdmissionOutcomeError(`Telegram update ${currentUpdateId} returned an unknown admission outcome.`);
|
|
799
|
+
}
|
|
800
|
+
function normalizeTelegramUpdateRetryPolicy(input) {
|
|
801
|
+
const policy = {
|
|
802
|
+
baseDelayMs: input?.baseDelayMs ?? TELEGRAM_UPDATE_RETRY_BASE_DELAY_MS,
|
|
803
|
+
maxDelayMs: input?.maxDelayMs ?? TELEGRAM_UPDATE_RETRY_MAX_DELAY_MS,
|
|
804
|
+
};
|
|
805
|
+
if (!Number.isSafeInteger(policy.baseDelayMs) ||
|
|
806
|
+
policy.baseDelayMs <= 0 ||
|
|
807
|
+
!Number.isSafeInteger(policy.maxDelayMs) ||
|
|
808
|
+
policy.maxDelayMs < policy.baseDelayMs) {
|
|
809
|
+
throw new Error("Telegram update retry policy is invalid.");
|
|
810
|
+
}
|
|
811
|
+
return policy;
|
|
812
|
+
}
|
|
813
|
+
function normalizeTelegramUpdateFailureClass(value) {
|
|
814
|
+
const normalized = value
|
|
815
|
+
.trim()
|
|
816
|
+
.replace(/[^A-Za-z0-9._:-]+/gu, "-")
|
|
817
|
+
.replace(/^-+|-+$/gu, "")
|
|
818
|
+
.slice(0, TELEGRAM_UPDATE_JOURNAL_FAILURE_CLASS_MAX_LENGTH);
|
|
819
|
+
return normalized || "execution-error";
|
|
820
|
+
}
|
|
821
|
+
function normalizeTelegramUpdateFailureSummary(value) {
|
|
822
|
+
const normalized = value
|
|
823
|
+
.trim()
|
|
824
|
+
.slice(0, TELEGRAM_UPDATE_JOURNAL_FAILURE_SUMMARY_MAX_LENGTH);
|
|
825
|
+
return normalized || "Telegram update execution failed.";
|
|
826
|
+
}
|
|
827
|
+
function classifyTelegramUpdateExecutionFailure(error) {
|
|
828
|
+
if (error instanceof TelegramForeignUpdateSettlementError) {
|
|
829
|
+
return {
|
|
830
|
+
disposition: error.settlement.status === "retryable" ? "retryable" : "terminal",
|
|
831
|
+
failureClass: normalizeTelegramUpdateFailureClass(error.settlement.failureClass),
|
|
832
|
+
summary: normalizeTelegramUpdateFailureSummary(error.settlement.message),
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
const errorName = error instanceof Error && error.name ? error.name : "UnknownError";
|
|
836
|
+
return {
|
|
837
|
+
disposition: "retryable",
|
|
838
|
+
failureClass: normalizeTelegramUpdateFailureClass(`execution-${errorName}`),
|
|
839
|
+
summary: normalizeTelegramUpdateFailureSummary(`${errorName}: Telegram update execution failed.`),
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
function getTelegramUpdateRetryDelayMs(attemptCount, policy) {
|
|
843
|
+
const multiplier = 2 ** Math.max(0, attemptCount - 1);
|
|
844
|
+
return Math.min(policy.maxDelayMs, policy.baseDelayMs * multiplier);
|
|
845
|
+
}
|
|
846
|
+
function scheduleTelegramUpdateRetry(callback, delayMs) {
|
|
847
|
+
const handle = setTimeout(callback, delayMs);
|
|
848
|
+
handle.unref?.();
|
|
849
|
+
return handle;
|
|
850
|
+
}
|
|
851
|
+
function normalizeTelegramUpdateQueueOwnerIdentity(value) {
|
|
852
|
+
if (typeof value.instanceId !== "string" ||
|
|
853
|
+
value.instanceId.length === 0 ||
|
|
854
|
+
value.instanceId.length >
|
|
855
|
+
TELEGRAM_UPDATE_JOURNAL_QUEUE_OWNER_ID_MAX_LENGTH ||
|
|
856
|
+
!Number.isSafeInteger(value.processId) ||
|
|
857
|
+
value.processId <= 0 ||
|
|
858
|
+
typeof value.processBirthId !== "string" ||
|
|
859
|
+
value.processBirthId.length === 0 ||
|
|
860
|
+
value.processBirthId.length >
|
|
861
|
+
TELEGRAM_UPDATE_JOURNAL_QUEUE_OWNER_ID_MAX_LENGTH ||
|
|
862
|
+
!Number.isSafeInteger(value.sessionGeneration) ||
|
|
863
|
+
value.sessionGeneration <= 0) {
|
|
864
|
+
throw new Error("Telegram update queue owner identity is invalid.");
|
|
865
|
+
}
|
|
866
|
+
return { ...value };
|
|
867
|
+
}
|
|
868
|
+
export function createTelegramUpdateWorkerRuntime(deps) {
|
|
869
|
+
if ((deps.scheduleRetry === undefined) !==
|
|
870
|
+
(deps.cancelRetry === undefined)) {
|
|
871
|
+
throw new Error("Telegram update retry scheduling requires matching schedule and cancel ports.");
|
|
872
|
+
}
|
|
873
|
+
const getNowMs = deps.getNowMs ?? Date.now;
|
|
874
|
+
const batchSize = deps.batchSize ?? TELEGRAM_UPDATE_WORKER_BATCH_SIZE;
|
|
875
|
+
if (!Number.isSafeInteger(batchSize) || batchSize <= 0) {
|
|
876
|
+
throw new Error("Telegram update worker batch size must be positive.");
|
|
877
|
+
}
|
|
878
|
+
const yieldToEventLoop = deps.yieldToEventLoop ??
|
|
879
|
+
(() => new Promise((resolve) => setTimeout(resolve, 0)));
|
|
880
|
+
const fallbackQueueOwnerInstanceId = `worker-${randomUUID()}`;
|
|
881
|
+
const fallbackQueueOwnerProcessId = process.pid > 0 ? process.pid : 1;
|
|
882
|
+
const createAbortController = deps.createAbortController ?? (() => new AbortController());
|
|
883
|
+
const retryPolicy = normalizeTelegramUpdateRetryPolicy(deps.retryPolicy);
|
|
884
|
+
const scheduleRetry = deps.scheduleRetry ?? scheduleTelegramUpdateRetry;
|
|
885
|
+
const cancelRetry = deps.cancelRetry ??
|
|
886
|
+
((handle) => clearTimeout(handle));
|
|
887
|
+
const resolveQueueOwnerIdentity = (ctx, generation) => normalizeTelegramUpdateQueueOwnerIdentity(deps.getQueueOwnerIdentity?.(ctx) ?? {
|
|
888
|
+
instanceId: fallbackQueueOwnerInstanceId,
|
|
889
|
+
processId: fallbackQueueOwnerProcessId,
|
|
890
|
+
processBirthId: `${fallbackQueueOwnerProcessId}:${fallbackQueueOwnerInstanceId}`,
|
|
891
|
+
sessionGeneration: generation,
|
|
892
|
+
});
|
|
893
|
+
const state = {
|
|
894
|
+
phase: "stopped",
|
|
895
|
+
generation: 0,
|
|
896
|
+
journalEntryCount: 0,
|
|
897
|
+
journalSerializedBytes: 0,
|
|
898
|
+
deferredClaimCount: 0,
|
|
899
|
+
queuedClaimCount: 0,
|
|
900
|
+
foreignQueuedCount: 0,
|
|
901
|
+
retryWaitCount: 0,
|
|
902
|
+
failedCount: 0,
|
|
903
|
+
unsettledExecutionCount: 0,
|
|
904
|
+
};
|
|
905
|
+
const claims = new Map();
|
|
906
|
+
const unsettledExecutionsByUpdateId = new Map();
|
|
907
|
+
const committedQueueReceipts = new Map();
|
|
908
|
+
const unsettledExecutions = new Set();
|
|
909
|
+
let owner;
|
|
910
|
+
let drainPromise;
|
|
911
|
+
let pendingSignal = false;
|
|
912
|
+
let blocked = false;
|
|
913
|
+
let nextGeneration = 0;
|
|
914
|
+
let retryTimer;
|
|
915
|
+
let retryTimerAtMs;
|
|
916
|
+
let retryTimerToken;
|
|
917
|
+
let launchDrain = () => { };
|
|
918
|
+
const recordRuntimeEvent = (error, details) => {
|
|
919
|
+
try {
|
|
920
|
+
deps.recordRuntimeEvent?.("inbound-worker", error, details);
|
|
921
|
+
}
|
|
922
|
+
catch {
|
|
923
|
+
// Diagnostics cannot own or terminate worker progress.
|
|
924
|
+
}
|
|
925
|
+
};
|
|
926
|
+
const notifyStateChange = () => {
|
|
927
|
+
try {
|
|
928
|
+
deps.onStateChange?.(getTelegramUpdateWorkerStateSnapshot(state));
|
|
929
|
+
}
|
|
930
|
+
catch (error) {
|
|
931
|
+
recordRuntimeEvent(error, { phase: "state-observer" });
|
|
932
|
+
}
|
|
933
|
+
};
|
|
934
|
+
const updateClaimCounts = () => {
|
|
935
|
+
let deferredClaimCount = 0;
|
|
936
|
+
let queuedClaimCount = 0;
|
|
937
|
+
for (const claim of claims.values()) {
|
|
938
|
+
if (claim === "queued")
|
|
939
|
+
queuedClaimCount += 1;
|
|
940
|
+
else
|
|
941
|
+
deferredClaimCount += 1;
|
|
942
|
+
}
|
|
943
|
+
state.deferredClaimCount = deferredClaimCount;
|
|
944
|
+
state.queuedClaimCount = queuedClaimCount;
|
|
945
|
+
};
|
|
946
|
+
const releaseDeferredClaims = (updateIds) => {
|
|
947
|
+
let changed = false;
|
|
948
|
+
for (const updateId of updateIds) {
|
|
949
|
+
if (claims.get(updateId) !== "deferred")
|
|
950
|
+
continue;
|
|
951
|
+
claims.delete(updateId);
|
|
952
|
+
changed = true;
|
|
953
|
+
}
|
|
954
|
+
if (!changed)
|
|
955
|
+
return;
|
|
956
|
+
updateClaimCounts();
|
|
957
|
+
notifyStateChange();
|
|
958
|
+
};
|
|
959
|
+
const transition = (phase, currentUpdateId, blockedReason) => {
|
|
960
|
+
state.phase = phase;
|
|
961
|
+
state.phaseStartedAtMs = getNowMs();
|
|
962
|
+
state.currentUpdateId = currentUpdateId;
|
|
963
|
+
state.blockedReason = phase === "blocked" ? blockedReason : undefined;
|
|
964
|
+
state.unsettledExecutionCount = unsettledExecutions.size;
|
|
965
|
+
updateClaimCounts();
|
|
966
|
+
notifyStateChange();
|
|
967
|
+
};
|
|
968
|
+
const blockWithFailure = (blockedReason, failurePhase, error, currentUpdateId, extraDetails) => {
|
|
969
|
+
blocked = true;
|
|
970
|
+
state.lastFailureAtMs = getNowMs();
|
|
971
|
+
state.lastFailurePhase = failurePhase;
|
|
972
|
+
recordRuntimeEvent(error, {
|
|
973
|
+
phase: failurePhase,
|
|
974
|
+
generation: owner?.generation,
|
|
975
|
+
...(currentUpdateId !== undefined ? { updateId: currentUpdateId } : {}),
|
|
976
|
+
...extraDetails,
|
|
977
|
+
});
|
|
978
|
+
transition("blocked", currentUpdateId, blockedReason);
|
|
979
|
+
return "blocked";
|
|
980
|
+
};
|
|
981
|
+
const normalizeQueueReceipt = (receipt) => ({
|
|
982
|
+
queueKind: receipt.queueKind,
|
|
983
|
+
receiptId: receipt.receiptId,
|
|
984
|
+
sourceUpdateIds: [...receipt.sourceUpdateIds].sort((left, right) => left - right),
|
|
985
|
+
...(receipt.journalBindingKey
|
|
986
|
+
? { journalBindingKey: receipt.journalBindingKey }
|
|
987
|
+
: {}),
|
|
988
|
+
});
|
|
989
|
+
const bindQueueReceiptToJournal = (receipt) => ({
|
|
990
|
+
...normalizeQueueReceipt(receipt),
|
|
991
|
+
...(deps.getJournalBindingKey?.()
|
|
992
|
+
? { journalBindingKey: deps.getJournalBindingKey() }
|
|
993
|
+
: {}),
|
|
994
|
+
});
|
|
995
|
+
const publishCommittedQueueReceipt = (receipt, queueOwner, ctx) => {
|
|
996
|
+
const normalized = bindQueueReceiptToJournal(receipt);
|
|
997
|
+
const existing = committedQueueReceipts.get(receipt.receiptId);
|
|
998
|
+
if (existing) {
|
|
999
|
+
if (!areTelegramQueueAdmissionReceiptsEqual(existing.receipt, normalized) ||
|
|
1000
|
+
!areTelegramUpdateJournalQueueOwnersEqual(existing.queueOwner, queueOwner)) {
|
|
1001
|
+
throw new TelegramUpdateAdmissionOutcomeError(`Telegram queue receipt ${receipt.receiptId} has conflicting committed authority.`);
|
|
1002
|
+
}
|
|
1003
|
+
return false;
|
|
1004
|
+
}
|
|
1005
|
+
committedQueueReceipts.set(receipt.receiptId, {
|
|
1006
|
+
receipt: normalized,
|
|
1007
|
+
queueOwner: { ...queueOwner },
|
|
1008
|
+
});
|
|
1009
|
+
try {
|
|
1010
|
+
deps.onQueueReceiptCommitted?.(normalized, ctx);
|
|
1011
|
+
}
|
|
1012
|
+
catch (error) {
|
|
1013
|
+
recordRuntimeEvent(error, {
|
|
1014
|
+
phase: "queue-receipt-observer",
|
|
1015
|
+
receiptId: normalized.receiptId,
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
return true;
|
|
1019
|
+
};
|
|
1020
|
+
const clearRetryTimer = () => {
|
|
1021
|
+
if (retryTimer !== undefined)
|
|
1022
|
+
cancelRetry(retryTimer);
|
|
1023
|
+
retryTimer = undefined;
|
|
1024
|
+
retryTimerAtMs = undefined;
|
|
1025
|
+
retryTimerToken = undefined;
|
|
1026
|
+
};
|
|
1027
|
+
const scheduleNextRetry = (nextRetryAtMs, expectedOwner) => {
|
|
1028
|
+
if (nextRetryAtMs === undefined) {
|
|
1029
|
+
clearRetryTimer();
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
if (retryTimer !== undefined && retryTimerAtMs === nextRetryAtMs) {
|
|
1033
|
+
return;
|
|
1034
|
+
}
|
|
1035
|
+
clearRetryTimer();
|
|
1036
|
+
const token = {};
|
|
1037
|
+
retryTimerAtMs = nextRetryAtMs;
|
|
1038
|
+
retryTimerToken = token;
|
|
1039
|
+
retryTimer = scheduleRetry(() => {
|
|
1040
|
+
if (retryTimerToken !== token)
|
|
1041
|
+
return;
|
|
1042
|
+
retryTimer = undefined;
|
|
1043
|
+
retryTimerAtMs = undefined;
|
|
1044
|
+
retryTimerToken = undefined;
|
|
1045
|
+
if (owner !== expectedOwner ||
|
|
1046
|
+
expectedOwner.controller.signal.aborted) {
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
pendingSignal = true;
|
|
1050
|
+
launchDrain();
|
|
1051
|
+
}, Math.max(0, nextRetryAtMs - getNowMs()));
|
|
1052
|
+
};
|
|
1053
|
+
const refreshJournalState = (snapshot, expectedOwner) => {
|
|
1054
|
+
// Validate the whole snapshot before reconstructing any queue authority.
|
|
1055
|
+
if ((snapshot.version !== TELEGRAM_UPDATE_JOURNAL_VERSION &&
|
|
1056
|
+
snapshot.version !== TELEGRAM_UPDATE_JOURNAL_EXCLUSION_VERSION &&
|
|
1057
|
+
snapshot.version !== TELEGRAM_UPDATE_JOURNAL_CUSTODY_VERSION) ||
|
|
1058
|
+
(snapshot.version === TELEGRAM_UPDATE_JOURNAL_CUSTODY_VERSION &&
|
|
1059
|
+
!deps.executeCustodiedUpdate) ||
|
|
1060
|
+
snapshot.entries.some((entry) => ((snapshot.version === TELEGRAM_UPDATE_JOURNAL_EXCLUSION_VERSION || entry.preApprovalExcluded !== undefined) &&
|
|
1061
|
+
typeof entry.preApprovalExcluded !== "boolean") ||
|
|
1062
|
+
(entry.preApprovalExcluded === true && entry.state === "queued"))) {
|
|
1063
|
+
throw new TelegramUpdateAdmissionOutcomeError("Telegram journal snapshot has invalid pairing exclusion evidence.");
|
|
1064
|
+
}
|
|
1065
|
+
const availableUpdateIds = new Set();
|
|
1066
|
+
const queuedReceiptEntries = new Map();
|
|
1067
|
+
const locallyOwnedQueuedUpdateIds = new Set();
|
|
1068
|
+
let foreignQueuedCount = 0;
|
|
1069
|
+
let foreignQueuedOwner;
|
|
1070
|
+
let oldestAdmittedAtMs;
|
|
1071
|
+
let retryWaitCount = 0;
|
|
1072
|
+
let scheduledRetryAtMs;
|
|
1073
|
+
let nextRetry;
|
|
1074
|
+
let failedCount = 0;
|
|
1075
|
+
let latestFailure;
|
|
1076
|
+
for (const entry of snapshot.entries) {
|
|
1077
|
+
availableUpdateIds.add(entry.updateId);
|
|
1078
|
+
oldestAdmittedAtMs =
|
|
1079
|
+
oldestAdmittedAtMs === undefined
|
|
1080
|
+
? entry.admittedAtMs
|
|
1081
|
+
: Math.min(oldestAdmittedAtMs, entry.admittedAtMs);
|
|
1082
|
+
if (entry.state === "retry-wait" && entry.nextRetryAtMs !== undefined) {
|
|
1083
|
+
scheduledRetryAtMs =
|
|
1084
|
+
scheduledRetryAtMs === undefined
|
|
1085
|
+
? entry.nextRetryAtMs
|
|
1086
|
+
: Math.min(scheduledRetryAtMs, entry.nextRetryAtMs);
|
|
1087
|
+
}
|
|
1088
|
+
if (entry.state === "retry-wait" &&
|
|
1089
|
+
entry.failure !== undefined &&
|
|
1090
|
+
entry.nextRetryAtMs !== undefined) {
|
|
1091
|
+
retryWaitCount += 1;
|
|
1092
|
+
if (!nextRetry ||
|
|
1093
|
+
nextRetry.nextRetryAtMs === undefined ||
|
|
1094
|
+
entry.nextRetryAtMs < nextRetry.nextRetryAtMs ||
|
|
1095
|
+
(entry.nextRetryAtMs === nextRetry.nextRetryAtMs &&
|
|
1096
|
+
entry.updateId < nextRetry.updateId)) {
|
|
1097
|
+
nextRetry = entry;
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
if (entry.state === "failed" &&
|
|
1101
|
+
entry.failure !== undefined &&
|
|
1102
|
+
entry.terminalAtMs !== undefined) {
|
|
1103
|
+
failedCount += 1;
|
|
1104
|
+
if (!latestFailure ||
|
|
1105
|
+
latestFailure.terminalAtMs === undefined ||
|
|
1106
|
+
entry.terminalAtMs > latestFailure.terminalAtMs ||
|
|
1107
|
+
(entry.terminalAtMs === latestFailure.terminalAtMs &&
|
|
1108
|
+
entry.updateId > latestFailure.updateId)) {
|
|
1109
|
+
latestFailure = entry;
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
if (entry.state !== "queued")
|
|
1113
|
+
continue;
|
|
1114
|
+
if (!entry.queueKind || !entry.queueReceiptId) {
|
|
1115
|
+
throw new TelegramUpdateAdmissionOutcomeError(`Telegram queued update ${entry.updateId} has no receipt metadata.`);
|
|
1116
|
+
}
|
|
1117
|
+
if (entry.queueHandoff ||
|
|
1118
|
+
!entry.queueOwner ||
|
|
1119
|
+
!isTelegramUpdateJournalQueueOwnerProcess(entry.queueOwner, expectedOwner.queueOwnerIdentity)) {
|
|
1120
|
+
foreignQueuedCount += 1;
|
|
1121
|
+
foreignQueuedOwner ??= entry.queueOwner;
|
|
1122
|
+
continue;
|
|
1123
|
+
}
|
|
1124
|
+
locallyOwnedQueuedUpdateIds.add(entry.updateId);
|
|
1125
|
+
claims.set(entry.updateId, "queued");
|
|
1126
|
+
const receipt = queuedReceiptEntries.get(entry.queueReceiptId);
|
|
1127
|
+
if (receipt &&
|
|
1128
|
+
(receipt.queueKind !== entry.queueKind ||
|
|
1129
|
+
!areTelegramUpdateJournalQueueOwnersEqual(receipt.queueOwner, entry.queueOwner))) {
|
|
1130
|
+
throw new TelegramUpdateAdmissionOutcomeError(`Telegram queue receipt ${entry.queueReceiptId} has conflicting authority.`);
|
|
1131
|
+
}
|
|
1132
|
+
if (receipt)
|
|
1133
|
+
receipt.sourceUpdateIds.push(entry.updateId);
|
|
1134
|
+
else {
|
|
1135
|
+
queuedReceiptEntries.set(entry.queueReceiptId, {
|
|
1136
|
+
queueKind: entry.queueKind,
|
|
1137
|
+
sourceUpdateIds: [entry.updateId],
|
|
1138
|
+
queueOwner: { ...entry.queueOwner },
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
for (const [updateId, claim] of claims) {
|
|
1143
|
+
if (!availableUpdateIds.has(updateId) ||
|
|
1144
|
+
(claim === "queued" && !locallyOwnedQueuedUpdateIds.has(updateId))) {
|
|
1145
|
+
claims.delete(updateId);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
for (const [receiptId, receipt] of queuedReceiptEntries) {
|
|
1149
|
+
publishCommittedQueueReceipt({
|
|
1150
|
+
queueKind: receipt.queueKind,
|
|
1151
|
+
receiptId,
|
|
1152
|
+
sourceUpdateIds: receipt.sourceUpdateIds,
|
|
1153
|
+
}, receipt.queueOwner, expectedOwner.ctx);
|
|
1154
|
+
}
|
|
1155
|
+
for (const [receiptId] of committedQueueReceipts) {
|
|
1156
|
+
if (!queuedReceiptEntries.has(receiptId)) {
|
|
1157
|
+
committedQueueReceipts.delete(receiptId);
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
state.foreignQueuedCount = foreignQueuedCount;
|
|
1161
|
+
if (foreignQueuedOwner)
|
|
1162
|
+
state.foreignQueuedOwner = foreignQueuedOwner;
|
|
1163
|
+
else
|
|
1164
|
+
delete state.foreignQueuedOwner;
|
|
1165
|
+
state.journalEntryCount = snapshot.entries.length;
|
|
1166
|
+
state.journalSerializedBytes = snapshot.serializedBytes;
|
|
1167
|
+
state.oldestAdmittedAtMs = oldestAdmittedAtMs;
|
|
1168
|
+
state.retryWaitCount = retryWaitCount;
|
|
1169
|
+
state.nextRetryUpdateId = nextRetry?.updateId;
|
|
1170
|
+
state.nextRetryAtMs = nextRetry?.nextRetryAtMs;
|
|
1171
|
+
state.nextRetryAttemptCount = nextRetry?.failure?.attemptCount;
|
|
1172
|
+
state.nextRetryFailureClass = nextRetry?.failure?.failureClass;
|
|
1173
|
+
state.failedCount = failedCount;
|
|
1174
|
+
state.failedUpdateId = latestFailure?.updateId;
|
|
1175
|
+
state.failedFailureId = latestFailure?.terminalFailureId;
|
|
1176
|
+
state.failedAttemptCount = latestFailure?.failure?.attemptCount;
|
|
1177
|
+
state.failedClass = latestFailure?.failure?.failureClass;
|
|
1178
|
+
state.failedSummary = latestFailure?.failure?.summary;
|
|
1179
|
+
state.terminalFailureAtMs = latestFailure?.terminalAtMs;
|
|
1180
|
+
state.unsettledExecutionCount = unsettledExecutions.size;
|
|
1181
|
+
updateClaimCounts();
|
|
1182
|
+
return scheduledRetryAtMs;
|
|
1183
|
+
};
|
|
1184
|
+
const checkAuthority = (expectedOwner, currentUpdateId) => {
|
|
1185
|
+
if (blocked)
|
|
1186
|
+
return "blocked";
|
|
1187
|
+
if (owner !== expectedOwner || expectedOwner.controller.signal.aborted) {
|
|
1188
|
+
return "aborted";
|
|
1189
|
+
}
|
|
1190
|
+
try {
|
|
1191
|
+
if (deps.hasAuthority(expectedOwner.ctx))
|
|
1192
|
+
return undefined;
|
|
1193
|
+
}
|
|
1194
|
+
catch (error) {
|
|
1195
|
+
return blockWithFailure("authority-check", "authority-check", error, currentUpdateId);
|
|
1196
|
+
}
|
|
1197
|
+
transition("blocked", currentUpdateId, "authority-lost");
|
|
1198
|
+
return "blocked";
|
|
1199
|
+
};
|
|
1200
|
+
const executeWithinOwner = async (expectedOwner, update) => {
|
|
1201
|
+
if (expectedOwner.controller.signal.aborted) {
|
|
1202
|
+
return TELEGRAM_UPDATE_WORKER_EXECUTION_ABORTED;
|
|
1203
|
+
}
|
|
1204
|
+
const custodied = deps.executeCustodiedUpdate !== undefined;
|
|
1205
|
+
const execution = Promise.resolve().then(async () => deps.executeCustodiedUpdate
|
|
1206
|
+
? deps.executeCustodiedUpdate(update, expectedOwner.ctx, expectedOwner.controller.signal)
|
|
1207
|
+
: deps.executeUpdate(update, expectedOwner.ctx, expectedOwner.controller.signal));
|
|
1208
|
+
const settlement = execution.then((outcome) => custodied
|
|
1209
|
+
? ({ ok: true, outcome: outcome, custodied: true })
|
|
1210
|
+
: ({ ok: true, outcome: outcome }), (error) => ({ ok: false, error }));
|
|
1211
|
+
unsettledExecutions.add(settlement);
|
|
1212
|
+
const updateExecutions = unsettledExecutionsByUpdateId.get(update.update_id) ?? new Set();
|
|
1213
|
+
updateExecutions.add(settlement);
|
|
1214
|
+
unsettledExecutionsByUpdateId.set(update.update_id, updateExecutions);
|
|
1215
|
+
state.unsettledExecutionCount = unsettledExecutions.size;
|
|
1216
|
+
notifyStateChange();
|
|
1217
|
+
void settlement.then((result) => {
|
|
1218
|
+
unsettledExecutions.delete(settlement);
|
|
1219
|
+
const currentExecutions = unsettledExecutionsByUpdateId.get(update.update_id);
|
|
1220
|
+
currentExecutions?.delete(settlement);
|
|
1221
|
+
if (currentExecutions?.size === 0) {
|
|
1222
|
+
unsettledExecutionsByUpdateId.delete(update.update_id);
|
|
1223
|
+
}
|
|
1224
|
+
state.unsettledExecutionCount = unsettledExecutions.size;
|
|
1225
|
+
if (owner !== expectedOwner || expectedOwner.controller.signal.aborted) {
|
|
1226
|
+
recordRuntimeEvent(result.ok
|
|
1227
|
+
? "Superseded Telegram update execution settled successfully."
|
|
1228
|
+
: result.error, {
|
|
1229
|
+
phase: result.ok ? "late-execution-success" : "late-execution",
|
|
1230
|
+
generation: expectedOwner.generation,
|
|
1231
|
+
updateId: update.update_id,
|
|
1232
|
+
});
|
|
1233
|
+
}
|
|
1234
|
+
notifyStateChange();
|
|
1235
|
+
});
|
|
1236
|
+
let removeAbortListener = () => { };
|
|
1237
|
+
const aborted = new Promise((resolve) => {
|
|
1238
|
+
const onAbort = () => resolve(TELEGRAM_UPDATE_WORKER_EXECUTION_ABORTED);
|
|
1239
|
+
removeAbortListener = () => expectedOwner.controller.signal.removeEventListener("abort", onAbort);
|
|
1240
|
+
expectedOwner.controller.signal.addEventListener("abort", onAbort, {
|
|
1241
|
+
once: true,
|
|
1242
|
+
});
|
|
1243
|
+
if (expectedOwner.controller.signal.aborted)
|
|
1244
|
+
onAbort();
|
|
1245
|
+
});
|
|
1246
|
+
try {
|
|
1247
|
+
return await Promise.race([settlement, aborted]);
|
|
1248
|
+
}
|
|
1249
|
+
finally {
|
|
1250
|
+
removeAbortListener();
|
|
1251
|
+
}
|
|
1252
|
+
};
|
|
1253
|
+
const commitQueuedOutcome = (expectedOwner, currentUpdateId, outcome) => {
|
|
1254
|
+
const normalized = normalizeQueueReceipt(outcome);
|
|
1255
|
+
const existing = committedQueueReceipts.get(normalized.receiptId);
|
|
1256
|
+
if (existing) {
|
|
1257
|
+
if (!areTelegramQueueAdmissionReceiptsEqual(existing.receipt, normalized) ||
|
|
1258
|
+
!isTelegramUpdateJournalQueueOwnerProcess(existing.queueOwner, expectedOwner.queueOwnerIdentity)) {
|
|
1259
|
+
return blockWithFailure("invalid-outcome", "queue-receipt-conflict", new TelegramUpdateAdmissionOutcomeError(`Telegram queue receipt ${normalized.receiptId} conflicts with committed authority.`), currentUpdateId, {
|
|
1260
|
+
receiptId: normalized.receiptId,
|
|
1261
|
+
sourceUpdateIds: normalized.sourceUpdateIds,
|
|
1262
|
+
});
|
|
1263
|
+
}
|
|
1264
|
+
return "duplicate";
|
|
1265
|
+
}
|
|
1266
|
+
const commitAuthority = checkAuthority(expectedOwner, currentUpdateId);
|
|
1267
|
+
if (commitAuthority)
|
|
1268
|
+
return commitAuthority;
|
|
1269
|
+
let queueOwner;
|
|
1270
|
+
try {
|
|
1271
|
+
const committed = deps.journal.markQueued({
|
|
1272
|
+
...normalized,
|
|
1273
|
+
owner: expectedOwner.queueOwnerIdentity,
|
|
1274
|
+
});
|
|
1275
|
+
const committedUpdateIds = new Set([
|
|
1276
|
+
...committed.queuedUpdateIds,
|
|
1277
|
+
...committed.duplicateUpdateIds,
|
|
1278
|
+
]);
|
|
1279
|
+
if (normalized.sourceUpdateIds.some((updateId) => !committedUpdateIds.has(updateId))) {
|
|
1280
|
+
throw new Error(`Telegram queue receipt ${normalized.receiptId} did not commit every source update.`);
|
|
1281
|
+
}
|
|
1282
|
+
if (!committed.queueOwner ||
|
|
1283
|
+
!isTelegramUpdateJournalQueueOwnerProcess(committed.queueOwner, expectedOwner.queueOwnerIdentity)) {
|
|
1284
|
+
throw new Error(`Telegram queue receipt ${normalized.receiptId} belongs to another live process.`);
|
|
1285
|
+
}
|
|
1286
|
+
queueOwner = committed.queueOwner;
|
|
1287
|
+
}
|
|
1288
|
+
catch (error) {
|
|
1289
|
+
return blockWithFailure("journal-write", "queue-receipt-commit", error, currentUpdateId);
|
|
1290
|
+
}
|
|
1291
|
+
for (const sourceUpdateId of normalized.sourceUpdateIds) {
|
|
1292
|
+
claims.set(sourceUpdateId, "queued");
|
|
1293
|
+
}
|
|
1294
|
+
try {
|
|
1295
|
+
publishCommittedQueueReceipt(normalized, queueOwner, expectedOwner.ctx);
|
|
1296
|
+
}
|
|
1297
|
+
catch (error) {
|
|
1298
|
+
return blockWithFailure("invalid-outcome", "queue-receipt-publish", error, currentUpdateId);
|
|
1299
|
+
}
|
|
1300
|
+
return "committed";
|
|
1301
|
+
};
|
|
1302
|
+
const persistExecutionFailure = (expectedOwner, entry, error) => {
|
|
1303
|
+
const authorityResult = checkAuthority(expectedOwner, entry.updateId);
|
|
1304
|
+
if (authorityResult)
|
|
1305
|
+
return authorityResult;
|
|
1306
|
+
let rawClassification;
|
|
1307
|
+
try {
|
|
1308
|
+
rawClassification = deps.classifyExecutionFailure
|
|
1309
|
+
? deps.classifyExecutionFailure(error)
|
|
1310
|
+
: classifyTelegramUpdateExecutionFailure(error);
|
|
1311
|
+
if ((rawClassification.disposition !== "retryable" &&
|
|
1312
|
+
rawClassification.disposition !== "terminal") ||
|
|
1313
|
+
typeof rawClassification.failureClass !== "string" ||
|
|
1314
|
+
typeof rawClassification.summary !== "string") {
|
|
1315
|
+
throw new Error("Telegram update failure classifier returned invalid data.");
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
catch (classificationError) {
|
|
1319
|
+
return blockWithFailure("execution", "failure-classification", classificationError, entry.updateId);
|
|
1320
|
+
}
|
|
1321
|
+
const failureClass = normalizeTelegramUpdateFailureClass(rawClassification.failureClass);
|
|
1322
|
+
const summary = normalizeTelegramUpdateFailureSummary(rawClassification.summary);
|
|
1323
|
+
const expectedAttemptCount = entry.failure?.attemptCount ?? 0;
|
|
1324
|
+
const attemptCount = expectedAttemptCount + 1;
|
|
1325
|
+
const failedAtMs = getNowMs();
|
|
1326
|
+
const disposition = "retry-wait";
|
|
1327
|
+
const nextRetryAtMs = failedAtMs + getTelegramUpdateRetryDelayMs(attemptCount, retryPolicy);
|
|
1328
|
+
try {
|
|
1329
|
+
const result = deps.journal.markExecutionFailure({
|
|
1330
|
+
updateId: entry.updateId,
|
|
1331
|
+
expectedAttemptCount,
|
|
1332
|
+
failedAtMs,
|
|
1333
|
+
failureClass,
|
|
1334
|
+
summary,
|
|
1335
|
+
disposition,
|
|
1336
|
+
nextRetryAtMs,
|
|
1337
|
+
});
|
|
1338
|
+
if (result.entry.state !== disposition) {
|
|
1339
|
+
throw new Error(`Telegram update ${entry.updateId} failure disposition did not persist.`);
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
catch (journalError) {
|
|
1343
|
+
return blockWithFailure("journal-write", "execution-failure-commit", journalError, entry.updateId);
|
|
1344
|
+
}
|
|
1345
|
+
state.lastFailureAtMs = failedAtMs;
|
|
1346
|
+
state.lastFailurePhase = "execute";
|
|
1347
|
+
recordRuntimeEvent(error, {
|
|
1348
|
+
phase: "execute",
|
|
1349
|
+
generation: expectedOwner.generation,
|
|
1350
|
+
updateId: entry.updateId,
|
|
1351
|
+
failureClass,
|
|
1352
|
+
attemptCount,
|
|
1353
|
+
disposition,
|
|
1354
|
+
nextRetryAtMs,
|
|
1355
|
+
});
|
|
1356
|
+
transition(disposition, entry.updateId);
|
|
1357
|
+
return disposition;
|
|
1358
|
+
};
|
|
1359
|
+
const commitCompletedBatch = (expectedOwner, updateIds) => {
|
|
1360
|
+
if (updateIds.length === 0)
|
|
1361
|
+
return undefined;
|
|
1362
|
+
const commitAuthority = checkAuthority(expectedOwner, updateIds[updateIds.length - 1]);
|
|
1363
|
+
if (commitAuthority)
|
|
1364
|
+
return commitAuthority;
|
|
1365
|
+
try {
|
|
1366
|
+
const removed = deps.journal.removeCompleted(updateIds);
|
|
1367
|
+
const removedIds = new Set(removed.removedUpdateIds);
|
|
1368
|
+
if (updateIds.some((updateId) => !removedIds.has(updateId))) {
|
|
1369
|
+
throw new Error("Telegram update batch changed before completion commit.");
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
catch (error) {
|
|
1373
|
+
return blockWithFailure("journal-write", "completion-commit", error, updateIds[updateIds.length - 1]);
|
|
1374
|
+
}
|
|
1375
|
+
const completedAtMs = getNowMs();
|
|
1376
|
+
for (const updateId of updateIds) {
|
|
1377
|
+
claims.delete(updateId);
|
|
1378
|
+
state.lastCompletedUpdateId = updateId;
|
|
1379
|
+
state.lastCompletedAtMs = completedAtMs;
|
|
1380
|
+
try {
|
|
1381
|
+
deps.onUpdateCompleted?.(updateId, expectedOwner.ctx);
|
|
1382
|
+
}
|
|
1383
|
+
catch (error) {
|
|
1384
|
+
recordRuntimeEvent(error, {
|
|
1385
|
+
phase: "update-completion-observer",
|
|
1386
|
+
updateId,
|
|
1387
|
+
});
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
return undefined;
|
|
1391
|
+
};
|
|
1392
|
+
const drain = async (expectedOwner) => {
|
|
1393
|
+
while (owner === expectedOwner && !expectedOwner.controller.signal.aborted) {
|
|
1394
|
+
const authorityResult = checkAuthority(expectedOwner);
|
|
1395
|
+
if (authorityResult)
|
|
1396
|
+
return authorityResult;
|
|
1397
|
+
let snapshot;
|
|
1398
|
+
let scheduledRetryAtMs;
|
|
1399
|
+
try {
|
|
1400
|
+
snapshot = deps.journal.read();
|
|
1401
|
+
scheduledRetryAtMs = refreshJournalState(snapshot, expectedOwner);
|
|
1402
|
+
}
|
|
1403
|
+
catch (error) {
|
|
1404
|
+
return blockWithFailure("journal-read", "journal-read", error);
|
|
1405
|
+
}
|
|
1406
|
+
const nowMs = getNowMs();
|
|
1407
|
+
const entries = [];
|
|
1408
|
+
let hasMoreEntries = false;
|
|
1409
|
+
let hasOutcomeUnknownInput = false;
|
|
1410
|
+
let hasUnavailableInputCustody = false;
|
|
1411
|
+
let blockedInputCustody;
|
|
1412
|
+
delete state.blockedInputCustody;
|
|
1413
|
+
for (const candidate of snapshot.entries) {
|
|
1414
|
+
if (snapshot.version === TELEGRAM_UPDATE_JOURNAL_CUSTODY_VERSION &&
|
|
1415
|
+
(candidate.state === "retry-wait" || candidate.state === "failed")) {
|
|
1416
|
+
hasUnavailableInputCustody = true;
|
|
1417
|
+
blockedInputCustody ??= { updateId: candidate.updateId, kind: "legacy-retry-state" };
|
|
1418
|
+
continue;
|
|
1419
|
+
}
|
|
1420
|
+
if (snapshot.version === TELEGRAM_UPDATE_JOURNAL_CUSTODY_VERSION && candidate.inputClaim) {
|
|
1421
|
+
if (candidate.inputClaim.phase === "running") {
|
|
1422
|
+
hasOutcomeUnknownInput = true;
|
|
1423
|
+
if (!blockedInputCustody || blockedInputCustody.kind !== "running-outcome-unknown")
|
|
1424
|
+
blockedInputCustody = { updateId: candidate.updateId,
|
|
1425
|
+
kind: "running-outcome-unknown" };
|
|
1426
|
+
continue;
|
|
1427
|
+
}
|
|
1428
|
+
if (candidate.inputClaim.handoff || !isTelegramUpdateJournalQueueOwnerProcess(candidate.inputClaim.owner, expectedOwner.queueOwnerIdentity)) {
|
|
1429
|
+
hasUnavailableInputCustody = true;
|
|
1430
|
+
blockedInputCustody ??= { updateId: candidate.updateId,
|
|
1431
|
+
kind: candidate.inputClaim.handoff ? "handoff-frozen" : "foreign-ready" };
|
|
1432
|
+
continue;
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
if (!claims.has(candidate.updateId) &&
|
|
1436
|
+
(candidate.state === "pending" ||
|
|
1437
|
+
(candidate.state === "retry-wait" &&
|
|
1438
|
+
candidate.nextRetryAtMs !== undefined &&
|
|
1439
|
+
candidate.nextRetryAtMs <= nowMs))) {
|
|
1440
|
+
if (entries.length === batchSize) {
|
|
1441
|
+
hasMoreEntries = true;
|
|
1442
|
+
break;
|
|
1443
|
+
}
|
|
1444
|
+
entries.push(candidate);
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
if (entries.length === 0) {
|
|
1448
|
+
scheduleNextRetry(scheduledRetryAtMs, expectedOwner);
|
|
1449
|
+
if (hasOutcomeUnknownInput || hasUnavailableInputCustody) {
|
|
1450
|
+
if (blockedInputCustody)
|
|
1451
|
+
state.blockedInputCustody = blockedInputCustody;
|
|
1452
|
+
transition("blocked", blockedInputCustody?.updateId, hasOutcomeUnknownInput ? "execution" : "input-custody");
|
|
1453
|
+
return "blocked";
|
|
1454
|
+
}
|
|
1455
|
+
transition("idle");
|
|
1456
|
+
return "idle";
|
|
1457
|
+
}
|
|
1458
|
+
const completedUpdateIds = [];
|
|
1459
|
+
let snapshotInvalidated = false;
|
|
1460
|
+
for (const entry of entries) {
|
|
1461
|
+
const priorExecutions = unsettledExecutionsByUpdateId.get(entry.updateId);
|
|
1462
|
+
if (priorExecutions?.size) {
|
|
1463
|
+
const completionResult = commitCompletedBatch(expectedOwner, completedUpdateIds);
|
|
1464
|
+
if (completionResult)
|
|
1465
|
+
return completionResult;
|
|
1466
|
+
transition("blocked", entry.updateId, "prior-generation-executing");
|
|
1467
|
+
await Promise.allSettled([...priorExecutions]);
|
|
1468
|
+
if (owner !== expectedOwner ||
|
|
1469
|
+
expectedOwner.controller.signal.aborted) {
|
|
1470
|
+
return "aborted";
|
|
1471
|
+
}
|
|
1472
|
+
snapshotInvalidated = true;
|
|
1473
|
+
break;
|
|
1474
|
+
}
|
|
1475
|
+
if (entry.preApprovalExcluded === true) {
|
|
1476
|
+
completedUpdateIds.push(entry.updateId);
|
|
1477
|
+
continue;
|
|
1478
|
+
}
|
|
1479
|
+
clearRetryTimer();
|
|
1480
|
+
transition("executing", entry.updateId);
|
|
1481
|
+
const execution = await executeWithinOwner(expectedOwner, entry.update);
|
|
1482
|
+
if (execution === TELEGRAM_UPDATE_WORKER_EXECUTION_ABORTED) {
|
|
1483
|
+
return "aborted";
|
|
1484
|
+
}
|
|
1485
|
+
if (!execution.ok) {
|
|
1486
|
+
if (deps.executeCustodiedUpdate) {
|
|
1487
|
+
transition("blocked", entry.updateId, "execution");
|
|
1488
|
+
return "blocked";
|
|
1489
|
+
}
|
|
1490
|
+
const completionResult = commitCompletedBatch(expectedOwner, completedUpdateIds);
|
|
1491
|
+
if (completionResult)
|
|
1492
|
+
return completionResult;
|
|
1493
|
+
const failureResult = persistExecutionFailure(expectedOwner, entry, execution.error);
|
|
1494
|
+
if (failureResult === "blocked" || failureResult === "aborted") {
|
|
1495
|
+
return failureResult;
|
|
1496
|
+
}
|
|
1497
|
+
snapshotInvalidated = true;
|
|
1498
|
+
break;
|
|
1499
|
+
}
|
|
1500
|
+
if (execution.custodied) {
|
|
1501
|
+
const outcome = execution.outcome;
|
|
1502
|
+
if (outcome.status === "outcome-unknown") {
|
|
1503
|
+
transition("blocked", entry.updateId, "execution");
|
|
1504
|
+
return "blocked";
|
|
1505
|
+
}
|
|
1506
|
+
if (outcome.status === "deferred") {
|
|
1507
|
+
claims.set(entry.updateId, "deferred");
|
|
1508
|
+
transition("deferred", entry.updateId);
|
|
1509
|
+
continue;
|
|
1510
|
+
}
|
|
1511
|
+
if (outcome.status === "queued") {
|
|
1512
|
+
const receipt = normalizeQueueReceipt(outcome.queueReceipt);
|
|
1513
|
+
if (!isTelegramUpdateJournalQueueOwnerProcess(outcome.queueReceipt.queueOwner, expectedOwner.queueOwnerIdentity)) {
|
|
1514
|
+
return blockWithFailure("invalid-outcome", "custody-queue-owner", new Error("Telegram custodied queue receipt belongs to another process."), entry.updateId);
|
|
1515
|
+
}
|
|
1516
|
+
for (const sourceUpdateId of receipt.sourceUpdateIds)
|
|
1517
|
+
claims.set(sourceUpdateId, "queued");
|
|
1518
|
+
try {
|
|
1519
|
+
publishCommittedQueueReceipt(receipt, outcome.queueReceipt.queueOwner, expectedOwner.ctx);
|
|
1520
|
+
}
|
|
1521
|
+
catch (error) {
|
|
1522
|
+
return blockWithFailure("invalid-outcome", "queue-receipt-publish", error, entry.updateId);
|
|
1523
|
+
}
|
|
1524
|
+
transition("queued", entry.updateId);
|
|
1525
|
+
}
|
|
1526
|
+
snapshotInvalidated = true;
|
|
1527
|
+
break;
|
|
1528
|
+
}
|
|
1529
|
+
const postExecutionAuthority = checkAuthority(expectedOwner, entry.updateId);
|
|
1530
|
+
if (postExecutionAuthority)
|
|
1531
|
+
return postExecutionAuthority;
|
|
1532
|
+
const claimableUpdateIds = new Set([
|
|
1533
|
+
entry.updateId,
|
|
1534
|
+
...claims.keys(),
|
|
1535
|
+
]);
|
|
1536
|
+
let outcome;
|
|
1537
|
+
try {
|
|
1538
|
+
outcome = validateTelegramUpdateAdmissionOutcome(execution.outcome, entry.updateId, claimableUpdateIds);
|
|
1539
|
+
}
|
|
1540
|
+
catch (error) {
|
|
1541
|
+
return blockWithFailure("invalid-outcome", "invalid-outcome", error, entry.updateId);
|
|
1542
|
+
}
|
|
1543
|
+
if (outcome.kind === "deferred") {
|
|
1544
|
+
claims.set(entry.updateId, "deferred");
|
|
1545
|
+
transition("deferred", entry.updateId);
|
|
1546
|
+
continue;
|
|
1547
|
+
}
|
|
1548
|
+
if (outcome.kind === "queued") {
|
|
1549
|
+
const completionResult = commitCompletedBatch(expectedOwner, completedUpdateIds);
|
|
1550
|
+
if (completionResult)
|
|
1551
|
+
return completionResult;
|
|
1552
|
+
const queuedResult = commitQueuedOutcome(expectedOwner, entry.updateId, outcome);
|
|
1553
|
+
if (queuedResult === "blocked" || queuedResult === "aborted") {
|
|
1554
|
+
return queuedResult;
|
|
1555
|
+
}
|
|
1556
|
+
transition("queued", entry.updateId);
|
|
1557
|
+
snapshotInvalidated = true;
|
|
1558
|
+
break;
|
|
1559
|
+
}
|
|
1560
|
+
completedUpdateIds.push(entry.updateId);
|
|
1561
|
+
}
|
|
1562
|
+
const completionResult = commitCompletedBatch(expectedOwner, completedUpdateIds);
|
|
1563
|
+
if (completionResult)
|
|
1564
|
+
return completionResult;
|
|
1565
|
+
if (!snapshotInvalidated && !hasMoreEntries)
|
|
1566
|
+
continue;
|
|
1567
|
+
await yieldToEventLoop();
|
|
1568
|
+
if (owner !== expectedOwner ||
|
|
1569
|
+
expectedOwner.controller.signal.aborted) {
|
|
1570
|
+
return "aborted";
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
return "aborted";
|
|
1574
|
+
};
|
|
1575
|
+
launchDrain = () => {
|
|
1576
|
+
const expectedOwner = owner;
|
|
1577
|
+
if (!expectedOwner || drainPromise)
|
|
1578
|
+
return;
|
|
1579
|
+
const run = async () => {
|
|
1580
|
+
while (pendingSignal &&
|
|
1581
|
+
owner === expectedOwner &&
|
|
1582
|
+
!expectedOwner.controller.signal.aborted) {
|
|
1583
|
+
pendingSignal = false;
|
|
1584
|
+
const result = await drain(expectedOwner);
|
|
1585
|
+
if (result !== "idle") {
|
|
1586
|
+
pendingSignal = false;
|
|
1587
|
+
return;
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
};
|
|
1591
|
+
const operation = run();
|
|
1592
|
+
let tracked;
|
|
1593
|
+
const finish = () => {
|
|
1594
|
+
if (owner === expectedOwner && drainPromise === tracked) {
|
|
1595
|
+
drainPromise = undefined;
|
|
1596
|
+
if (pendingSignal && !expectedOwner.controller.signal.aborted) {
|
|
1597
|
+
launchDrain();
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
};
|
|
1601
|
+
tracked = operation.then(() => finish(), (error) => {
|
|
1602
|
+
pendingSignal = false;
|
|
1603
|
+
if (owner === expectedOwner) {
|
|
1604
|
+
blockWithFailure("execution", "worker-loop", error);
|
|
1605
|
+
}
|
|
1606
|
+
finish();
|
|
1607
|
+
});
|
|
1608
|
+
drainPromise = tracked;
|
|
1609
|
+
};
|
|
1610
|
+
return {
|
|
1611
|
+
start(ctx) {
|
|
1612
|
+
if (owner) {
|
|
1613
|
+
if (!owner.controller.signal.aborted) {
|
|
1614
|
+
pendingSignal = true;
|
|
1615
|
+
launchDrain();
|
|
1616
|
+
}
|
|
1617
|
+
return;
|
|
1618
|
+
}
|
|
1619
|
+
clearRetryTimer();
|
|
1620
|
+
const nowMs = getNowMs();
|
|
1621
|
+
const generation = ++nextGeneration;
|
|
1622
|
+
const queueOwnerIdentity = resolveQueueOwnerIdentity(ctx, generation);
|
|
1623
|
+
owner = {
|
|
1624
|
+
generation,
|
|
1625
|
+
ctx,
|
|
1626
|
+
controller: createAbortController(),
|
|
1627
|
+
queueOwnerIdentity,
|
|
1628
|
+
};
|
|
1629
|
+
claims.clear();
|
|
1630
|
+
committedQueueReceipts.clear();
|
|
1631
|
+
blocked = false;
|
|
1632
|
+
pendingSignal = true;
|
|
1633
|
+
state.phase = "idle";
|
|
1634
|
+
state.generation = generation;
|
|
1635
|
+
state.phaseStartedAtMs = nowMs;
|
|
1636
|
+
state.currentUpdateId = undefined;
|
|
1637
|
+
state.blockedReason = undefined;
|
|
1638
|
+
state.journalEntryCount = 0;
|
|
1639
|
+
state.journalSerializedBytes = 0;
|
|
1640
|
+
state.oldestAdmittedAtMs = undefined;
|
|
1641
|
+
state.deferredClaimCount = 0;
|
|
1642
|
+
state.queuedClaimCount = 0;
|
|
1643
|
+
state.foreignQueuedCount = 0;
|
|
1644
|
+
delete state.foreignQueuedOwner;
|
|
1645
|
+
state.retryWaitCount = 0;
|
|
1646
|
+
state.failedCount = 0;
|
|
1647
|
+
state.nextRetryUpdateId = undefined;
|
|
1648
|
+
state.nextRetryAtMs = undefined;
|
|
1649
|
+
state.nextRetryAttemptCount = undefined;
|
|
1650
|
+
state.nextRetryFailureClass = undefined;
|
|
1651
|
+
state.failedUpdateId = undefined;
|
|
1652
|
+
state.failedFailureId = undefined;
|
|
1653
|
+
state.failedAttemptCount = undefined;
|
|
1654
|
+
state.failedClass = undefined;
|
|
1655
|
+
state.failedSummary = undefined;
|
|
1656
|
+
state.terminalFailureAtMs = undefined;
|
|
1657
|
+
state.unsettledExecutionCount = unsettledExecutions.size;
|
|
1658
|
+
state.lastCompletedUpdateId = undefined;
|
|
1659
|
+
state.lastCompletedAtMs = undefined;
|
|
1660
|
+
state.lastFailureAtMs = undefined;
|
|
1661
|
+
state.lastFailurePhase = undefined;
|
|
1662
|
+
notifyStateChange();
|
|
1663
|
+
launchDrain();
|
|
1664
|
+
},
|
|
1665
|
+
signal() {
|
|
1666
|
+
if (!owner || owner.controller.signal.aborted)
|
|
1667
|
+
return;
|
|
1668
|
+
blocked = false;
|
|
1669
|
+
pendingSignal = true;
|
|
1670
|
+
launchDrain();
|
|
1671
|
+
},
|
|
1672
|
+
settleDeferred(input) {
|
|
1673
|
+
const expectedOwner = owner;
|
|
1674
|
+
if (!expectedOwner ||
|
|
1675
|
+
expectedOwner.controller.signal !== input.signal ||
|
|
1676
|
+
input.signal.aborted) {
|
|
1677
|
+
return;
|
|
1678
|
+
}
|
|
1679
|
+
const authorityResult = checkAuthority(expectedOwner, input.updateId);
|
|
1680
|
+
if (authorityResult) {
|
|
1681
|
+
if (authorityResult === "blocked") {
|
|
1682
|
+
releaseDeferredClaims(input.outcome.kind === "queued"
|
|
1683
|
+
? input.outcome.sourceUpdateIds
|
|
1684
|
+
: [input.updateId]);
|
|
1685
|
+
}
|
|
1686
|
+
return;
|
|
1687
|
+
}
|
|
1688
|
+
const claim = claims.get(input.updateId);
|
|
1689
|
+
if (input.outcome.kind === "complete") {
|
|
1690
|
+
// Expiry may retire only a still-deferred source, never accepted queue work.
|
|
1691
|
+
if (claim !== "deferred")
|
|
1692
|
+
return;
|
|
1693
|
+
const result = commitCompletedBatch(expectedOwner, [input.updateId]);
|
|
1694
|
+
if (!result)
|
|
1695
|
+
transition("idle", input.updateId);
|
|
1696
|
+
return;
|
|
1697
|
+
}
|
|
1698
|
+
if (input.outcome.kind === "deferred") {
|
|
1699
|
+
if (claim === "queued")
|
|
1700
|
+
return;
|
|
1701
|
+
if (claim !== "deferred") {
|
|
1702
|
+
blockWithFailure("invalid-outcome", "late-outcome-unclaimed", new TelegramUpdateAdmissionOutcomeError(`Telegram update ${input.updateId} reported a late deferred outcome without a live claim.`), input.updateId);
|
|
1703
|
+
return;
|
|
1704
|
+
}
|
|
1705
|
+
transition("deferred", input.updateId);
|
|
1706
|
+
return;
|
|
1707
|
+
}
|
|
1708
|
+
let outcome;
|
|
1709
|
+
try {
|
|
1710
|
+
outcome = validateTelegramUpdateAdmissionOutcome(input.outcome, input.updateId, new Set([input.updateId, ...claims.keys()]));
|
|
1711
|
+
}
|
|
1712
|
+
catch (error) {
|
|
1713
|
+
blockWithFailure("invalid-outcome", "late-invalid-outcome", error, input.updateId);
|
|
1714
|
+
releaseDeferredClaims(input.outcome.sourceUpdateIds);
|
|
1715
|
+
return;
|
|
1716
|
+
}
|
|
1717
|
+
if (outcome.kind !== "queued") {
|
|
1718
|
+
blockWithFailure("invalid-outcome", "late-invalid-outcome", new TelegramUpdateAdmissionOutcomeError(`Telegram update ${input.updateId} reported a non-queue late outcome.`), input.updateId);
|
|
1719
|
+
return;
|
|
1720
|
+
}
|
|
1721
|
+
if (claim !== "deferred" && claim !== "queued") {
|
|
1722
|
+
blockWithFailure("invalid-outcome", "late-outcome-unclaimed", new TelegramUpdateAdmissionOutcomeError(`Telegram update ${input.updateId} reported a late queue outcome without a live claim.`), input.updateId);
|
|
1723
|
+
releaseDeferredClaims(outcome.sourceUpdateIds);
|
|
1724
|
+
return;
|
|
1725
|
+
}
|
|
1726
|
+
const result = commitQueuedOutcome(expectedOwner, input.updateId, outcome);
|
|
1727
|
+
if (result === "blocked") {
|
|
1728
|
+
releaseDeferredClaims(outcome.sourceUpdateIds);
|
|
1729
|
+
return;
|
|
1730
|
+
}
|
|
1731
|
+
if (result === "aborted")
|
|
1732
|
+
return;
|
|
1733
|
+
transition("queued", input.updateId);
|
|
1734
|
+
},
|
|
1735
|
+
settleCustodied(input) {
|
|
1736
|
+
const expectedOwner = owner;
|
|
1737
|
+
if (!expectedOwner || expectedOwner.controller.signal !== input.signal || input.signal.aborted)
|
|
1738
|
+
return;
|
|
1739
|
+
if (checkAuthority(expectedOwner, input.updateId))
|
|
1740
|
+
return;
|
|
1741
|
+
const claim = claims.get(input.updateId);
|
|
1742
|
+
if (claim !== "deferred" && claim !== "queued")
|
|
1743
|
+
return;
|
|
1744
|
+
if (input.result.status === "deferred")
|
|
1745
|
+
return;
|
|
1746
|
+
if (input.result.status === "outcome-unknown") {
|
|
1747
|
+
transition("blocked", input.updateId, "execution");
|
|
1748
|
+
return;
|
|
1749
|
+
}
|
|
1750
|
+
if (input.result.status === "completed") {
|
|
1751
|
+
if (claim !== "deferred")
|
|
1752
|
+
return;
|
|
1753
|
+
claims.delete(input.updateId);
|
|
1754
|
+
transition("idle", input.updateId);
|
|
1755
|
+
pendingSignal = true;
|
|
1756
|
+
launchDrain();
|
|
1757
|
+
return;
|
|
1758
|
+
}
|
|
1759
|
+
const receipt = normalizeQueueReceipt(input.result.queueReceipt);
|
|
1760
|
+
const existing = committedQueueReceipts.get(receipt.receiptId);
|
|
1761
|
+
if (existing && areTelegramQueueAdmissionReceiptsEqual(existing.receipt, receipt) &&
|
|
1762
|
+
areTelegramUpdateJournalQueueOwnersEqual(existing.queueOwner, input.result.queueReceipt.queueOwner))
|
|
1763
|
+
return;
|
|
1764
|
+
if (existing) {
|
|
1765
|
+
blockWithFailure("invalid-outcome", "queue-receipt-conflict", new TelegramUpdateAdmissionOutcomeError(`Telegram queue receipt ${receipt.receiptId} conflicts with custodied authority.`), input.updateId);
|
|
1766
|
+
return;
|
|
1767
|
+
}
|
|
1768
|
+
if (!receipt.sourceUpdateIds.includes(input.updateId) ||
|
|
1769
|
+
receipt.sourceUpdateIds.some(updateId => claims.get(updateId) !== "deferred")) {
|
|
1770
|
+
blockWithFailure("invalid-outcome", "late-custody-unclaimed", new TelegramUpdateAdmissionOutcomeError(`Telegram update ${input.updateId} reported custodied queue without exact deferred claims.`), input.updateId);
|
|
1771
|
+
return;
|
|
1772
|
+
}
|
|
1773
|
+
if (!isTelegramUpdateJournalQueueOwnerProcess(input.result.queueReceipt.queueOwner, expectedOwner.queueOwnerIdentity)) {
|
|
1774
|
+
blockWithFailure("invalid-outcome", "custody-queue-owner", new Error("Telegram custodied queue receipt belongs to another process."), input.updateId);
|
|
1775
|
+
return;
|
|
1776
|
+
}
|
|
1777
|
+
for (const sourceUpdateId of receipt.sourceUpdateIds)
|
|
1778
|
+
claims.set(sourceUpdateId, "queued");
|
|
1779
|
+
try {
|
|
1780
|
+
publishCommittedQueueReceipt(receipt, input.result.queueReceipt.queueOwner, expectedOwner.ctx);
|
|
1781
|
+
}
|
|
1782
|
+
catch (error) {
|
|
1783
|
+
blockWithFailure("invalid-outcome", "queue-receipt-publish", error, input.updateId);
|
|
1784
|
+
return;
|
|
1785
|
+
}
|
|
1786
|
+
transition("queued", input.updateId);
|
|
1787
|
+
},
|
|
1788
|
+
isQueueReceiptCommitted(receipt) {
|
|
1789
|
+
const committed = committedQueueReceipts.get(receipt.receiptId);
|
|
1790
|
+
return (committed !== undefined &&
|
|
1791
|
+
areTelegramQueueAdmissionReceiptsEqual(committed.receipt, normalizeQueueReceipt(receipt)));
|
|
1792
|
+
},
|
|
1793
|
+
getQueueReceiptOwner(receipt) {
|
|
1794
|
+
const committed = committedQueueReceipts.get(receipt.receiptId);
|
|
1795
|
+
return committed &&
|
|
1796
|
+
areTelegramQueueAdmissionReceiptsEqual(committed.receipt, normalizeQueueReceipt(receipt))
|
|
1797
|
+
? { ...committed.queueOwner }
|
|
1798
|
+
: undefined;
|
|
1799
|
+
},
|
|
1800
|
+
completeQueueReceipts(input) {
|
|
1801
|
+
const expectedOwner = owner;
|
|
1802
|
+
if (!expectedOwner ||
|
|
1803
|
+
expectedOwner.controller.signal.aborted ||
|
|
1804
|
+
!(deps.isContextCurrent?.(input.ctx) ?? expectedOwner.ctx === input.ctx)) {
|
|
1805
|
+
return;
|
|
1806
|
+
}
|
|
1807
|
+
if (input.receipts.length === 0)
|
|
1808
|
+
return;
|
|
1809
|
+
const normalizedReceipts = input.receipts.map(normalizeQueueReceipt);
|
|
1810
|
+
const receiptIds = new Set();
|
|
1811
|
+
const sourceUpdateIds = new Set();
|
|
1812
|
+
const queuedCompletions = [];
|
|
1813
|
+
for (const receipt of normalizedReceipts) {
|
|
1814
|
+
const committed = committedQueueReceipts.get(receipt.receiptId);
|
|
1815
|
+
if (!committed)
|
|
1816
|
+
continue;
|
|
1817
|
+
if (receiptIds.has(receipt.receiptId) ||
|
|
1818
|
+
!areTelegramQueueAdmissionReceiptsEqual(committed.receipt, receipt) ||
|
|
1819
|
+
!isTelegramUpdateJournalQueueOwnerProcess(committed.queueOwner, expectedOwner.queueOwnerIdentity) ||
|
|
1820
|
+
receipt.sourceUpdateIds.some((updateId) => sourceUpdateIds.has(updateId))) {
|
|
1821
|
+
blockWithFailure("invalid-outcome", "queue-receipt-completion-invalid", new TelegramUpdateAdmissionOutcomeError(`Telegram ${input.reason} requested invalid queue receipt ${receipt.receiptId}.`));
|
|
1822
|
+
return;
|
|
1823
|
+
}
|
|
1824
|
+
receiptIds.add(receipt.receiptId);
|
|
1825
|
+
queuedCompletions.push({
|
|
1826
|
+
...receipt,
|
|
1827
|
+
queueOwner: { ...committed.queueOwner },
|
|
1828
|
+
});
|
|
1829
|
+
for (const updateId of receipt.sourceUpdateIds) {
|
|
1830
|
+
sourceUpdateIds.add(updateId);
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
if (receiptIds.size === 0)
|
|
1834
|
+
return;
|
|
1835
|
+
let removedUpdateIds;
|
|
1836
|
+
try {
|
|
1837
|
+
removedUpdateIds = deps.journal.completeQueued(queuedCompletions).removedUpdateIds;
|
|
1838
|
+
}
|
|
1839
|
+
catch (error) {
|
|
1840
|
+
blockWithFailure("journal-write", "queue-receipt-completion", error);
|
|
1841
|
+
return;
|
|
1842
|
+
}
|
|
1843
|
+
const removed = new Set(removedUpdateIds);
|
|
1844
|
+
if (removed.size !== sourceUpdateIds.size ||
|
|
1845
|
+
[...sourceUpdateIds].some((updateId) => !removed.has(updateId))) {
|
|
1846
|
+
blockWithFailure("journal-write", "queue-receipt-completion", new Error(`Telegram ${input.reason} did not complete every receipt source.`));
|
|
1847
|
+
return;
|
|
1848
|
+
}
|
|
1849
|
+
for (const updateId of sourceUpdateIds)
|
|
1850
|
+
claims.delete(updateId);
|
|
1851
|
+
for (const receiptId of receiptIds) {
|
|
1852
|
+
committedQueueReceipts.delete(receiptId);
|
|
1853
|
+
}
|
|
1854
|
+
const completedUpdateIds = [...sourceUpdateIds];
|
|
1855
|
+
state.lastCompletedUpdateId = Math.max(state.lastCompletedUpdateId ?? -1, ...completedUpdateIds);
|
|
1856
|
+
state.lastCompletedAtMs = getNowMs();
|
|
1857
|
+
state.journalEntryCount = Math.max(0, state.journalEntryCount - completedUpdateIds.length);
|
|
1858
|
+
updateClaimCounts();
|
|
1859
|
+
notifyStateChange();
|
|
1860
|
+
pendingSignal = true;
|
|
1861
|
+
launchDrain();
|
|
1862
|
+
},
|
|
1863
|
+
async stop() {
|
|
1864
|
+
const expectedOwner = owner;
|
|
1865
|
+
if (!expectedOwner)
|
|
1866
|
+
return;
|
|
1867
|
+
pendingSignal = false;
|
|
1868
|
+
clearRetryTimer();
|
|
1869
|
+
expectedOwner.controller.abort();
|
|
1870
|
+
await drainPromise?.catch(() => undefined);
|
|
1871
|
+
if (owner !== expectedOwner)
|
|
1872
|
+
return;
|
|
1873
|
+
owner = undefined;
|
|
1874
|
+
drainPromise = undefined;
|
|
1875
|
+
claims.clear();
|
|
1876
|
+
committedQueueReceipts.clear();
|
|
1877
|
+
blocked = false;
|
|
1878
|
+
state.phase = "stopped";
|
|
1879
|
+
state.phaseStartedAtMs = getNowMs();
|
|
1880
|
+
state.currentUpdateId = undefined;
|
|
1881
|
+
state.blockedReason = undefined;
|
|
1882
|
+
state.deferredClaimCount = 0;
|
|
1883
|
+
state.queuedClaimCount = 0;
|
|
1884
|
+
state.foreignQueuedCount = 0;
|
|
1885
|
+
delete state.foreignQueuedOwner;
|
|
1886
|
+
state.retryWaitCount = 0;
|
|
1887
|
+
state.failedCount = 0;
|
|
1888
|
+
state.nextRetryUpdateId = undefined;
|
|
1889
|
+
state.nextRetryAtMs = undefined;
|
|
1890
|
+
state.nextRetryAttemptCount = undefined;
|
|
1891
|
+
state.nextRetryFailureClass = undefined;
|
|
1892
|
+
state.failedUpdateId = undefined;
|
|
1893
|
+
state.failedFailureId = undefined;
|
|
1894
|
+
state.failedAttemptCount = undefined;
|
|
1895
|
+
state.failedClass = undefined;
|
|
1896
|
+
state.failedSummary = undefined;
|
|
1897
|
+
state.terminalFailureAtMs = undefined;
|
|
1898
|
+
state.unsettledExecutionCount = unsettledExecutions.size;
|
|
1899
|
+
notifyStateChange();
|
|
1900
|
+
},
|
|
1901
|
+
waitForDrain() {
|
|
1902
|
+
return drainPromise ?? Promise.resolve();
|
|
1903
|
+
},
|
|
1904
|
+
getState() {
|
|
1905
|
+
return getTelegramUpdateWorkerStateSnapshot(state);
|
|
1906
|
+
},
|
|
1907
|
+
};
|
|
1908
|
+
}
|
|
1909
|
+
const TELEGRAM_UPDATE_EXECUTION_FENCE = Symbol("pi-telegram.update-execution-fence");
|
|
1910
|
+
export function getTelegramUpdateExecutionFence(update) {
|
|
1911
|
+
if (!update || typeof update !== "object")
|
|
1912
|
+
return undefined;
|
|
1913
|
+
return update[TELEGRAM_UPDATE_EXECUTION_FENCE];
|
|
1914
|
+
}
|
|
1915
|
+
function bindTelegramUpdateExecutionFenceCarrier(value, execution) {
|
|
1916
|
+
if (!value || typeof value !== "object")
|
|
1917
|
+
return value;
|
|
1918
|
+
Object.defineProperty(value, TELEGRAM_UPDATE_EXECUTION_FENCE, {
|
|
1919
|
+
configurable: true,
|
|
1920
|
+
enumerable: false,
|
|
1921
|
+
value: execution,
|
|
1922
|
+
});
|
|
1923
|
+
return value;
|
|
1924
|
+
}
|
|
1925
|
+
function bindTelegramUpdateExecutionFence(update, execution) {
|
|
1926
|
+
bindTelegramUpdateExecutionFenceCarrier(update, execution);
|
|
1927
|
+
bindTelegramUpdateExecutionFenceCarrier(update.message, execution);
|
|
1928
|
+
bindTelegramUpdateExecutionFenceCarrier(update.edited_message, execution);
|
|
1929
|
+
bindTelegramUpdateExecutionFenceCarrier(update.callback_query, execution);
|
|
1930
|
+
bindTelegramUpdateExecutionFenceCarrier(update.callback_query?.message, execution);
|
|
1931
|
+
bindTelegramUpdateExecutionFenceCarrier(update.guest_message, execution);
|
|
1932
|
+
bindTelegramUpdateExecutionFenceCarrier(update.message_reaction, execution);
|
|
1933
|
+
return update;
|
|
1934
|
+
}
|
|
1935
|
+
export function assertTelegramUpdateExecutionCurrent(update) {
|
|
1936
|
+
getTelegramUpdateExecutionFence(update)?.assertCurrent();
|
|
1937
|
+
}
|
|
1938
|
+
export function createTelegramUpdateExecutionFenceGuard(update) {
|
|
1939
|
+
const execution = getTelegramUpdateExecutionFence(update);
|
|
1940
|
+
return () => execution?.assertCurrent();
|
|
1941
|
+
}
|
|
1942
|
+
export function carryTelegramUpdateExecutionFence(source, target) {
|
|
1943
|
+
const execution = getTelegramUpdateExecutionFence(source);
|
|
1944
|
+
return execution
|
|
1945
|
+
? bindTelegramUpdateExecutionFenceCarrier(target, execution)
|
|
1946
|
+
: target;
|
|
1947
|
+
}
|
|
1948
|
+
const UPDATE_HANDLER_REGISTRY_KEY = "__piTelegramUpdateHandlerRegistry__";
|
|
1949
|
+
function isValidV1UpdateHandlerRegistry(candidate) {
|
|
1950
|
+
if (!candidate || typeof candidate !== "object")
|
|
1951
|
+
return false;
|
|
1952
|
+
const r = candidate;
|
|
1953
|
+
return (r.version === 1 &&
|
|
1954
|
+
typeof r.add === "function" &&
|
|
1955
|
+
typeof r.dispatch === "function");
|
|
1956
|
+
}
|
|
1957
|
+
function getOrCreateUpdateHandlerRegistry() {
|
|
1958
|
+
const g = globalThis;
|
|
1959
|
+
const existing = g[UPDATE_HANDLER_REGISTRY_KEY];
|
|
1960
|
+
if (isValidV1UpdateHandlerRegistry(existing))
|
|
1961
|
+
return existing;
|
|
1962
|
+
const handlers = new Set();
|
|
1963
|
+
const registry = {
|
|
1964
|
+
version: 1,
|
|
1965
|
+
add(handler) {
|
|
1966
|
+
handlers.add(handler);
|
|
1967
|
+
return () => handlers.delete(handler);
|
|
1968
|
+
},
|
|
1969
|
+
async dispatch(update, execution) {
|
|
1970
|
+
for (const handler of handlers) {
|
|
1971
|
+
execution?.assertCurrent();
|
|
1972
|
+
try {
|
|
1973
|
+
const result = await handler(update, execution);
|
|
1974
|
+
if (result === "consume")
|
|
1975
|
+
return "consume";
|
|
1976
|
+
}
|
|
1977
|
+
catch {
|
|
1978
|
+
// Update handler errors must not break polling.
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
return "pass";
|
|
1982
|
+
},
|
|
1983
|
+
};
|
|
1984
|
+
g[UPDATE_HANDLER_REGISTRY_KEY] = registry;
|
|
1985
|
+
return registry;
|
|
1986
|
+
}
|
|
1987
|
+
/**
|
|
1988
|
+
* Called by pi-telegram's own runtime to obtain the registry it dispatches
|
|
1989
|
+
* through. Extension consumers should not call this; use
|
|
1990
|
+
* {@link registerTelegramUpdateHandler} instead.
|
|
1991
|
+
*/
|
|
1992
|
+
export function getTelegramUpdateHandlerRegistry() {
|
|
1993
|
+
return getOrCreateUpdateHandlerRegistry();
|
|
1994
|
+
}
|
|
1995
|
+
/**
|
|
1996
|
+
* Wrap a default polling `handleUpdate` with the public update handler registry.
|
|
1997
|
+
*/
|
|
1998
|
+
export function createTelegramUpdateHandle(deps) {
|
|
1999
|
+
const registry = deps.registry ?? getOrCreateUpdateHandlerRegistry();
|
|
2000
|
+
const { defaultHandle } = deps;
|
|
2001
|
+
return async (update, ctx) => {
|
|
2002
|
+
const verdict = await registry.dispatch(update);
|
|
2003
|
+
if (verdict === "consume")
|
|
2004
|
+
return;
|
|
2005
|
+
await defaultHandle(update, ctx);
|
|
2006
|
+
};
|
|
2007
|
+
}
|
|
2008
|
+
function mergeTelegramReportedAdmissionOutcome(current, next, updateId) {
|
|
2009
|
+
if (!current || current.kind === "deferred")
|
|
2010
|
+
return next;
|
|
2011
|
+
if (next.kind === "deferred")
|
|
2012
|
+
return current;
|
|
2013
|
+
if (current.kind === "complete" && next.kind === "complete")
|
|
2014
|
+
return current;
|
|
2015
|
+
if (current.kind === "queued" && next.kind === "queued" &&
|
|
2016
|
+
areTelegramQueueAdmissionReceiptsEqual(current, next))
|
|
2017
|
+
return current;
|
|
2018
|
+
throw new TelegramUpdateAdmissionOutcomeError(`Telegram update ${updateId} reported conflicting queue outcomes.`);
|
|
2019
|
+
}
|
|
2020
|
+
export function createTelegramInputCustodyWorkerJournalPort(store) {
|
|
2021
|
+
const legacyMutation = () => {
|
|
2022
|
+
throw new TelegramUpdateAdmissionOutcomeError("Telegram v3 custody forbids legacy raw worker settlement.");
|
|
2023
|
+
};
|
|
2024
|
+
return {
|
|
2025
|
+
read: () => store.read(),
|
|
2026
|
+
markQueued: legacyMutation,
|
|
2027
|
+
markExecutionFailure: legacyMutation,
|
|
2028
|
+
removeCompleted: legacyMutation,
|
|
2029
|
+
completeQueued: receipts => store.completeQueued(receipts),
|
|
2030
|
+
inputCustody: {
|
|
2031
|
+
acquireInput: store.acquireInput,
|
|
2032
|
+
startInput: store.startInput,
|
|
2033
|
+
completeInput: store.completeInput,
|
|
2034
|
+
queueInputs: store.queueInputs,
|
|
2035
|
+
},
|
|
2036
|
+
};
|
|
2037
|
+
}
|
|
2038
|
+
export function createTelegramInputCustodyLegacyDispositionRuntime(deps) {
|
|
2039
|
+
const withBinding = (recoveryKey, operation) => {
|
|
2040
|
+
if (!recoveryKey)
|
|
2041
|
+
throw new TelegramUpdateAdmissionOutcomeError("Telegram legacy custody disposition binding is unavailable.");
|
|
2042
|
+
return deps.withBindingReference(recoveryKey, binding => {
|
|
2043
|
+
if (binding.recoveryKey !== recoveryKey)
|
|
2044
|
+
throw new TelegramUpdateAdmissionOutcomeError("Telegram legacy custody disposition binding is unavailable.");
|
|
2045
|
+
return operation(binding.journal);
|
|
2046
|
+
});
|
|
2047
|
+
};
|
|
2048
|
+
return {
|
|
2049
|
+
list(recoveryKey) {
|
|
2050
|
+
return withBinding(recoveryKey, journal => journal.listLegacyCustodyCandidates());
|
|
2051
|
+
},
|
|
2052
|
+
apply(recoveryKey, authority) {
|
|
2053
|
+
return withBinding(recoveryKey, journal => journal.applyLegacyCustodyDisposition(authority));
|
|
2054
|
+
},
|
|
2055
|
+
};
|
|
2056
|
+
}
|
|
2057
|
+
export function createTelegramInputCustodyHandoffClient(deps) {
|
|
2058
|
+
return {
|
|
2059
|
+
async transfer(input) {
|
|
2060
|
+
const reconciled = deps.resolveAcceptedReference?.({
|
|
2061
|
+
sourceUpdateId: input.receipt.updateId,
|
|
2062
|
+
recipientBindingKey: input.recipientBindingKey
|
|
2063
|
+
});
|
|
2064
|
+
if (reconciled)
|
|
2065
|
+
return { ...reconciled, duplicate: true };
|
|
2066
|
+
const offered = deps.journal.offerInputHandoff({ receipt: input.receipt,
|
|
2067
|
+
recipientOwner: input.recipientOwner, handoffToken: input.handoffToken });
|
|
2068
|
+
const envelope = {
|
|
2069
|
+
kind: "leader.offerInputCustodyHandoff", requestId: input.requestId,
|
|
2070
|
+
recipientInstanceId: input.recipientInstanceId,
|
|
2071
|
+
recipientRegistrationGeneration: input.recipientRegistrationGeneration,
|
|
2072
|
+
recipientBindingKey: input.recipientBindingKey,
|
|
2073
|
+
sourceRecoveryKey: offered.source.journalBindingKey,
|
|
2074
|
+
source: offered.source, handoffId: offered.handoff.handoffId,
|
|
2075
|
+
sentAtMs: input.sentAtMs, ...(input.auth ? { auth: input.auth } : {}),
|
|
2076
|
+
};
|
|
2077
|
+
const response = await deps.sendEnvelope(envelope);
|
|
2078
|
+
if (response?.kind !== "bus.ack" || response.requestId !== input.requestId ||
|
|
2079
|
+
!response.ok || !response.result || typeof response.result !== "object" ||
|
|
2080
|
+
Array.isArray(response.result))
|
|
2081
|
+
throw new Error("Telegram input custody handoff acknowledgement is missing or rejected.");
|
|
2082
|
+
const result = response.result;
|
|
2083
|
+
const source = result.source;
|
|
2084
|
+
if (result.sourceRecoveryKey !== offered.source.journalBindingKey ||
|
|
2085
|
+
!source || typeof source !== "object" || Array.isArray(source) ||
|
|
2086
|
+
source.updateId !== offered.source.updateId ||
|
|
2087
|
+
!source.owner ||
|
|
2088
|
+
typeof source.owner !== "object" ||
|
|
2089
|
+
Array.isArray(source.owner) ||
|
|
2090
|
+
typeof source.owner.acquisitionId !== "string" ||
|
|
2091
|
+
source.owner.handoffId !== offered.handoff.handoffId ||
|
|
2092
|
+
typeof result.duplicate !== "boolean")
|
|
2093
|
+
throw new Error("Telegram input custody handoff acknowledgement returned mismatched authority.");
|
|
2094
|
+
return { sourceRecoveryKey: result.sourceRecoveryKey,
|
|
2095
|
+
source: { updateId: source.updateId, owner: {
|
|
2096
|
+
acquisitionId: source.owner.acquisitionId,
|
|
2097
|
+
handoffId: source.owner.handoffId
|
|
2098
|
+
} },
|
|
2099
|
+
duplicate: result.duplicate };
|
|
2100
|
+
},
|
|
2101
|
+
};
|
|
2102
|
+
}
|
|
2103
|
+
export function createTelegramInputCustodyHandoffAcceptanceRuntime(deps) {
|
|
2104
|
+
return {
|
|
2105
|
+
accept(input, ctx) {
|
|
2106
|
+
const binding = deps.resolveBinding(input.sourceRecoveryKey);
|
|
2107
|
+
if (!binding || binding.recoveryKey !== input.sourceRecoveryKey ||
|
|
2108
|
+
binding.recipientBindingKey !== input.recipientBindingKey ||
|
|
2109
|
+
input.source.journalBindingKey !== input.sourceRecoveryKey)
|
|
2110
|
+
throw new Error("Telegram input custody handoff binding is unavailable or changed.");
|
|
2111
|
+
const accepted = binding.journal.acceptInputHandoff({ source: input.source,
|
|
2112
|
+
recipientOwner: binding.recipientOwner, handoffId: input.handoffId });
|
|
2113
|
+
const acceptedHandoffId = accepted.receipt.owner.handoffId;
|
|
2114
|
+
if (!acceptedHandoffId || acceptedHandoffId !== input.handoffId)
|
|
2115
|
+
throw new Error("Telegram input custody handoff acceptance returned mismatched authority.");
|
|
2116
|
+
binding.signalWorker(ctx);
|
|
2117
|
+
return { sourceRecoveryKey: binding.recoveryKey,
|
|
2118
|
+
source: { updateId: accepted.receipt.updateId,
|
|
2119
|
+
owner: { acquisitionId: accepted.receipt.owner.acquisitionId,
|
|
2120
|
+
handoffId: acceptedHandoffId } }, duplicate: accepted.duplicate };
|
|
2121
|
+
},
|
|
2122
|
+
};
|
|
2123
|
+
}
|
|
2124
|
+
export function createTelegramInputCustodyForwardReferenceResolver(deps) {
|
|
2125
|
+
return input => {
|
|
2126
|
+
if (input.recipientBindingKey !== deps.recipientBindingKey)
|
|
2127
|
+
return undefined;
|
|
2128
|
+
const entry = deps.journal.read().entries.find(candidate => candidate.updateId === input.sourceUpdateId);
|
|
2129
|
+
const claim = entry?.inputClaim;
|
|
2130
|
+
if (!entry || entry.state !== "pending" || entry.preApprovalExcluded !== false ||
|
|
2131
|
+
!claim || claim.phase !== "ready" || claim.handoff ||
|
|
2132
|
+
claim.recipientBindingKey !== deps.recipientBindingKey ||
|
|
2133
|
+
!claim.owner.handoffId ||
|
|
2134
|
+
claim.owner.sessionGeneration !== deps.recipientOwner.sessionGeneration ||
|
|
2135
|
+
!isTelegramUpdateJournalQueueOwnerProcess(claim.owner, deps.recipientOwner))
|
|
2136
|
+
return undefined;
|
|
2137
|
+
return { sourceRecoveryKey: deps.recoveryKey, source: { updateId: entry.updateId,
|
|
2138
|
+
owner: { acquisitionId: claim.owner.acquisitionId,
|
|
2139
|
+
handoffId: claim.owner.handoffId } } };
|
|
2140
|
+
};
|
|
2141
|
+
}
|
|
2142
|
+
export function createTelegramInputCustodySourceReferenceWakeRuntime(deps) {
|
|
2143
|
+
return {
|
|
2144
|
+
wakeSource(input, ctx) {
|
|
2145
|
+
const binding = deps.resolveBinding(input.sourceRecoveryKey);
|
|
2146
|
+
if (!binding || binding.recoveryKey !== input.sourceRecoveryKey ||
|
|
2147
|
+
binding.recipientBindingKey !== input.recipientBindingKey)
|
|
2148
|
+
throw new Error("Telegram follower source-reference binding is unavailable or changed.");
|
|
2149
|
+
const entry = binding.journal.read().entries.find(candidate => candidate.updateId === input.sourceUpdateId);
|
|
2150
|
+
const claim = entry?.inputClaim;
|
|
2151
|
+
if (!entry || entry.state !== "pending" || entry.preApprovalExcluded !== false ||
|
|
2152
|
+
!claim || claim.phase !== "ready" || claim.handoff ||
|
|
2153
|
+
claim.recipientBindingKey !== input.recipientBindingKey ||
|
|
2154
|
+
claim.owner.acquisitionId !== input.sourceClaim.acquisitionId ||
|
|
2155
|
+
claim.owner.handoffId !== input.sourceClaim.handoffId ||
|
|
2156
|
+
claim.owner.sessionGeneration !== binding.recipientOwner.sessionGeneration ||
|
|
2157
|
+
!isTelegramUpdateJournalQueueOwnerProcess(claim.owner, binding.recipientOwner))
|
|
2158
|
+
throw new Error("Telegram follower source-reference claim is unavailable or changed.");
|
|
2159
|
+
binding.signalWorker(ctx);
|
|
2160
|
+
},
|
|
2161
|
+
};
|
|
2162
|
+
}
|
|
2163
|
+
export function createTelegramInputCustodyBusBindingRuntime(deps) {
|
|
2164
|
+
const acceptance = createTelegramInputCustodyHandoffAcceptanceRuntime({
|
|
2165
|
+
resolveBinding: deps.resolveBinding
|
|
2166
|
+
});
|
|
2167
|
+
const wake = createTelegramInputCustodySourceReferenceWakeRuntime({
|
|
2168
|
+
resolveBinding: deps.resolveBinding
|
|
2169
|
+
});
|
|
2170
|
+
return {
|
|
2171
|
+
acceptHandoff: acceptance.accept,
|
|
2172
|
+
wakeSource: wake.wakeSource,
|
|
2173
|
+
resolveForwardReference(input) {
|
|
2174
|
+
const recoveryKey = deps.getForwardRecoveryKey();
|
|
2175
|
+
if (!recoveryKey)
|
|
2176
|
+
return undefined;
|
|
2177
|
+
const binding = deps.resolveBinding(recoveryKey);
|
|
2178
|
+
if (!binding || binding.recoveryKey !== recoveryKey)
|
|
2179
|
+
return undefined;
|
|
2180
|
+
return createTelegramInputCustodyForwardReferenceResolver({
|
|
2181
|
+
recoveryKey: binding.recoveryKey,
|
|
2182
|
+
recipientBindingKey: binding.recipientBindingKey,
|
|
2183
|
+
recipientOwner: binding.recipientOwner,
|
|
2184
|
+
journal: binding.journal,
|
|
2185
|
+
})(input);
|
|
2186
|
+
},
|
|
2187
|
+
};
|
|
2188
|
+
}
|
|
2189
|
+
export function evaluateTelegramInputCustodyActivationReadiness(input) {
|
|
2190
|
+
if (!input.requested)
|
|
2191
|
+
return { enabled: false, blocker: "disabled" };
|
|
2192
|
+
if (!input.legacyWritersExcluded)
|
|
2193
|
+
return { enabled: false, blocker: "legacy-writers-present" };
|
|
2194
|
+
if (!input.historicalMigrationComplete)
|
|
2195
|
+
return { enabled: false, blocker: "migration-incomplete" };
|
|
2196
|
+
if (input.sourceStatus !== "absent" && input.sourceStatus !== "v3")
|
|
2197
|
+
return { enabled: false, blocker: "source-unready" };
|
|
2198
|
+
if (input.peerReadiness.some(readiness => readiness !== "ready"))
|
|
2199
|
+
return { enabled: false, blocker: "peer-capability-mismatch" };
|
|
2200
|
+
return { enabled: true };
|
|
2201
|
+
}
|
|
2202
|
+
export function createTelegramInputCustodyReadinessEvidenceStore(deps) {
|
|
2203
|
+
const decode = (serialized) => {
|
|
2204
|
+
if (serialized === undefined)
|
|
2205
|
+
return { version: 1, revision: 0 };
|
|
2206
|
+
if (serialized.length > 16_384)
|
|
2207
|
+
throw new Error("Telegram custody readiness evidence exceeds capacity.");
|
|
2208
|
+
let value;
|
|
2209
|
+
try {
|
|
2210
|
+
value = JSON.parse(serialized);
|
|
2211
|
+
}
|
|
2212
|
+
catch {
|
|
2213
|
+
throw new Error("Telegram custody readiness evidence is malformed.");
|
|
2214
|
+
}
|
|
2215
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
2216
|
+
throw new Error("Telegram custody readiness evidence is malformed.");
|
|
2217
|
+
const snapshot = value;
|
|
2218
|
+
if (snapshot.version !== 1 || !Number.isSafeInteger(snapshot.revision) ||
|
|
2219
|
+
snapshot.revision < 0 || Object.keys(snapshot).some(key => !["version", "revision", "writerExclusion", "migration", "startupExclusion",
|
|
2220
|
+
"migrationCompletion"].includes(key)))
|
|
2221
|
+
throw new Error("Telegram custody readiness evidence is malformed.");
|
|
2222
|
+
const validate = (evidence, kind) => {
|
|
2223
|
+
if (evidence === undefined)
|
|
2224
|
+
return undefined;
|
|
2225
|
+
if (!evidence || typeof evidence !== "object" || Array.isArray(evidence))
|
|
2226
|
+
throw new Error("Telegram custody readiness evidence is malformed.");
|
|
2227
|
+
const record = evidence;
|
|
2228
|
+
const statuses = kind === "writer" ? ["excluded", "present", "unknown"]
|
|
2229
|
+
: ["complete", "incomplete", "unknown"];
|
|
2230
|
+
const writerKeys = ["startupAuthorityId", "closureOperationId", "writerInventorySha256"];
|
|
2231
|
+
const migrationKeys = ["migrationAuthorityId", "startupAuthorityId", "closureOperationId",
|
|
2232
|
+
"migrationInventorySha256", "resultingSourceFamily"];
|
|
2233
|
+
const linkedKeys = kind === "writer" ? writerKeys : migrationKeys;
|
|
2234
|
+
const suppliedLinkedKeys = linkedKeys.filter(key => record[key] !== undefined);
|
|
2235
|
+
const linkedMalformed = suppliedLinkedKeys.length === linkedKeys.length && (typeof record.startupAuthorityId !== "string" || !record.startupAuthorityId ||
|
|
2236
|
+
typeof record.closureOperationId !== "string" || !record.closureOperationId ||
|
|
2237
|
+
(kind === "writer"
|
|
2238
|
+
? typeof record.writerInventorySha256 !== "string" ||
|
|
2239
|
+
!/^[a-f0-9]{64}$/u.test(record.writerInventorySha256)
|
|
2240
|
+
: typeof record.migrationAuthorityId !== "string" || !record.migrationAuthorityId ||
|
|
2241
|
+
typeof record.migrationInventorySha256 !== "string" ||
|
|
2242
|
+
!/^[a-f0-9]{64}$/u.test(record.migrationInventorySha256) ||
|
|
2243
|
+
(record.resultingSourceFamily !== "absent" && record.resultingSourceFamily !== "v3")));
|
|
2244
|
+
if (record.version !== 1 || typeof record.profileKey !== "string" || !record.profileKey ||
|
|
2245
|
+
typeof record.recoveryKey !== "string" || !record.recoveryKey ||
|
|
2246
|
+
!statuses.includes(record.status) || Object.keys(record).some(key => !["version", "profileKey", "recoveryKey", "status", ...linkedKeys].includes(key)) ||
|
|
2247
|
+
(suppliedLinkedKeys.length !== 0 && suppliedLinkedKeys.length !== linkedKeys.length) ||
|
|
2248
|
+
linkedMalformed)
|
|
2249
|
+
throw new Error("Telegram custody readiness evidence is malformed.");
|
|
2250
|
+
return { version: 1, profileKey: record.profileKey,
|
|
2251
|
+
recoveryKey: record.recoveryKey, status: record.status,
|
|
2252
|
+
...(suppliedLinkedKeys.length === linkedKeys.length ? kind === "writer" ? {
|
|
2253
|
+
startupAuthorityId: record.startupAuthorityId,
|
|
2254
|
+
closureOperationId: record.closureOperationId,
|
|
2255
|
+
writerInventorySha256: record.writerInventorySha256,
|
|
2256
|
+
} : {
|
|
2257
|
+
migrationAuthorityId: record.migrationAuthorityId,
|
|
2258
|
+
startupAuthorityId: record.startupAuthorityId,
|
|
2259
|
+
closureOperationId: record.closureOperationId,
|
|
2260
|
+
migrationInventorySha256: record.migrationInventorySha256,
|
|
2261
|
+
resultingSourceFamily: record.resultingSourceFamily,
|
|
2262
|
+
} : {}) };
|
|
2263
|
+
};
|
|
2264
|
+
const writerExclusion = validate(snapshot.writerExclusion, "writer");
|
|
2265
|
+
const migration = validate(snapshot.migration, "migration");
|
|
2266
|
+
let startupExclusion;
|
|
2267
|
+
if (snapshot.startupExclusion !== undefined) {
|
|
2268
|
+
const candidate = snapshot.startupExclusion;
|
|
2269
|
+
if (!candidate || typeof candidate.profileKey !== "string" ||
|
|
2270
|
+
typeof candidate.recoveryKey !== "string")
|
|
2271
|
+
throw new Error("Telegram custody readiness evidence is malformed.");
|
|
2272
|
+
startupExclusion = normalizeTelegramInputCustodyStartupExclusionAuthority(candidate, { profileKey: candidate.profileKey, recoveryKey: candidate.recoveryKey });
|
|
2273
|
+
if (!startupExclusion)
|
|
2274
|
+
throw new Error("Telegram custody readiness evidence is malformed.");
|
|
2275
|
+
}
|
|
2276
|
+
let migrationCompletion;
|
|
2277
|
+
if (snapshot.migrationCompletion !== undefined) {
|
|
2278
|
+
const candidate = snapshot.migrationCompletion;
|
|
2279
|
+
if (!candidate || typeof candidate.profileKey !== "string" ||
|
|
2280
|
+
typeof candidate.recoveryKey !== "string")
|
|
2281
|
+
throw new Error("Telegram custody readiness evidence is malformed.");
|
|
2282
|
+
migrationCompletion = normalizeTelegramInputCustodyMigrationCompletionAuthority(candidate, { profileKey: candidate.profileKey, recoveryKey: candidate.recoveryKey });
|
|
2283
|
+
if (!migrationCompletion)
|
|
2284
|
+
throw new Error("Telegram custody readiness evidence is malformed.");
|
|
2285
|
+
}
|
|
2286
|
+
const identities = [];
|
|
2287
|
+
if (writerExclusion)
|
|
2288
|
+
identities.push(writerExclusion);
|
|
2289
|
+
if (migration)
|
|
2290
|
+
identities.push(migration);
|
|
2291
|
+
if (startupExclusion)
|
|
2292
|
+
identities.push(startupExclusion);
|
|
2293
|
+
if (migrationCompletion)
|
|
2294
|
+
identities.push(migrationCompletion);
|
|
2295
|
+
if (identities.some(evidence => evidence.profileKey !== identities[0]?.profileKey ||
|
|
2296
|
+
evidence.recoveryKey !== identities[0]?.recoveryKey))
|
|
2297
|
+
throw new Error("Telegram custody readiness evidence has conflicting identities.");
|
|
2298
|
+
if (writerExclusion?.startupAuthorityId && startupExclusion &&
|
|
2299
|
+
(writerExclusion.startupAuthorityId !== startupExclusion.authorityId ||
|
|
2300
|
+
writerExclusion.closureOperationId !== startupExclusion.closureOperationId ||
|
|
2301
|
+
writerExclusion.writerInventorySha256 !== startupExclusion.writerInventorySha256))
|
|
2302
|
+
throw new Error("Telegram custody readiness evidence has conflicting authorities.");
|
|
2303
|
+
if (migration?.migrationAuthorityId && migrationCompletion &&
|
|
2304
|
+
(migration.migrationAuthorityId !== migrationCompletion.authorityId ||
|
|
2305
|
+
migration.startupAuthorityId !== migrationCompletion.startupAuthorityId ||
|
|
2306
|
+
migration.closureOperationId !== migrationCompletion.closureOperationId ||
|
|
2307
|
+
migration.migrationInventorySha256 !== migrationCompletion.migrationInventorySha256 ||
|
|
2308
|
+
migration.resultingSourceFamily !== migrationCompletion.resultingSourceFamily))
|
|
2309
|
+
throw new Error("Telegram custody readiness evidence has conflicting migration authorities.");
|
|
2310
|
+
if (migrationCompletion && startupExclusion &&
|
|
2311
|
+
(migrationCompletion.startupAuthorityId !== startupExclusion.authorityId ||
|
|
2312
|
+
migrationCompletion.closureOperationId !== startupExclusion.closureOperationId))
|
|
2313
|
+
throw new Error("Telegram custody readiness evidence has conflicting cutover authorities.");
|
|
2314
|
+
return { version: 1, revision: snapshot.revision,
|
|
2315
|
+
...(writerExclusion ? { writerExclusion } : {}),
|
|
2316
|
+
...(migration ? { migration } : {}),
|
|
2317
|
+
...(startupExclusion ? { startupExclusion } : {}),
|
|
2318
|
+
...(migrationCompletion ? { migrationCompletion } : {}) };
|
|
2319
|
+
};
|
|
2320
|
+
const read = () => decode(deps.readRetained());
|
|
2321
|
+
const publish = (input) => deps.withSerialization(() => {
|
|
2322
|
+
const current = read();
|
|
2323
|
+
if (current.revision !== input.expectedRevision)
|
|
2324
|
+
throw new Error("Telegram custody readiness evidence revision changed.");
|
|
2325
|
+
const next = { ...current, revision: current.revision + 1,
|
|
2326
|
+
...(input.kind === "writer-exclusion" ? { writerExclusion: input.evidence } :
|
|
2327
|
+
input.kind === "migration" ? { migration: input.evidence } :
|
|
2328
|
+
input.kind === "startup-exclusion" ? { startupExclusion: input.evidence } :
|
|
2329
|
+
{ migrationCompletion: input.evidence }) };
|
|
2330
|
+
const serialized = `${JSON.stringify(next)}\n`;
|
|
2331
|
+
const validated = decode(serialized);
|
|
2332
|
+
const normalizedEvidence = input.kind === "writer-exclusion"
|
|
2333
|
+
? validated.writerExclusion : input.kind === "migration"
|
|
2334
|
+
? validated.migration : input.kind === "startup-exclusion"
|
|
2335
|
+
? validated.startupExclusion : validated.migrationCompletion;
|
|
2336
|
+
if (!normalizedEvidence || !deps.authorizePublication(input.kind, normalizedEvidence))
|
|
2337
|
+
throw new Error("Telegram custody readiness evidence publication is unauthorized.");
|
|
2338
|
+
deps.publishRetained(serialized);
|
|
2339
|
+
return validated;
|
|
2340
|
+
});
|
|
2341
|
+
return { read, publish };
|
|
2342
|
+
}
|
|
2343
|
+
export function normalizeTelegramInputCustodyStartupExclusionAuthority(value, expected) {
|
|
2344
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
2345
|
+
return undefined;
|
|
2346
|
+
const record = value;
|
|
2347
|
+
const keys = ["version", "status", "authorityId", "closureOperationId", "profileKey",
|
|
2348
|
+
"recoveryKey", "writerInventorySha256", "allowedWriterProtocol", "authorizedAtMs"];
|
|
2349
|
+
if (Object.keys(record).some(key => !keys.includes(key)) || record.version !== 1 ||
|
|
2350
|
+
(record.status !== "enforced" && record.status !== "revoked") ||
|
|
2351
|
+
record.profileKey !== expected.profileKey || record.recoveryKey !== expected.recoveryKey ||
|
|
2352
|
+
typeof record.authorityId !== "string" || !record.authorityId || record.authorityId.length > 512 ||
|
|
2353
|
+
typeof record.closureOperationId !== "string" || !record.closureOperationId ||
|
|
2354
|
+
record.closureOperationId.length > 512 ||
|
|
2355
|
+
typeof record.writerInventorySha256 !== "string" ||
|
|
2356
|
+
!/^[a-f0-9]{64}$/u.test(record.writerInventorySha256) ||
|
|
2357
|
+
record.allowedWriterProtocol !== "custody-v3" ||
|
|
2358
|
+
!Number.isSafeInteger(record.authorizedAtMs) || record.authorizedAtMs < 0)
|
|
2359
|
+
return undefined;
|
|
2360
|
+
return { version: 1, status: record.status, authorityId: record.authorityId,
|
|
2361
|
+
closureOperationId: record.closureOperationId, profileKey: expected.profileKey,
|
|
2362
|
+
recoveryKey: expected.recoveryKey, writerInventorySha256: record.writerInventorySha256,
|
|
2363
|
+
allowedWriterProtocol: "custody-v3", authorizedAtMs: record.authorizedAtMs };
|
|
2364
|
+
}
|
|
2365
|
+
export function evaluateTelegramInputCustodyWriterExclusionEvidence(input) {
|
|
2366
|
+
const identity = { ...input.expected };
|
|
2367
|
+
const authority = input.startupAuthority;
|
|
2368
|
+
if (!input.inventory.complete ||
|
|
2369
|
+
input.inventory.profileKey !== input.expected.profileKey ||
|
|
2370
|
+
input.inventory.recoveryKey !== input.expected.recoveryKey ||
|
|
2371
|
+
!/^[a-f0-9]{64}$/u.test(input.inventory.writerInventorySha256) ||
|
|
2372
|
+
!authority || authority.status !== "enforced" ||
|
|
2373
|
+
authority.profileKey !== input.expected.profileKey ||
|
|
2374
|
+
authority.recoveryKey !== input.expected.recoveryKey ||
|
|
2375
|
+
authority.writerInventorySha256 !== input.inventory.writerInventorySha256)
|
|
2376
|
+
return { version: 1, status: "unknown", ...identity };
|
|
2377
|
+
const authorityBinding = { startupAuthorityId: authority.authorityId,
|
|
2378
|
+
closureOperationId: authority.closureOperationId,
|
|
2379
|
+
writerInventorySha256: authority.writerInventorySha256 };
|
|
2380
|
+
let unknown = false;
|
|
2381
|
+
for (const writer of input.inventory.writers) {
|
|
2382
|
+
let liveness;
|
|
2383
|
+
try {
|
|
2384
|
+
liveness = input.getProcessLiveness(writer);
|
|
2385
|
+
}
|
|
2386
|
+
catch {
|
|
2387
|
+
liveness = "unverifiable";
|
|
2388
|
+
}
|
|
2389
|
+
if (liveness === "alive")
|
|
2390
|
+
return { version: 1, status: "present", ...identity,
|
|
2391
|
+
...authorityBinding };
|
|
2392
|
+
if (liveness !== "dead")
|
|
2393
|
+
unknown = true;
|
|
2394
|
+
}
|
|
2395
|
+
return { version: 1, status: unknown ? "unknown" : "excluded", ...identity,
|
|
2396
|
+
...authorityBinding };
|
|
2397
|
+
}
|
|
2398
|
+
export function normalizeTelegramInputCustodyMigrationCompletionAuthority(value, expected) {
|
|
2399
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
2400
|
+
return undefined;
|
|
2401
|
+
const record = value;
|
|
2402
|
+
const keys = ["version", "status", "authorityId", "startupAuthorityId",
|
|
2403
|
+
"closureOperationId", "profileKey", "recoveryKey", "migrationInventorySha256",
|
|
2404
|
+
"resultingSourceFamily", "authorizedAtMs"];
|
|
2405
|
+
if (Object.keys(record).some(key => !keys.includes(key)) || record.version !== 1 ||
|
|
2406
|
+
(record.status !== "authorized" && record.status !== "revoked") ||
|
|
2407
|
+
record.profileKey !== expected.profileKey || record.recoveryKey !== expected.recoveryKey ||
|
|
2408
|
+
typeof record.authorityId !== "string" || !record.authorityId || record.authorityId.length > 512 ||
|
|
2409
|
+
typeof record.startupAuthorityId !== "string" || !record.startupAuthorityId ||
|
|
2410
|
+
record.startupAuthorityId.length > 512 ||
|
|
2411
|
+
typeof record.closureOperationId !== "string" || !record.closureOperationId ||
|
|
2412
|
+
record.closureOperationId.length > 512 ||
|
|
2413
|
+
typeof record.migrationInventorySha256 !== "string" ||
|
|
2414
|
+
!/^[a-f0-9]{64}$/u.test(record.migrationInventorySha256) ||
|
|
2415
|
+
(record.resultingSourceFamily !== "absent" && record.resultingSourceFamily !== "v3") ||
|
|
2416
|
+
!Number.isSafeInteger(record.authorizedAtMs) || record.authorizedAtMs < 0)
|
|
2417
|
+
return undefined;
|
|
2418
|
+
return { version: 1, status: record.status, authorityId: record.authorityId,
|
|
2419
|
+
startupAuthorityId: record.startupAuthorityId,
|
|
2420
|
+
closureOperationId: record.closureOperationId, profileKey: expected.profileKey,
|
|
2421
|
+
recoveryKey: expected.recoveryKey,
|
|
2422
|
+
migrationInventorySha256: record.migrationInventorySha256,
|
|
2423
|
+
resultingSourceFamily: record.resultingSourceFamily,
|
|
2424
|
+
authorizedAtMs: record.authorizedAtMs };
|
|
2425
|
+
}
|
|
2426
|
+
export function executeTelegramInputCustodyWriterCutover(input) {
|
|
2427
|
+
const matchesAuthority = (candidate) => Boolean(candidate && candidate.status === "enforced" && candidate.version === 1 &&
|
|
2428
|
+
candidate.profileKey === input.expected.profileKey &&
|
|
2429
|
+
candidate.recoveryKey === input.expected.recoveryKey &&
|
|
2430
|
+
candidate.authorityId === input.startupAuthority.authorityId &&
|
|
2431
|
+
candidate.closureOperationId === input.closure.operationId &&
|
|
2432
|
+
candidate.writerInventorySha256 === input.startupAuthority.writerInventorySha256 &&
|
|
2433
|
+
candidate.allowedWriterProtocol === "custody-v3" &&
|
|
2434
|
+
candidate.authorizedAtMs === input.startupAuthority.authorizedAtMs);
|
|
2435
|
+
if (input.closure.profileKey !== input.expected.profileKey ||
|
|
2436
|
+
input.closure.recoveryKey !== input.expected.recoveryKey ||
|
|
2437
|
+
!matchesAuthority(input.evidenceStore.read().startupExclusion))
|
|
2438
|
+
return { kind: "blocked", blocker: "startup-authority" };
|
|
2439
|
+
const evidence = evaluateTelegramInputCustodyWriterExclusionEvidence({
|
|
2440
|
+
expected: input.expected, inventory: input.inventory,
|
|
2441
|
+
startupAuthority: input.startupAuthority, getProcessLiveness: input.getProcessLiveness
|
|
2442
|
+
});
|
|
2443
|
+
if (evidence.status !== "excluded")
|
|
2444
|
+
return { kind: "blocked", blocker: "writer-inventory", evidence };
|
|
2445
|
+
const installed = input.installProtocolMode(input.closure, {
|
|
2446
|
+
startupAuthorityId: input.startupAuthority.authorityId,
|
|
2447
|
+
writerInventorySha256: input.startupAuthority.writerInventorySha256,
|
|
2448
|
+
});
|
|
2449
|
+
if (installed.mode.protocol !== "custody-v3" ||
|
|
2450
|
+
installed.mode.profileKey !== input.expected.profileKey ||
|
|
2451
|
+
installed.mode.recoveryKey !== input.expected.recoveryKey ||
|
|
2452
|
+
installed.mode.startupAuthorityId !== evidence.startupAuthorityId ||
|
|
2453
|
+
installed.mode.closureOperationId !== evidence.closureOperationId ||
|
|
2454
|
+
installed.mode.writerInventorySha256 !== evidence.writerInventorySha256)
|
|
2455
|
+
throw new Error("Telegram custody writer protocol installation returned mismatched authority.");
|
|
2456
|
+
const current = input.evidenceStore.read();
|
|
2457
|
+
if (!matchesAuthority(current.startupExclusion))
|
|
2458
|
+
return { kind: "blocked", blocker: "startup-authority", evidence };
|
|
2459
|
+
const existing = current.writerExclusion;
|
|
2460
|
+
if (existing?.status === "excluded" && existing.profileKey === evidence.profileKey &&
|
|
2461
|
+
existing.recoveryKey === evidence.recoveryKey &&
|
|
2462
|
+
existing.startupAuthorityId === evidence.startupAuthorityId &&
|
|
2463
|
+
existing.closureOperationId === evidence.closureOperationId &&
|
|
2464
|
+
existing.writerInventorySha256 === evidence.writerInventorySha256)
|
|
2465
|
+
return { kind: "completed", mode: installed.mode, evidence: existing,
|
|
2466
|
+
resumed: installed.resumed };
|
|
2467
|
+
input.evidenceStore.publish({ expectedRevision: current.revision,
|
|
2468
|
+
kind: "writer-exclusion", evidence });
|
|
2469
|
+
return { kind: "completed", mode: installed.mode, evidence, resumed: false };
|
|
2470
|
+
}
|
|
2471
|
+
function matchesTelegramInputCustodyActivationEvidence(evidence, expected) {
|
|
2472
|
+
return Boolean(evidence && evidence.profileKey === expected.profileKey &&
|
|
2473
|
+
evidence.recoveryKey === expected.recoveryKey);
|
|
2474
|
+
}
|
|
2475
|
+
export function executeTelegramInputCustodyMigrationCompletion(input) {
|
|
2476
|
+
const matchesAuthority = (candidate) => Boolean(candidate && candidate.version === 1 && candidate.status === "authorized" &&
|
|
2477
|
+
candidate.profileKey === input.expected.profileKey &&
|
|
2478
|
+
candidate.recoveryKey === input.expected.recoveryKey &&
|
|
2479
|
+
candidate.authorityId === input.authority.authorityId &&
|
|
2480
|
+
candidate.startupAuthorityId === input.authority.startupAuthorityId &&
|
|
2481
|
+
candidate.closureOperationId === input.authority.closureOperationId &&
|
|
2482
|
+
candidate.migrationInventorySha256 === input.authority.migrationInventorySha256 &&
|
|
2483
|
+
candidate.resultingSourceFamily === input.authority.resultingSourceFamily &&
|
|
2484
|
+
candidate.authorizedAtMs === input.authority.authorizedAtMs);
|
|
2485
|
+
const current = input.evidenceStore.read();
|
|
2486
|
+
if (!matchesAuthority(current.migrationCompletion))
|
|
2487
|
+
return { kind: "blocked", blocker: "migration-authority" };
|
|
2488
|
+
if (input.migrationInventorySha256 !== input.authority.migrationInventorySha256)
|
|
2489
|
+
return { kind: "blocked", blocker: "inventory-drift" };
|
|
2490
|
+
let source;
|
|
2491
|
+
try {
|
|
2492
|
+
source = input.inspectSource();
|
|
2493
|
+
}
|
|
2494
|
+
catch {
|
|
2495
|
+
source = "ambiguous";
|
|
2496
|
+
}
|
|
2497
|
+
if (source !== input.authority.resultingSourceFamily)
|
|
2498
|
+
return { kind: "blocked", blocker: "source-drift" };
|
|
2499
|
+
const evidence = { version: 1,
|
|
2500
|
+
status: "complete", ...input.expected,
|
|
2501
|
+
migrationAuthorityId: input.authority.authorityId,
|
|
2502
|
+
startupAuthorityId: input.authority.startupAuthorityId,
|
|
2503
|
+
closureOperationId: input.authority.closureOperationId,
|
|
2504
|
+
migrationInventorySha256: input.authority.migrationInventorySha256,
|
|
2505
|
+
resultingSourceFamily: input.authority.resultingSourceFamily };
|
|
2506
|
+
const existing = current.migration;
|
|
2507
|
+
if (existing?.status === "complete" && existing.profileKey === evidence.profileKey &&
|
|
2508
|
+
existing.recoveryKey === evidence.recoveryKey &&
|
|
2509
|
+
existing.migrationAuthorityId === evidence.migrationAuthorityId &&
|
|
2510
|
+
existing.startupAuthorityId === evidence.startupAuthorityId &&
|
|
2511
|
+
existing.closureOperationId === evidence.closureOperationId &&
|
|
2512
|
+
existing.migrationInventorySha256 === evidence.migrationInventorySha256 &&
|
|
2513
|
+
existing.resultingSourceFamily === evidence.resultingSourceFamily)
|
|
2514
|
+
return { kind: "completed", evidence: existing, resumed: true };
|
|
2515
|
+
input.evidenceStore.publish({ expectedRevision: current.revision, kind: "migration", evidence });
|
|
2516
|
+
return { kind: "completed", evidence, resumed: false };
|
|
2517
|
+
}
|
|
2518
|
+
export function createTelegramInputCustodyProvenReadinessResolver(deps) {
|
|
2519
|
+
return createTelegramInputCustodyActivationReadinessResolver({
|
|
2520
|
+
isRequested: deps.isRequested,
|
|
2521
|
+
inspectSource() {
|
|
2522
|
+
let source;
|
|
2523
|
+
try {
|
|
2524
|
+
source = deps.inspectSource();
|
|
2525
|
+
}
|
|
2526
|
+
catch {
|
|
2527
|
+
return "ambiguous";
|
|
2528
|
+
}
|
|
2529
|
+
const expected = deps.expectedIdentity();
|
|
2530
|
+
const authority = deps.readMigrationCompletionAuthority();
|
|
2531
|
+
return matchesTelegramInputCustodyActivationEvidence(authority, expected) &&
|
|
2532
|
+
authority?.status === "authorized" && authority.resultingSourceFamily === source
|
|
2533
|
+
? source : "ambiguous";
|
|
2534
|
+
},
|
|
2535
|
+
areLegacyWritersExcluded() {
|
|
2536
|
+
const expected = deps.expectedIdentity();
|
|
2537
|
+
const evidence = deps.readWriterExclusionEvidence();
|
|
2538
|
+
const authority = deps.readStartupExclusionAuthority();
|
|
2539
|
+
const mode = deps.readWriterProtocolMode();
|
|
2540
|
+
return matchesTelegramInputCustodyActivationEvidence(evidence, expected) &&
|
|
2541
|
+
matchesTelegramInputCustodyActivationEvidence(authority, expected) &&
|
|
2542
|
+
matchesTelegramInputCustodyActivationEvidence(mode, expected) &&
|
|
2543
|
+
evidence?.version === 1 && evidence.status === "excluded" &&
|
|
2544
|
+
authority?.version === 1 && authority.status === "enforced" &&
|
|
2545
|
+
mode?.protocol === "custody-v3" &&
|
|
2546
|
+
evidence.startupAuthorityId === authority.authorityId &&
|
|
2547
|
+
evidence.startupAuthorityId === mode.startupAuthorityId &&
|
|
2548
|
+
evidence.closureOperationId === authority.closureOperationId &&
|
|
2549
|
+
evidence.closureOperationId === mode.closureOperationId &&
|
|
2550
|
+
evidence.writerInventorySha256 === authority.writerInventorySha256 &&
|
|
2551
|
+
evidence.writerInventorySha256 === mode.writerInventorySha256;
|
|
2552
|
+
},
|
|
2553
|
+
isHistoricalMigrationComplete() {
|
|
2554
|
+
const expected = deps.expectedIdentity();
|
|
2555
|
+
const evidence = deps.readMigrationEvidence();
|
|
2556
|
+
const authority = deps.readMigrationCompletionAuthority();
|
|
2557
|
+
const startup = deps.readStartupExclusionAuthority();
|
|
2558
|
+
const mode = deps.readWriterProtocolMode();
|
|
2559
|
+
let source;
|
|
2560
|
+
try {
|
|
2561
|
+
source = deps.inspectSource();
|
|
2562
|
+
}
|
|
2563
|
+
catch {
|
|
2564
|
+
source = "ambiguous";
|
|
2565
|
+
}
|
|
2566
|
+
return matchesTelegramInputCustodyActivationEvidence(evidence, expected) &&
|
|
2567
|
+
matchesTelegramInputCustodyActivationEvidence(authority, expected) &&
|
|
2568
|
+
evidence?.version === 1 && evidence.status === "complete" &&
|
|
2569
|
+
authority?.version === 1 && authority.status === "authorized" &&
|
|
2570
|
+
authority.resultingSourceFamily === source &&
|
|
2571
|
+
evidence.migrationAuthorityId === authority.authorityId &&
|
|
2572
|
+
evidence.startupAuthorityId === authority.startupAuthorityId &&
|
|
2573
|
+
evidence.startupAuthorityId === startup?.authorityId &&
|
|
2574
|
+
evidence.startupAuthorityId === mode?.startupAuthorityId &&
|
|
2575
|
+
evidence.closureOperationId === authority.closureOperationId &&
|
|
2576
|
+
evidence.closureOperationId === mode?.closureOperationId &&
|
|
2577
|
+
evidence.migrationInventorySha256 === authority.migrationInventorySha256 &&
|
|
2578
|
+
evidence.resultingSourceFamily === authority.resultingSourceFamily;
|
|
2579
|
+
},
|
|
2580
|
+
listPeerReadiness: deps.listPeerReadiness,
|
|
2581
|
+
});
|
|
2582
|
+
}
|
|
2583
|
+
export function createTelegramInputCustodyActivationReadinessResolver(deps) {
|
|
2584
|
+
return () => {
|
|
2585
|
+
const requested = deps.isRequested();
|
|
2586
|
+
if (!requested)
|
|
2587
|
+
return { enabled: false, blocker: "disabled" };
|
|
2588
|
+
let legacyWritersExcluded = false;
|
|
2589
|
+
try {
|
|
2590
|
+
legacyWritersExcluded = deps.areLegacyWritersExcluded();
|
|
2591
|
+
}
|
|
2592
|
+
catch { /* blocked */ }
|
|
2593
|
+
if (!legacyWritersExcluded)
|
|
2594
|
+
return { enabled: false, blocker: "legacy-writers-present" };
|
|
2595
|
+
let historicalMigrationComplete = false;
|
|
2596
|
+
try {
|
|
2597
|
+
historicalMigrationComplete = deps.isHistoricalMigrationComplete();
|
|
2598
|
+
}
|
|
2599
|
+
catch { /* blocked */ }
|
|
2600
|
+
if (!historicalMigrationComplete)
|
|
2601
|
+
return { enabled: false, blocker: "migration-incomplete" };
|
|
2602
|
+
let sourceStatus;
|
|
2603
|
+
try {
|
|
2604
|
+
sourceStatus = deps.inspectSource();
|
|
2605
|
+
}
|
|
2606
|
+
catch {
|
|
2607
|
+
sourceStatus = "ambiguous";
|
|
2608
|
+
}
|
|
2609
|
+
if (sourceStatus !== "absent" && sourceStatus !== "v3")
|
|
2610
|
+
return { enabled: false, blocker: "source-unready" };
|
|
2611
|
+
let peerReadiness;
|
|
2612
|
+
try {
|
|
2613
|
+
peerReadiness = deps.listPeerReadiness();
|
|
2614
|
+
}
|
|
2615
|
+
catch {
|
|
2616
|
+
peerReadiness = ["unknown"];
|
|
2617
|
+
}
|
|
2618
|
+
return evaluateTelegramInputCustodyActivationReadiness({ requested,
|
|
2619
|
+
sourceStatus, legacyWritersExcluded, historicalMigrationComplete, peerReadiness });
|
|
2620
|
+
};
|
|
2621
|
+
}
|
|
2622
|
+
export function createTelegramInputCustodyLifecycleBindingResolver(deps) {
|
|
2623
|
+
return () => {
|
|
2624
|
+
if (!deps.isEnabled())
|
|
2625
|
+
return undefined;
|
|
2626
|
+
const source = deps.resolveInputJournal();
|
|
2627
|
+
const recipientBindingKey = deps.getRecipientBindingKey()?.trim();
|
|
2628
|
+
if (!source || !source.runtimeKey || !source.recoveryKey || !recipientBindingKey)
|
|
2629
|
+
return undefined;
|
|
2630
|
+
const port = createTelegramInputCustodyWorkerJournalPort(source.journal);
|
|
2631
|
+
return { runtimeKey: JSON.stringify({ source: source.runtimeKey, recipientBindingKey }),
|
|
2632
|
+
recoveryKey: source.recoveryKey,
|
|
2633
|
+
recipientBindingKey, journal: { ...port, appendBatch: source.journal.appendBatch,
|
|
2634
|
+
discardQueued: source.journal.discardQueued,
|
|
2635
|
+
recoverDeadQueueOwner: source.journal.recoverDeadQueueOwner,
|
|
2636
|
+
offerQueuedHandoff: source.journal.offerQueuedHandoff,
|
|
2637
|
+
acceptQueuedHandoff: source.journal.acceptQueuedHandoff,
|
|
2638
|
+
cancelQueuedHandoff: source.journal.cancelQueuedHandoff } };
|
|
2639
|
+
};
|
|
2640
|
+
}
|
|
2641
|
+
export function createTelegramCustodiedExecutionSession(input) {
|
|
2642
|
+
const receipts = new Map();
|
|
2643
|
+
const settledQueues = new Map();
|
|
2644
|
+
const settle = async (updateId, outcome) => {
|
|
2645
|
+
const receipt = receipts.get(updateId);
|
|
2646
|
+
if (!receipt) {
|
|
2647
|
+
const duplicate = settledQueues.get(updateId);
|
|
2648
|
+
if (duplicate && outcome.kind === "queued" &&
|
|
2649
|
+
duplicate.queueReceipt.queueKind === outcome.queueKind &&
|
|
2650
|
+
duplicate.queueReceipt.receiptId === outcome.receiptId &&
|
|
2651
|
+
duplicate.queueReceipt.sourceUpdateIds.length === outcome.sourceUpdateIds.length &&
|
|
2652
|
+
duplicate.queueReceipt.sourceUpdateIds.every((id, index) => id === outcome.sourceUpdateIds[index])) {
|
|
2653
|
+
settledQueues.delete(updateId);
|
|
2654
|
+
return duplicate;
|
|
2655
|
+
}
|
|
2656
|
+
throw new TelegramUpdateAdmissionOutcomeError(`Telegram deferred custody ${updateId} has no exact running receipt.`);
|
|
2657
|
+
}
|
|
2658
|
+
if (outcome.kind === "deferred")
|
|
2659
|
+
return { status: "deferred", receipt };
|
|
2660
|
+
if (outcome.kind === "queued") {
|
|
2661
|
+
if (!outcome.sourceUpdateIds.includes(updateId))
|
|
2662
|
+
throw new TelegramUpdateAdmissionOutcomeError("Telegram grouped queue custody omitted the settling input.");
|
|
2663
|
+
const grouped = outcome.sourceUpdateIds.map(sourceUpdateId => receipts.get(sourceUpdateId));
|
|
2664
|
+
if (grouped.some(candidate => !candidate))
|
|
2665
|
+
throw new TelegramUpdateAdmissionOutcomeError("Telegram grouped queue custody omitted an exact running source receipt.");
|
|
2666
|
+
const queued = input.journal.queueInputs({ queueKind: outcome.queueKind,
|
|
2667
|
+
receiptId: outcome.receiptId, receipts: grouped });
|
|
2668
|
+
const result = { status: "queued", queueReceipt: queued.queueReceipt };
|
|
2669
|
+
for (const sourceUpdateId of outcome.sourceUpdateIds) {
|
|
2670
|
+
receipts.delete(sourceUpdateId);
|
|
2671
|
+
if (sourceUpdateId !== updateId)
|
|
2672
|
+
settledQueues.set(sourceUpdateId, result);
|
|
2673
|
+
}
|
|
2674
|
+
return result;
|
|
2675
|
+
}
|
|
2676
|
+
input.journal.completeInput(receipt);
|
|
2677
|
+
receipts.delete(updateId);
|
|
2678
|
+
return { status: "completed" };
|
|
2679
|
+
};
|
|
2680
|
+
return {
|
|
2681
|
+
settle,
|
|
2682
|
+
async execute(update, handler) {
|
|
2683
|
+
const acquired = input.journal.acquireInput({ updateId: update.update_id,
|
|
2684
|
+
recipientBindingKey: input.recipientBindingKey, executionUpdate: update });
|
|
2685
|
+
const started = input.journal.startInput(acquired.receipt);
|
|
2686
|
+
if (!started.started)
|
|
2687
|
+
return { status: "outcome-unknown", receipt: acquired.receipt };
|
|
2688
|
+
receipts.set(update.update_id, acquired.receipt);
|
|
2689
|
+
let outcome;
|
|
2690
|
+
try {
|
|
2691
|
+
outcome = await handler(started.update);
|
|
2692
|
+
}
|
|
2693
|
+
catch (error) {
|
|
2694
|
+
receipts.delete(update.update_id);
|
|
2695
|
+
throw error;
|
|
2696
|
+
}
|
|
2697
|
+
return settle(update.update_id, outcome);
|
|
2698
|
+
},
|
|
2699
|
+
};
|
|
2700
|
+
}
|
|
2701
|
+
export async function executeTelegramCustodiedInput(input) {
|
|
2702
|
+
return createTelegramCustodiedExecutionSession(input).execute(input.update, input.execute);
|
|
2703
|
+
}
|
|
2704
|
+
/**
|
|
2705
|
+
* Compose the stable public handler registry with source-bound semantic
|
|
2706
|
+
* admission. Production polling switches to this only with the journal worker.
|
|
2707
|
+
*/
|
|
2708
|
+
export function createTelegramUpdateAdmissionHandle(deps) {
|
|
2709
|
+
if (deps.onLateOutcome && !deps.onLateOutcomeError) {
|
|
2710
|
+
throw new Error("Telegram late admission outcomes require a diagnostic error sink.");
|
|
2711
|
+
}
|
|
2712
|
+
const registry = deps.registry ?? getOrCreateUpdateHandlerRegistry();
|
|
2713
|
+
let nextExecutionGeneration = 0;
|
|
2714
|
+
return async (update, ctx, signal) => {
|
|
2715
|
+
const generation = ++nextExecutionGeneration;
|
|
2716
|
+
const execution = {
|
|
2717
|
+
generation,
|
|
2718
|
+
updateId: update.update_id,
|
|
2719
|
+
signal,
|
|
2720
|
+
isCurrent: () => !signal.aborted,
|
|
2721
|
+
assertCurrent() {
|
|
2722
|
+
if (signal.aborted) {
|
|
2723
|
+
throw signal.reason ?? new DOMException("Aborted", "AbortError");
|
|
2724
|
+
}
|
|
2725
|
+
},
|
|
2726
|
+
};
|
|
2727
|
+
execution.assertCurrent();
|
|
2728
|
+
const verdict = await registry.dispatch(update, execution);
|
|
2729
|
+
execution.assertCurrent();
|
|
2730
|
+
if (verdict === "consume")
|
|
2731
|
+
return { kind: "complete" };
|
|
2732
|
+
let immediate = true;
|
|
2733
|
+
let outcome;
|
|
2734
|
+
const boundUpdate = bindTelegramUpdateExecutionFence(bindTelegramUpdateAdmissionSource(update, (next) => {
|
|
2735
|
+
if (immediate) {
|
|
2736
|
+
outcome = mergeTelegramReportedAdmissionOutcome(outcome, next, update.update_id);
|
|
2737
|
+
return;
|
|
2738
|
+
}
|
|
2739
|
+
if (!deps.onLateOutcome) {
|
|
2740
|
+
throw new TelegramUpdateAdmissionOutcomeError(`Telegram update ${update.update_id} reported a late outcome without an owner.`);
|
|
2741
|
+
}
|
|
2742
|
+
void Promise.resolve()
|
|
2743
|
+
.then(() => deps.onLateOutcome(next, {
|
|
2744
|
+
updateId: update.update_id,
|
|
2745
|
+
ctx,
|
|
2746
|
+
signal,
|
|
2747
|
+
}))
|
|
2748
|
+
.catch((error) => {
|
|
2749
|
+
try {
|
|
2750
|
+
deps.onLateOutcomeError?.(error, update.update_id);
|
|
2751
|
+
}
|
|
2752
|
+
catch {
|
|
2753
|
+
// Diagnostic sinks must not create an unhandled late Promise.
|
|
2754
|
+
}
|
|
2755
|
+
});
|
|
2756
|
+
}), execution);
|
|
2757
|
+
try {
|
|
2758
|
+
execution.assertCurrent();
|
|
2759
|
+
await deps.defaultHandle(boundUpdate, ctx, execution);
|
|
2760
|
+
}
|
|
2761
|
+
finally {
|
|
2762
|
+
immediate = false;
|
|
2763
|
+
}
|
|
2764
|
+
return outcome ?? { kind: "complete" };
|
|
2765
|
+
};
|
|
2766
|
+
}
|
|
2767
|
+
export function createTelegramCustodiedUpdateAdmissionHandle(deps) {
|
|
2768
|
+
const session = createTelegramCustodiedExecutionSession({ journal: deps.journal,
|
|
2769
|
+
recipientBindingKey: deps.recipientBindingKey });
|
|
2770
|
+
const admission = createTelegramUpdateAdmissionHandle({
|
|
2771
|
+
defaultHandle: deps.defaultHandle,
|
|
2772
|
+
registry: deps.registry,
|
|
2773
|
+
async onLateOutcome(outcome, details) {
|
|
2774
|
+
const result = await session.settle(details.updateId, outcome);
|
|
2775
|
+
deps.onCustodiedLateSettlement?.(result, { updateId: details.updateId, signal: details.signal });
|
|
2776
|
+
},
|
|
2777
|
+
onLateOutcomeError: deps.onLateOutcomeError,
|
|
2778
|
+
});
|
|
2779
|
+
return (update, ctx, signal) => session.execute(update, started => admission(started, ctx, signal));
|
|
2780
|
+
}
|
|
2781
|
+
export function createTelegramQueueAdmissionSettlementMuxRuntime(runtimes) {
|
|
2782
|
+
const settle = (operation) => {
|
|
2783
|
+
for (const runtime of runtimes)
|
|
2784
|
+
operation(runtime);
|
|
2785
|
+
};
|
|
2786
|
+
return {
|
|
2787
|
+
isItemReady: (item) => (item.admissionReceipts ?? []).every((receipt) => runtimes.some((runtime) => runtime.isItemReady({ admissionReceipts: [receipt] }))),
|
|
2788
|
+
getQueueReceiptOwner(receipt) {
|
|
2789
|
+
let owner;
|
|
2790
|
+
for (const runtime of runtimes) {
|
|
2791
|
+
const candidate = runtime.getQueueReceiptOwner(receipt);
|
|
2792
|
+
if (!candidate)
|
|
2793
|
+
continue;
|
|
2794
|
+
if (owner &&
|
|
2795
|
+
!areTelegramUpdateJournalQueueOwnersEqual(owner, candidate)) {
|
|
2796
|
+
throw new TelegramUpdateAdmissionOutcomeError(`Telegram queue receipt ${receipt.receiptId} has multiple live owners.`);
|
|
2797
|
+
}
|
|
2798
|
+
owner = candidate;
|
|
2799
|
+
}
|
|
2800
|
+
return owner ? { ...owner } : undefined;
|
|
2801
|
+
},
|
|
2802
|
+
onPromptHandedOff: (item, ctx) => settle((runtime) => runtime.onPromptHandedOff(item, ctx)),
|
|
2803
|
+
onControlSettled: (item, ctx) => settle((runtime) => runtime.onControlSettled(item, ctx)),
|
|
2804
|
+
onItemsDiscarded: (items, ctx) => settle((runtime) => runtime.onItemsDiscarded(items, ctx)),
|
|
2805
|
+
};
|
|
2806
|
+
}
|
|
2807
|
+
export function createTelegramQueueAdmissionSettlementRuntime(worker) {
|
|
2808
|
+
const complete = (items, ctx, reason) => {
|
|
2809
|
+
const receipts = [];
|
|
2810
|
+
for (const item of items) {
|
|
2811
|
+
if (item.admissionReceipts)
|
|
2812
|
+
receipts.push(...item.admissionReceipts);
|
|
2813
|
+
}
|
|
2814
|
+
worker.completeQueueReceipts({ receipts, ctx, reason });
|
|
2815
|
+
};
|
|
2816
|
+
return {
|
|
2817
|
+
isItemReady: (item) => (item.admissionReceipts ?? []).every(worker.isQueueReceiptCommitted),
|
|
2818
|
+
getQueueReceiptOwner: worker.getQueueReceiptOwner,
|
|
2819
|
+
onPromptHandedOff: (item, ctx) => complete([item], ctx, "prompt-handoff"),
|
|
2820
|
+
onControlSettled: (item, ctx) => complete([item], ctx, "control-settlement"),
|
|
2821
|
+
onItemsDiscarded: (items, ctx) => complete(items, ctx, "discard"),
|
|
2822
|
+
};
|
|
2823
|
+
}
|
|
2824
|
+
export function createTelegramQueueHandoffControlExecutionFactory(deps) {
|
|
2825
|
+
return (payload) => async (ctx) => {
|
|
2826
|
+
if (!deps.isContextCurrent(ctx))
|
|
2827
|
+
return;
|
|
2828
|
+
if (payload.controlType === "status") {
|
|
2829
|
+
await deps.showStatus(payload.chatId, payload.replyToMessageId, ctx, payload.target?.threadId);
|
|
2830
|
+
return;
|
|
2831
|
+
}
|
|
2832
|
+
await deps.openModelMenu(payload.chatId, payload.replyToMessageId, ctx, payload.target?.threadId);
|
|
2833
|
+
};
|
|
2834
|
+
}
|
|
2835
|
+
function assertTelegramQueueHandoffStageMatches(stage, receipt) {
|
|
2836
|
+
if (stage.status !== "staged" ||
|
|
2837
|
+
stage.receiptId !== receipt.receiptId ||
|
|
2838
|
+
stage.sourceUpdateIds.length !== receipt.sourceUpdateIds.length ||
|
|
2839
|
+
stage.sourceUpdateIds.some((updateId, index) => updateId !== receipt.sourceUpdateIds[index])) {
|
|
2840
|
+
throw new Error("Telegram queue handoff staging returned a mismatched receipt.");
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
2843
|
+
export async function coordinateTelegramQueueHandoff(input) {
|
|
2844
|
+
const handoff = createTelegramQueueHandoff({
|
|
2845
|
+
handoffToken: input.handoffToken,
|
|
2846
|
+
item: input.item,
|
|
2847
|
+
});
|
|
2848
|
+
const receipt = handoff.payload.admissionReceipts[0];
|
|
2849
|
+
if (!receipt || handoff.payload.admissionReceipts.length !== 1) {
|
|
2850
|
+
throw new Error("Telegram queue handoff requires exactly one complete receipt.");
|
|
2851
|
+
}
|
|
2852
|
+
const handoffInput = {
|
|
2853
|
+
queueKind: receipt.queueKind,
|
|
2854
|
+
receiptId: receipt.receiptId,
|
|
2855
|
+
sourceUpdateIds: receipt.sourceUpdateIds,
|
|
2856
|
+
expectedOwner: input.expectedOwner,
|
|
2857
|
+
recipientOwner: input.recipientOwner,
|
|
2858
|
+
handoffToken: input.handoffToken,
|
|
2859
|
+
};
|
|
2860
|
+
input.lifecycle.offerQueueReceiptHandoff(handoffInput);
|
|
2861
|
+
let stage;
|
|
2862
|
+
try {
|
|
2863
|
+
stage = await input.stageRemote({
|
|
2864
|
+
handoffToken: input.handoffToken,
|
|
2865
|
+
expectedOwner: input.expectedOwner,
|
|
2866
|
+
recipientOwner: input.recipientOwner,
|
|
2867
|
+
payload: handoff.payload,
|
|
2868
|
+
});
|
|
2869
|
+
assertTelegramQueueHandoffStageMatches(stage, receipt);
|
|
2870
|
+
}
|
|
2871
|
+
catch (error) {
|
|
2872
|
+
let cancelled = false;
|
|
2873
|
+
try {
|
|
2874
|
+
input.lifecycle.cancelQueueReceiptHandoff(handoffInput);
|
|
2875
|
+
cancelled = true;
|
|
2876
|
+
}
|
|
2877
|
+
catch {
|
|
2878
|
+
return {
|
|
2879
|
+
status: "retained",
|
|
2880
|
+
receipt: { ...receipt, sourceUpdateIds: [...receipt.sourceUpdateIds] },
|
|
2881
|
+
error,
|
|
2882
|
+
cancelled: false,
|
|
2883
|
+
};
|
|
2884
|
+
}
|
|
2885
|
+
return {
|
|
2886
|
+
status: "retained",
|
|
2887
|
+
receipt: { ...receipt, sourceUpdateIds: [...receipt.sourceUpdateIds] },
|
|
2888
|
+
error,
|
|
2889
|
+
cancelled,
|
|
2890
|
+
};
|
|
2891
|
+
}
|
|
2892
|
+
let donorRemoved = false;
|
|
2893
|
+
try {
|
|
2894
|
+
donorRemoved = input.removeDonorItem(receipt);
|
|
2895
|
+
}
|
|
2896
|
+
catch (error) {
|
|
2897
|
+
throw new Error(`Telegram queue handoff donor removal failed after acceptance: ${error instanceof Error ? error.message : String(error)}`);
|
|
2898
|
+
}
|
|
2899
|
+
if (!donorRemoved) {
|
|
2900
|
+
throw new Error(`Telegram queue handoff donor item ${receipt.receiptId} disappeared after acceptance.`);
|
|
2901
|
+
}
|
|
2902
|
+
return {
|
|
2903
|
+
status: "transferred",
|
|
2904
|
+
receipt: { ...receipt, sourceUpdateIds: [...receipt.sourceUpdateIds] },
|
|
2905
|
+
queueOwner: { ...stage.queueOwner },
|
|
2906
|
+
};
|
|
2907
|
+
}
|
|
2908
|
+
export function createTelegramQueueHandoffReconciliationBinding(recordFailure) {
|
|
2909
|
+
let reconcile;
|
|
2910
|
+
return {
|
|
2911
|
+
request(ctx) {
|
|
2912
|
+
void reconcile?.(ctx).catch((error) => recordFailure?.(error));
|
|
2913
|
+
},
|
|
2914
|
+
set(next) {
|
|
2915
|
+
reconcile = next;
|
|
2916
|
+
},
|
|
2917
|
+
};
|
|
2918
|
+
}
|
|
2919
|
+
export function createTelegramQueueHandoffRecipientRuntime(deps) {
|
|
2920
|
+
return async (envelope, ctx) => {
|
|
2921
|
+
const stage = deps.staging.stage(envelope.payload);
|
|
2922
|
+
const receipt = envelope.payload.admissionReceipts[0];
|
|
2923
|
+
if (!receipt || envelope.payload.admissionReceipts.length !== 1) {
|
|
2924
|
+
deps.staging.cancel(receipt ?? {
|
|
2925
|
+
queueKind: envelope.payload.kind,
|
|
2926
|
+
receiptId: stage.receiptId,
|
|
2927
|
+
sourceUpdateIds: stage.sourceUpdateIds,
|
|
2928
|
+
});
|
|
2929
|
+
throw new Error("Telegram queue handoff requires exactly one complete receipt.");
|
|
2930
|
+
}
|
|
2931
|
+
const journalBindingKey = receipt.journalBindingKey;
|
|
2932
|
+
if (!journalBindingKey) {
|
|
2933
|
+
deps.staging.cancel(receipt);
|
|
2934
|
+
throw new Error("Telegram queue handoff receipt omitted its journal binding.");
|
|
2935
|
+
}
|
|
2936
|
+
if (deps.isTransportStampActive &&
|
|
2937
|
+
!deps.isTransportStampActive(envelope.payload.transportStamp)) {
|
|
2938
|
+
deps.staging.cancel(receipt);
|
|
2939
|
+
throw new Error("Telegram queue handoff payload belongs to an inactive transport generation.");
|
|
2940
|
+
}
|
|
2941
|
+
const lifecycle = deps.getLifecycleForBinding(journalBindingKey);
|
|
2942
|
+
if (!lifecycle) {
|
|
2943
|
+
deps.staging.cancel(receipt);
|
|
2944
|
+
throw new Error("Telegram queue handoff journal binding is not active.");
|
|
2945
|
+
}
|
|
2946
|
+
const donorOwner = {
|
|
2947
|
+
instanceId: envelope.donorInstanceId,
|
|
2948
|
+
processId: envelope.donorProcessId,
|
|
2949
|
+
processBirthId: envelope.donorProcessBirthId,
|
|
2950
|
+
sessionGeneration: envelope.donorSessionGeneration,
|
|
2951
|
+
acquisitionId: envelope.donorAcquisitionId,
|
|
2952
|
+
acquiredAtMs: envelope.donorAcquiredAtMs,
|
|
2953
|
+
};
|
|
2954
|
+
let accepted;
|
|
2955
|
+
try {
|
|
2956
|
+
accepted = lifecycle.acceptQueueReceiptHandoff({
|
|
2957
|
+
queueKind: receipt.queueKind,
|
|
2958
|
+
receiptId: receipt.receiptId,
|
|
2959
|
+
sourceUpdateIds: receipt.sourceUpdateIds,
|
|
2960
|
+
expectedOwner: donorOwner,
|
|
2961
|
+
recipientOwner: deps.getRecipientOwner(),
|
|
2962
|
+
handoffToken: envelope.handoffToken,
|
|
2963
|
+
});
|
|
2964
|
+
await lifecycle.publishAcceptedQueueReceipt({
|
|
2965
|
+
receipt,
|
|
2966
|
+
queueOwner: accepted.queueOwner,
|
|
2967
|
+
ctx,
|
|
2968
|
+
});
|
|
2969
|
+
if (!deps.staging.accept(receipt)) {
|
|
2970
|
+
throw new Error("Telegram queue handoff payload disappeared before readiness publication.");
|
|
2971
|
+
}
|
|
2972
|
+
}
|
|
2973
|
+
catch (error) {
|
|
2974
|
+
deps.staging.cancel(receipt);
|
|
2975
|
+
throw error;
|
|
2976
|
+
}
|
|
2977
|
+
deps.dispatchNext(ctx);
|
|
2978
|
+
return { ...stage, queueOwner: { ...accepted.queueOwner } };
|
|
2979
|
+
};
|
|
2980
|
+
}
|
|
2981
|
+
/** Own queue-handoff projections over journals, admission, IPC, and live queue state. */
|
|
2982
|
+
export function createTelegramQueueHandoffReconciliationRuntimeAssembly(deps) {
|
|
2983
|
+
return createTelegramQueueHandoffReconciler({
|
|
2984
|
+
ownsDirect: deps.ownsDirect,
|
|
2985
|
+
isFollowerRegistered: deps.isFollowerRegistered,
|
|
2986
|
+
isBusEnabled: deps.isBusEnabled,
|
|
2987
|
+
canHandoffWithLeader: deps.canHandoffWithLeader,
|
|
2988
|
+
listFollowers: deps.listFollowers,
|
|
2989
|
+
createRecipientJournalBindingKey(recipient) {
|
|
2990
|
+
if (!recipient.profileKey)
|
|
2991
|
+
return undefined;
|
|
2992
|
+
return deps.createRecipientJournalResolver(recipient.profileKey)()
|
|
2993
|
+
?.recoveryKey;
|
|
2994
|
+
},
|
|
2995
|
+
getQueuedItems: deps.queueStore.getQueuedItems,
|
|
2996
|
+
getReceiptOwner(receipt) {
|
|
2997
|
+
return deps.admission.getSettlement()?.getQueueReceiptOwner(receipt);
|
|
2998
|
+
},
|
|
2999
|
+
getLifecycleForReceipt(receipt) {
|
|
3000
|
+
const bindingKey = receipt.journalBindingKey;
|
|
3001
|
+
return bindingKey
|
|
3002
|
+
? deps.admission.getLifecycleForJournalBinding(bindingKey)
|
|
3003
|
+
: undefined;
|
|
3004
|
+
},
|
|
3005
|
+
createHandoffToken: deps.createHandoffToken,
|
|
3006
|
+
createRequestId: deps.createRequestId,
|
|
3007
|
+
donorInstanceId: deps.donorInstanceId,
|
|
3008
|
+
authSecret: deps.authSecret,
|
|
3009
|
+
stageThroughFollower(input) {
|
|
3010
|
+
const registrationGeneration = input.recipient.registrationGeneration;
|
|
3011
|
+
if (!registrationGeneration) {
|
|
3012
|
+
throw new Error("Telegram queue handoff recipient registration generation is unavailable.");
|
|
3013
|
+
}
|
|
3014
|
+
return deps.stageThroughFollower({
|
|
3015
|
+
recipientInstanceId: input.recipient.instanceId,
|
|
3016
|
+
recipientRegistrationGeneration: registrationGeneration,
|
|
3017
|
+
donorProcessId: input.expectedOwner.processId,
|
|
3018
|
+
donorProcessBirthId: input.expectedOwner.processBirthId,
|
|
3019
|
+
donorSessionGeneration: input.expectedOwner.sessionGeneration,
|
|
3020
|
+
donorAcquisitionId: input.expectedOwner.acquisitionId,
|
|
3021
|
+
donorAcquiredAtMs: input.expectedOwner.acquiredAtMs,
|
|
3022
|
+
handoffToken: input.handoffToken,
|
|
3023
|
+
payload: input.payload,
|
|
3024
|
+
});
|
|
3025
|
+
},
|
|
3026
|
+
routeThroughLeader: deps.routeThroughLeader,
|
|
3027
|
+
removeDonorItem(receipt) {
|
|
3028
|
+
return removeTelegramQueueItemByReceipt({
|
|
3029
|
+
receipt,
|
|
3030
|
+
store: deps.queueStore,
|
|
3031
|
+
});
|
|
3032
|
+
},
|
|
3033
|
+
recordFailure(error, details) {
|
|
3034
|
+
deps.recordRuntimeEvent?.("inbound-worker", error, details);
|
|
3035
|
+
},
|
|
3036
|
+
});
|
|
3037
|
+
}
|
|
3038
|
+
export function createTelegramQueueHandoffReconciler(deps) {
|
|
3039
|
+
let operation;
|
|
3040
|
+
const reconcile = async (ctx) => {
|
|
3041
|
+
const followerRegistered = deps.isFollowerRegistered();
|
|
3042
|
+
if ((!deps.ownsDirect() && !followerRegistered) ||
|
|
3043
|
+
!deps.isBusEnabled() ||
|
|
3044
|
+
(followerRegistered && deps.canHandoffWithLeader?.() === false)) {
|
|
3045
|
+
return;
|
|
3046
|
+
}
|
|
3047
|
+
const followers = deps.listFollowers().filter((follower) => follower.instanceId !== deps.donorInstanceId);
|
|
3048
|
+
if (followers.length === 0)
|
|
3049
|
+
return;
|
|
3050
|
+
for (const item of [...deps.getQueuedItems()]) {
|
|
3051
|
+
const target = item.target;
|
|
3052
|
+
const recipient = target
|
|
3053
|
+
? followers.find((candidate) => candidate.target?.chatId === target.chatId &&
|
|
3054
|
+
candidate.target?.threadId === target.threadId)
|
|
3055
|
+
: undefined;
|
|
3056
|
+
if (!recipient)
|
|
3057
|
+
continue;
|
|
3058
|
+
const receipt = item.admissionReceipts?.[0];
|
|
3059
|
+
if (!receipt || item.admissionReceipts?.length !== 1)
|
|
3060
|
+
continue;
|
|
3061
|
+
if (!receipt.journalBindingKey ||
|
|
3062
|
+
!getTelegramUpdateJournalBindingPath(receipt.journalBindingKey)) {
|
|
3063
|
+
continue;
|
|
3064
|
+
}
|
|
3065
|
+
const recipientJournalBindingKey = deps.createRecipientJournalBindingKey(recipient);
|
|
3066
|
+
if (!recipientJournalBindingKey)
|
|
3067
|
+
continue;
|
|
3068
|
+
const recipientItem = structuredClone(item);
|
|
3069
|
+
recipientItem.admissionReceipts = [
|
|
3070
|
+
{ ...receipt, journalBindingKey: recipientJournalBindingKey },
|
|
3071
|
+
];
|
|
3072
|
+
const expectedOwner = deps.getReceiptOwner(receipt);
|
|
3073
|
+
const lifecycle = deps.getLifecycleForReceipt(receipt);
|
|
3074
|
+
if (!expectedOwner ||
|
|
3075
|
+
!lifecycle ||
|
|
3076
|
+
!recipient.registrationGeneration ||
|
|
3077
|
+
!recipient.pid ||
|
|
3078
|
+
!recipient.processBirthId ||
|
|
3079
|
+
!recipient.sessionGeneration) {
|
|
3080
|
+
continue;
|
|
3081
|
+
}
|
|
3082
|
+
const handoffToken = deps.createHandoffToken();
|
|
3083
|
+
const result = await coordinateTelegramQueueHandoff({
|
|
3084
|
+
item,
|
|
3085
|
+
expectedOwner,
|
|
3086
|
+
recipientOwner: {
|
|
3087
|
+
instanceId: recipient.instanceId,
|
|
3088
|
+
processId: recipient.pid,
|
|
3089
|
+
processBirthId: recipient.processBirthId,
|
|
3090
|
+
sessionGeneration: recipient.sessionGeneration,
|
|
3091
|
+
},
|
|
3092
|
+
handoffToken,
|
|
3093
|
+
lifecycle,
|
|
3094
|
+
stageRemote: async () => {
|
|
3095
|
+
const payload = createTelegramQueueHandoff({
|
|
3096
|
+
handoffToken,
|
|
3097
|
+
item: recipientItem,
|
|
3098
|
+
}).payload;
|
|
3099
|
+
if (followerRegistered) {
|
|
3100
|
+
return deps.stageThroughFollower({
|
|
3101
|
+
recipient,
|
|
3102
|
+
expectedOwner,
|
|
3103
|
+
handoffToken,
|
|
3104
|
+
payload,
|
|
3105
|
+
});
|
|
3106
|
+
}
|
|
3107
|
+
const response = await deps.routeThroughLeader({
|
|
3108
|
+
requestId: deps.createRequestId(),
|
|
3109
|
+
auth: deps.authSecret,
|
|
3110
|
+
recipientInstanceId: recipient.instanceId,
|
|
3111
|
+
recipientRegistrationGeneration: recipient.registrationGeneration,
|
|
3112
|
+
donorInstanceId: deps.donorInstanceId,
|
|
3113
|
+
donorProcessId: expectedOwner.processId,
|
|
3114
|
+
donorProcessBirthId: expectedOwner.processBirthId,
|
|
3115
|
+
donorSessionGeneration: expectedOwner.sessionGeneration,
|
|
3116
|
+
donorAcquisitionId: expectedOwner.acquisitionId,
|
|
3117
|
+
donorAcquiredAtMs: expectedOwner.acquiredAtMs,
|
|
3118
|
+
handoffToken,
|
|
3119
|
+
payload,
|
|
3120
|
+
sentAtMs: Date.now(),
|
|
3121
|
+
});
|
|
3122
|
+
const queueOwner = response.kind === "bus.ack" &&
|
|
3123
|
+
response.result &&
|
|
3124
|
+
typeof response.result === "object"
|
|
3125
|
+
? parseTelegramUpdateJournalQueueOwner(response.result.queueOwner)
|
|
3126
|
+
: undefined;
|
|
3127
|
+
if (response.kind !== "bus.ack" ||
|
|
3128
|
+
!response.ok ||
|
|
3129
|
+
!response.result ||
|
|
3130
|
+
typeof response.result !== "object" ||
|
|
3131
|
+
!queueOwner) {
|
|
3132
|
+
throw new Error(response.kind === "bus.ack"
|
|
3133
|
+
? response.message ?? "Telegram queue handoff was rejected."
|
|
3134
|
+
: "Telegram queue handoff returned no acknowledgement.");
|
|
3135
|
+
}
|
|
3136
|
+
return {
|
|
3137
|
+
...response.result,
|
|
3138
|
+
queueOwner,
|
|
3139
|
+
};
|
|
3140
|
+
},
|
|
3141
|
+
removeDonorItem: (exactReceipt) => deps.removeDonorItem(exactReceipt, ctx),
|
|
3142
|
+
});
|
|
3143
|
+
if (result.status === "retained") {
|
|
3144
|
+
deps.recordFailure?.(result.error, {
|
|
3145
|
+
phase: "queue-handoff-retained",
|
|
3146
|
+
receiptId: result.receipt.receiptId,
|
|
3147
|
+
recipientInstanceId: recipient.instanceId,
|
|
3148
|
+
cancelled: result.cancelled,
|
|
3149
|
+
});
|
|
3150
|
+
}
|
|
3151
|
+
}
|
|
3152
|
+
};
|
|
3153
|
+
return (ctx) => {
|
|
3154
|
+
if (operation)
|
|
3155
|
+
return operation;
|
|
3156
|
+
const current = reconcile(ctx).finally(() => {
|
|
3157
|
+
if (operation === current)
|
|
3158
|
+
operation = undefined;
|
|
3159
|
+
});
|
|
3160
|
+
operation = current;
|
|
3161
|
+
return current;
|
|
3162
|
+
};
|
|
3163
|
+
}
|
|
3164
|
+
export function createTelegramUpdateWorkerOwnerRuntime(deps) {
|
|
3165
|
+
return {
|
|
3166
|
+
getQueueOwnerIdentity() {
|
|
3167
|
+
return {
|
|
3168
|
+
instanceId: deps.instanceId,
|
|
3169
|
+
processId: deps.processId,
|
|
3170
|
+
processBirthId: deps.processBirthId,
|
|
3171
|
+
sessionGeneration: deps.getSessionGeneration(),
|
|
3172
|
+
};
|
|
3173
|
+
},
|
|
3174
|
+
onQueueReceiptCommitted(_receipt, ctx) {
|
|
3175
|
+
if (!deps.isContextCurrent(ctx))
|
|
3176
|
+
return;
|
|
3177
|
+
deps.dispatchNext(ctx);
|
|
3178
|
+
deps.requestQueueHandoffReconciliation(ctx);
|
|
3179
|
+
},
|
|
3180
|
+
onUpdateCompleted(_updateId, ctx) {
|
|
3181
|
+
if (deps.isContextCurrent(ctx))
|
|
3182
|
+
deps.dispatchNext(ctx);
|
|
3183
|
+
},
|
|
3184
|
+
};
|
|
3185
|
+
}
|
|
3186
|
+
export function createTelegramUpdateAdmissionRuntimeBinding(deps) {
|
|
3187
|
+
let leader;
|
|
3188
|
+
let follower;
|
|
3189
|
+
let settlement;
|
|
3190
|
+
let inputCustodyBus;
|
|
3191
|
+
return {
|
|
3192
|
+
bind(input) {
|
|
3193
|
+
leader = input.leader;
|
|
3194
|
+
follower = input.follower;
|
|
3195
|
+
settlement = createTelegramQueueAdmissionSettlementMuxRuntime([
|
|
3196
|
+
leader,
|
|
3197
|
+
follower,
|
|
3198
|
+
]);
|
|
3199
|
+
inputCustodyBus = input.inputCustodyBus;
|
|
3200
|
+
},
|
|
3201
|
+
getLeader: () => leader,
|
|
3202
|
+
getFollower: () => follower,
|
|
3203
|
+
getActive: () => (deps.isFollowerRegistered() ? follower : leader),
|
|
3204
|
+
getSettlement: () => settlement,
|
|
3205
|
+
getInputCustodyBus: () => inputCustodyBus,
|
|
3206
|
+
getLifecycleForJournalBinding(journalBindingKey) {
|
|
3207
|
+
if (follower?.ownsJournalBinding(journalBindingKey))
|
|
3208
|
+
return follower;
|
|
3209
|
+
return leader?.ownsJournalBinding(journalBindingKey) ? leader : undefined;
|
|
3210
|
+
},
|
|
3211
|
+
hasPendingQueueMutationForItem(item) {
|
|
3212
|
+
return Boolean(leader?.hasPendingQueueMutationForItem(item) ||
|
|
3213
|
+
follower?.hasPendingQueueMutationForItem(item));
|
|
3214
|
+
},
|
|
3215
|
+
async onSessionShutdown() {
|
|
3216
|
+
await Promise.all([
|
|
3217
|
+
leader?.onSessionShutdown(),
|
|
3218
|
+
follower?.onSessionShutdown(),
|
|
3219
|
+
]);
|
|
3220
|
+
},
|
|
3221
|
+
};
|
|
3222
|
+
}
|
|
3223
|
+
function isTelegramReactionDependencyForQueueItem(update, item) {
|
|
3224
|
+
const reaction = update.message_reaction;
|
|
3225
|
+
if (!isTelegramUpdateAdmissionRecord(reaction))
|
|
3226
|
+
return false;
|
|
3227
|
+
const chat = reaction.chat;
|
|
3228
|
+
if (!isTelegramUpdateAdmissionRecord(chat) ||
|
|
3229
|
+
!Number.isSafeInteger(chat.id) ||
|
|
3230
|
+
!Number.isSafeInteger(reaction.message_id)) {
|
|
3231
|
+
return false;
|
|
3232
|
+
}
|
|
3233
|
+
const itemMessageIds = new Set([
|
|
3234
|
+
item.replyToMessageId,
|
|
3235
|
+
...(item.sourceMessageIds ?? []),
|
|
3236
|
+
]);
|
|
3237
|
+
return (chat.id === (item.target?.chatId ?? item.chatId) &&
|
|
3238
|
+
itemMessageIds.has(reaction.message_id));
|
|
3239
|
+
}
|
|
3240
|
+
/** Own one worker per active transport identity without assuming queued-owner death. */
|
|
3241
|
+
export function createTelegramUpdateAdmissionLifecycleRuntime(deps) {
|
|
3242
|
+
let activeRuntimeKey;
|
|
3243
|
+
let journal;
|
|
3244
|
+
let worker;
|
|
3245
|
+
let settlement;
|
|
3246
|
+
let journalBindingKey;
|
|
3247
|
+
let operation = Promise.resolve();
|
|
3248
|
+
let foreignQueueOwnerLiveness;
|
|
3249
|
+
let releaseSourceReference;
|
|
3250
|
+
const stopCurrent = async (forget) => {
|
|
3251
|
+
await worker?.stop();
|
|
3252
|
+
const release = releaseSourceReference;
|
|
3253
|
+
releaseSourceReference = undefined;
|
|
3254
|
+
try {
|
|
3255
|
+
release?.();
|
|
3256
|
+
}
|
|
3257
|
+
catch (error) {
|
|
3258
|
+
deps.recordRuntimeEvent?.("inbound-worker", error, { phase: "source-reference-release" });
|
|
3259
|
+
}
|
|
3260
|
+
if (!forget)
|
|
3261
|
+
return;
|
|
3262
|
+
journal = undefined;
|
|
3263
|
+
worker = undefined;
|
|
3264
|
+
settlement = undefined;
|
|
3265
|
+
journalBindingKey = undefined;
|
|
3266
|
+
activeRuntimeKey = undefined;
|
|
3267
|
+
foreignQueueOwnerLiveness = undefined;
|
|
3268
|
+
};
|
|
3269
|
+
const bind = async (ctx, replace = false) => {
|
|
3270
|
+
let binding = deps.resolveBinding();
|
|
3271
|
+
if (!binding) {
|
|
3272
|
+
await stopCurrent(true);
|
|
3273
|
+
return;
|
|
3274
|
+
}
|
|
3275
|
+
if (replace || !worker || activeRuntimeKey !== binding.runtimeKey) {
|
|
3276
|
+
const { runtimeKey, recoveryKey } = binding;
|
|
3277
|
+
await stopCurrent(true);
|
|
3278
|
+
binding = deps.resolveBinding();
|
|
3279
|
+
if (!binding || binding.runtimeKey !== runtimeKey ||
|
|
3280
|
+
binding.recoveryKey !== recoveryKey) {
|
|
3281
|
+
throw new Error("Telegram update source binding changed during startup.");
|
|
3282
|
+
}
|
|
3283
|
+
const release = deps.acquireSourceReference?.(binding);
|
|
3284
|
+
try {
|
|
3285
|
+
journal = binding.journal;
|
|
3286
|
+
worker = deps.createWorker(binding.journal, binding);
|
|
3287
|
+
settlement = createTelegramQueueAdmissionSettlementRuntime(worker);
|
|
3288
|
+
releaseSourceReference = release;
|
|
3289
|
+
}
|
|
3290
|
+
catch (error) {
|
|
3291
|
+
release?.();
|
|
3292
|
+
throw error;
|
|
3293
|
+
}
|
|
3294
|
+
journalBindingKey = binding.recoveryKey;
|
|
3295
|
+
activeRuntimeKey = binding.runtimeKey;
|
|
3296
|
+
}
|
|
3297
|
+
if (!releaseSourceReference && deps.acquireSourceReference)
|
|
3298
|
+
releaseSourceReference = deps.acquireSourceReference(binding);
|
|
3299
|
+
if (binding.journal.applyOperatorDisposition) {
|
|
3300
|
+
for (const entry of binding.journal.read().entries) {
|
|
3301
|
+
if (entry.state !== "failed" || !entry.terminalFailureId)
|
|
3302
|
+
continue;
|
|
3303
|
+
const result = binding.journal.applyOperatorDisposition({
|
|
3304
|
+
action: "retry",
|
|
3305
|
+
updateId: entry.updateId,
|
|
3306
|
+
failureId: entry.terminalFailureId,
|
|
3307
|
+
});
|
|
3308
|
+
if (result.duplicate)
|
|
3309
|
+
continue;
|
|
3310
|
+
try {
|
|
3311
|
+
deps.recordRuntimeEvent?.("inbound-worker", "Resumed legacy terminal update under automatic retry policy.", {
|
|
3312
|
+
phase: "automatic-terminal-retry",
|
|
3313
|
+
updateId: entry.updateId,
|
|
3314
|
+
attemptCount: result.disposition.attemptCount,
|
|
3315
|
+
});
|
|
3316
|
+
}
|
|
3317
|
+
catch {
|
|
3318
|
+
// Diagnostics cannot revoke the committed retry.
|
|
3319
|
+
}
|
|
3320
|
+
}
|
|
3321
|
+
}
|
|
3322
|
+
if (deps.getQueueOwnerIdentity && binding.journal.recoverDeadQueueOwner) {
|
|
3323
|
+
const recoveryOwner = deps.getQueueOwnerIdentity(ctx);
|
|
3324
|
+
const foreignReceipts = new Map();
|
|
3325
|
+
for (const entry of binding.journal.read().entries) {
|
|
3326
|
+
if (entry.state !== "queued" ||
|
|
3327
|
+
!entry.queueKind ||
|
|
3328
|
+
!entry.queueReceiptId ||
|
|
3329
|
+
!entry.queueOwner ||
|
|
3330
|
+
entry.queueHandoff ||
|
|
3331
|
+
isTelegramUpdateJournalQueueOwnerProcess(entry.queueOwner, recoveryOwner)) {
|
|
3332
|
+
continue;
|
|
3333
|
+
}
|
|
3334
|
+
const receipt = foreignReceipts.get(entry.queueReceiptId);
|
|
3335
|
+
if (receipt)
|
|
3336
|
+
receipt.sourceUpdateIds.push(entry.updateId);
|
|
3337
|
+
else {
|
|
3338
|
+
foreignReceipts.set(entry.queueReceiptId, {
|
|
3339
|
+
queueKind: entry.queueKind,
|
|
3340
|
+
sourceUpdateIds: [entry.updateId],
|
|
3341
|
+
deadOwner: { ...entry.queueOwner },
|
|
3342
|
+
});
|
|
3343
|
+
}
|
|
3344
|
+
}
|
|
3345
|
+
for (const [receiptId, receipt] of foreignReceipts) {
|
|
3346
|
+
const result = binding.journal.recoverDeadQueueOwner({
|
|
3347
|
+
queueKind: receipt.queueKind,
|
|
3348
|
+
receiptId,
|
|
3349
|
+
sourceUpdateIds: receipt.sourceUpdateIds,
|
|
3350
|
+
deadOwner: receipt.deadOwner,
|
|
3351
|
+
recoveryOwner,
|
|
3352
|
+
});
|
|
3353
|
+
foreignQueueOwnerLiveness =
|
|
3354
|
+
result.status === "recovered"
|
|
3355
|
+
? "dead"
|
|
3356
|
+
: result.status === "owner-alive"
|
|
3357
|
+
? "alive"
|
|
3358
|
+
: "unverifiable";
|
|
3359
|
+
if (result.status !== "recovered")
|
|
3360
|
+
continue;
|
|
3361
|
+
try {
|
|
3362
|
+
deps.recordRuntimeEvent?.("inbound-worker", "Discarded session-owned queue authority from a confirmed-dead process.", {
|
|
3363
|
+
phase: "dead-queue-owner-cleanup",
|
|
3364
|
+
receiptId,
|
|
3365
|
+
removedUpdateCount: result.recoveredUpdateIds.length,
|
|
3366
|
+
});
|
|
3367
|
+
}
|
|
3368
|
+
catch {
|
|
3369
|
+
// Diagnostics cannot revoke the committed recovery.
|
|
3370
|
+
}
|
|
3371
|
+
}
|
|
3372
|
+
}
|
|
3373
|
+
worker.start(ctx);
|
|
3374
|
+
};
|
|
3375
|
+
const runExclusive = (task) => {
|
|
3376
|
+
const next = operation.then(task, task);
|
|
3377
|
+
operation = next.catch(() => undefined);
|
|
3378
|
+
return next;
|
|
3379
|
+
};
|
|
3380
|
+
return {
|
|
3381
|
+
onSessionStart: (ctx) => runExclusive(() => bind(ctx)),
|
|
3382
|
+
onSessionShutdown: () => runExclusive(() => stopCurrent(false)),
|
|
3383
|
+
onTransportChanged: (ctx) => runExclusive(async () => {
|
|
3384
|
+
if (ctx !== undefined)
|
|
3385
|
+
await bind(ctx, true);
|
|
3386
|
+
else
|
|
3387
|
+
await stopCurrent(true);
|
|
3388
|
+
}),
|
|
3389
|
+
appendBatch(updates, acceptedThroughUpdateId) {
|
|
3390
|
+
if (!journal || !worker) {
|
|
3391
|
+
throw new Error("Telegram update admission worker is not active.");
|
|
3392
|
+
}
|
|
3393
|
+
return journal.appendBatch(updates, acceptedThroughUpdateId);
|
|
3394
|
+
},
|
|
3395
|
+
discardQueueReceipt(input) {
|
|
3396
|
+
if (!journal || !worker || !journal.discardQueued) {
|
|
3397
|
+
throw new Error("Telegram update journal queue discard is not available.");
|
|
3398
|
+
}
|
|
3399
|
+
const result = journal.discardQueued(input);
|
|
3400
|
+
worker.signal();
|
|
3401
|
+
return result;
|
|
3402
|
+
},
|
|
3403
|
+
recoverDeadQueueReceipt(input) {
|
|
3404
|
+
if (!journal || !worker || !journal.recoverDeadQueueOwner) {
|
|
3405
|
+
throw new Error("Telegram update journal dead-owner recovery is not available.");
|
|
3406
|
+
}
|
|
3407
|
+
const result = journal.recoverDeadQueueOwner(input);
|
|
3408
|
+
worker.signal();
|
|
3409
|
+
return result;
|
|
3410
|
+
},
|
|
3411
|
+
offerQueueReceiptHandoff(input) {
|
|
3412
|
+
if (!journal || !worker || !journal.offerQueuedHandoff) {
|
|
3413
|
+
throw new Error("Telegram update journal queue handoff offer is not available.");
|
|
3414
|
+
}
|
|
3415
|
+
const result = journal.offerQueuedHandoff(input);
|
|
3416
|
+
worker.signal();
|
|
3417
|
+
return result;
|
|
3418
|
+
},
|
|
3419
|
+
acceptQueueReceiptHandoff(input) {
|
|
3420
|
+
if (!journal || !worker || !journal.acceptQueuedHandoff) {
|
|
3421
|
+
throw new Error("Telegram update journal queue handoff acceptance is not available.");
|
|
3422
|
+
}
|
|
3423
|
+
const result = journal.acceptQueuedHandoff(input);
|
|
3424
|
+
worker.signal();
|
|
3425
|
+
return result;
|
|
3426
|
+
},
|
|
3427
|
+
cancelQueueReceiptHandoff(input) {
|
|
3428
|
+
if (!journal || !worker || !journal.cancelQueuedHandoff) {
|
|
3429
|
+
throw new Error("Telegram update journal queue handoff cancellation is not available.");
|
|
3430
|
+
}
|
|
3431
|
+
const result = journal.cancelQueuedHandoff(input);
|
|
3432
|
+
worker.signal();
|
|
3433
|
+
return result;
|
|
3434
|
+
},
|
|
3435
|
+
async publishAcceptedQueueReceipt(input) {
|
|
3436
|
+
if (!worker || !journal) {
|
|
3437
|
+
throw new Error("Telegram update admission worker is not active.");
|
|
3438
|
+
}
|
|
3439
|
+
const entry = journal
|
|
3440
|
+
.read()
|
|
3441
|
+
.entries.find((candidate) => candidate.state === "queued" &&
|
|
3442
|
+
candidate.queueReceiptId === input.receipt.receiptId &&
|
|
3443
|
+
candidate.queueOwner?.acquisitionId ===
|
|
3444
|
+
input.queueOwner.acquisitionId);
|
|
3445
|
+
if (!entry) {
|
|
3446
|
+
throw new Error(`Telegram queue handoff receipt ${input.receipt.receiptId} is not owned by this journal.`);
|
|
3447
|
+
}
|
|
3448
|
+
worker.signal();
|
|
3449
|
+
await worker.waitForDrain();
|
|
3450
|
+
const currentOwner = worker.getQueueReceiptOwner(input.receipt);
|
|
3451
|
+
if (!currentOwner ||
|
|
3452
|
+
!areTelegramUpdateJournalQueueOwnersEqual(currentOwner, input.queueOwner)) {
|
|
3453
|
+
throw new Error(`Telegram queue handoff receipt ${input.receipt.receiptId} is not owned by this runtime.`);
|
|
3454
|
+
}
|
|
3455
|
+
},
|
|
3456
|
+
getQueueReceiptOwner(receipt) {
|
|
3457
|
+
if (!journalBindingKey ||
|
|
3458
|
+
receipt.journalBindingKey !== journalBindingKey) {
|
|
3459
|
+
return undefined;
|
|
3460
|
+
}
|
|
3461
|
+
return worker?.getQueueReceiptOwner(receipt);
|
|
3462
|
+
},
|
|
3463
|
+
getJournalBindingKey: () => journalBindingKey,
|
|
3464
|
+
getJournalPath: () => journalBindingKey
|
|
3465
|
+
? getTelegramUpdateJournalBindingPath(journalBindingKey)
|
|
3466
|
+
: undefined,
|
|
3467
|
+
ownsJournalBinding: (candidate) => journalBindingKey !== undefined && journalBindingKey === candidate,
|
|
3468
|
+
getForeignQueueOwnerLiveness: () => foreignQueueOwnerLiveness,
|
|
3469
|
+
getJournalEntryCount() {
|
|
3470
|
+
if (!journal || !worker) {
|
|
3471
|
+
throw new Error("Telegram update admission worker is not active.");
|
|
3472
|
+
}
|
|
3473
|
+
return journal.read().entries.length;
|
|
3474
|
+
},
|
|
3475
|
+
hasPendingQueueMutationForItem(item) {
|
|
3476
|
+
return Boolean(journal &&
|
|
3477
|
+
worker &&
|
|
3478
|
+
journal
|
|
3479
|
+
.read()
|
|
3480
|
+
.entries.some((entry) => entry.state !== "queued" &&
|
|
3481
|
+
isTelegramReactionDependencyForQueueItem(entry.update, item)));
|
|
3482
|
+
},
|
|
3483
|
+
signal: () => worker?.signal(),
|
|
3484
|
+
getState: () => {
|
|
3485
|
+
const state = worker?.getState();
|
|
3486
|
+
if (!state)
|
|
3487
|
+
return undefined;
|
|
3488
|
+
return {
|
|
3489
|
+
...state,
|
|
3490
|
+
...(state.foreignQueuedOwner && foreignQueueOwnerLiveness
|
|
3491
|
+
? { foreignQueuedOwnerLiveness: foreignQueueOwnerLiveness }
|
|
3492
|
+
: {}),
|
|
3493
|
+
};
|
|
3494
|
+
},
|
|
3495
|
+
isItemReady: (item) => settlement?.isItemReady(item) ??
|
|
3496
|
+
(item.admissionReceipts?.length ?? 0) === 0,
|
|
3497
|
+
onPromptHandedOff: (item, ctx) => settlement?.onPromptHandedOff(item, ctx),
|
|
3498
|
+
onControlSettled: (item, ctx) => settlement?.onControlSettled(item, ctx),
|
|
3499
|
+
onItemsDiscarded: (items, ctx) => settlement?.onItemsDiscarded(items, ctx),
|
|
3500
|
+
};
|
|
3501
|
+
}
|
|
3502
|
+
/** Compose source-bound routing and late grouped settlement under one worker. */
|
|
3503
|
+
export function createTelegramUpdateAdmissionWorkerRuntime(deps) {
|
|
3504
|
+
let worker;
|
|
3505
|
+
const executeUpdate = createTelegramUpdateAdmissionHandle({
|
|
3506
|
+
defaultHandle: deps.defaultHandle,
|
|
3507
|
+
registry: deps.registry,
|
|
3508
|
+
onLateOutcome(outcome, details) {
|
|
3509
|
+
worker?.settleDeferred({
|
|
3510
|
+
updateId: details.updateId,
|
|
3511
|
+
outcome,
|
|
3512
|
+
signal: details.signal,
|
|
3513
|
+
});
|
|
3514
|
+
},
|
|
3515
|
+
onLateOutcomeError(error, updateId) {
|
|
3516
|
+
deps.recordRuntimeEvent?.("inbound-worker", error, {
|
|
3517
|
+
phase: "late-admission-handler",
|
|
3518
|
+
updateId,
|
|
3519
|
+
});
|
|
3520
|
+
},
|
|
3521
|
+
});
|
|
3522
|
+
const custodyBindingKey = deps.inputCustody ? deps.getRecipientBindingKey?.() : undefined;
|
|
3523
|
+
if (deps.inputCustody && !custodyBindingKey)
|
|
3524
|
+
throw new Error("Telegram custodied admission worker requires one recipient binding key.");
|
|
3525
|
+
const executeCustodiedUpdate = deps.inputCustody && custodyBindingKey
|
|
3526
|
+
? createTelegramCustodiedUpdateAdmissionHandle({
|
|
3527
|
+
journal: deps.inputCustody, recipientBindingKey: custodyBindingKey,
|
|
3528
|
+
defaultHandle: deps.defaultHandle, registry: deps.registry,
|
|
3529
|
+
onLateOutcomeError(error, updateId) {
|
|
3530
|
+
deps.recordRuntimeEvent?.("inbound-worker", error, { phase: "late-custodied-admission-handler", updateId });
|
|
3531
|
+
},
|
|
3532
|
+
onCustodiedLateSettlement(result, details) {
|
|
3533
|
+
worker?.settleCustodied({ updateId: details.updateId, result, signal: details.signal });
|
|
3534
|
+
},
|
|
3535
|
+
}) : undefined;
|
|
3536
|
+
worker = createTelegramUpdateWorkerRuntime({
|
|
3537
|
+
...deps,
|
|
3538
|
+
...(executeCustodiedUpdate ? { executeCustodiedUpdate: async (update, ctx, signal) => executeCustodiedUpdate(deps.prepareUpdateForExecution?.(update) ?? update, ctx, signal) } : {}),
|
|
3539
|
+
async executeUpdate(update, ctx, signal) {
|
|
3540
|
+
const typedUpdate = update;
|
|
3541
|
+
try {
|
|
3542
|
+
return await executeUpdate(deps.prepareUpdateForExecution?.(typedUpdate) ?? typedUpdate, ctx, signal);
|
|
3543
|
+
}
|
|
3544
|
+
catch (error) {
|
|
3545
|
+
if (await deps.settleTerminalExecutionFailure?.(error)) {
|
|
3546
|
+
return { kind: "complete" };
|
|
3547
|
+
}
|
|
3548
|
+
throw error;
|
|
3549
|
+
}
|
|
3550
|
+
},
|
|
3551
|
+
});
|
|
3552
|
+
return worker;
|
|
3553
|
+
}
|
|
3554
|
+
/** Own leader/follower journal lifecycle construction and generation fencing. */
|
|
3555
|
+
export function createTelegramUpdateAdmissionLifecycleAssembly(deps) {
|
|
3556
|
+
const leader = createTelegramUpdateAdmissionLifecycleRuntime({
|
|
3557
|
+
resolveBinding: deps.leader.resolveBinding,
|
|
3558
|
+
...(deps.acquireSourceReference ? { acquireSourceReference: binding => deps.acquireSourceReference("leader", binding) } : {}),
|
|
3559
|
+
getQueueOwnerIdentity: deps.worker.getQueueOwnerIdentity,
|
|
3560
|
+
createWorker(journal, binding) {
|
|
3561
|
+
return createTelegramUpdateAdmissionWorkerRuntime({
|
|
3562
|
+
...deps.worker,
|
|
3563
|
+
journal,
|
|
3564
|
+
...(binding.journal.inputCustody ? { inputCustody: binding.journal.inputCustody } : {}),
|
|
3565
|
+
getJournalBindingKey: () => binding.recoveryKey,
|
|
3566
|
+
getRecipientBindingKey: () => binding.recipientBindingKey,
|
|
3567
|
+
hasAuthority: deps.leader.hasAuthority,
|
|
3568
|
+
});
|
|
3569
|
+
},
|
|
3570
|
+
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
3571
|
+
});
|
|
3572
|
+
const follower = createTelegramUpdateAdmissionLifecycleRuntime({
|
|
3573
|
+
getQueueOwnerIdentity: deps.worker.getQueueOwnerIdentity,
|
|
3574
|
+
...(deps.acquireSourceReference ? { acquireSourceReference: binding => deps.acquireSourceReference("follower", binding) } : {}),
|
|
3575
|
+
resolveBinding() {
|
|
3576
|
+
const generation = deps.follower.getGeneration();
|
|
3577
|
+
if (!deps.follower.isRegistered() || !generation)
|
|
3578
|
+
return undefined;
|
|
3579
|
+
const binding = deps.follower.resolveBinding();
|
|
3580
|
+
if (!binding)
|
|
3581
|
+
return undefined;
|
|
3582
|
+
return {
|
|
3583
|
+
...binding,
|
|
3584
|
+
runtimeKey: `${binding.runtimeKey}\u0000${generation}`,
|
|
3585
|
+
hasAuthority() {
|
|
3586
|
+
return (deps.follower.isRegistered() &&
|
|
3587
|
+
deps.follower.getGeneration() === generation);
|
|
3588
|
+
},
|
|
3589
|
+
};
|
|
3590
|
+
},
|
|
3591
|
+
createWorker(journal, binding) {
|
|
3592
|
+
return createTelegramUpdateAdmissionWorkerRuntime({
|
|
3593
|
+
...deps.worker,
|
|
3594
|
+
journal,
|
|
3595
|
+
...(binding.journal.inputCustody ? { inputCustody: binding.journal.inputCustody } : {}),
|
|
3596
|
+
getJournalBindingKey: () => binding.recoveryKey,
|
|
3597
|
+
getRecipientBindingKey: () => binding.recipientBindingKey,
|
|
3598
|
+
hasAuthority: () => binding.hasAuthority?.() ?? false,
|
|
3599
|
+
prepareUpdateForExecution: deps.follower.prepareUpdateForExecution,
|
|
3600
|
+
});
|
|
3601
|
+
},
|
|
3602
|
+
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
3603
|
+
});
|
|
3604
|
+
deps.runtimeBinding.bind({ leader, follower,
|
|
3605
|
+
...(deps.inputCustodyBus ? { inputCustodyBus: deps.inputCustodyBus } : {}) });
|
|
3606
|
+
return { leader, follower };
|
|
3607
|
+
}
|
|
3608
|
+
/** Own queue-owner projection and shared leader/follower worker composition. */
|
|
3609
|
+
export function createTelegramUpdateAdmissionRuntimeAssembly(deps) {
|
|
3610
|
+
const owner = createTelegramUpdateWorkerOwnerRuntime(deps.owner);
|
|
3611
|
+
const lifecycle = createTelegramUpdateAdmissionLifecycleAssembly({
|
|
3612
|
+
runtimeBinding: deps.runtimeBinding,
|
|
3613
|
+
worker: {
|
|
3614
|
+
...deps.worker,
|
|
3615
|
+
...owner,
|
|
3616
|
+
isContextCurrent: deps.owner.isContextCurrent,
|
|
3617
|
+
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
3618
|
+
},
|
|
3619
|
+
leader: deps.leader,
|
|
3620
|
+
follower: deps.follower,
|
|
3621
|
+
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
3622
|
+
});
|
|
3623
|
+
return { owner, ...lifecycle };
|
|
3624
|
+
}
|
|
3625
|
+
/**
|
|
3626
|
+
* Register a handler that runs before pi-telegram routes a Telegram update
|
|
3627
|
+
* through its built-in handlers.
|
|
3628
|
+
*
|
|
3629
|
+
* This is the low-level public surface for extensions that share the same bot
|
|
3630
|
+
* and Pi process with pi-telegram.
|
|
3631
|
+
*/
|
|
3632
|
+
export function registerTelegramUpdateHandler(handler) {
|
|
3633
|
+
return getOrCreateUpdateHandlerRegistry().add(handler);
|
|
3634
|
+
}
|