@llblab/pi-telegram 0.52.2 → 0.53.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +9 -7
- package/BACKLOG.md +1 -18
- package/CHANGELOG.md +15 -0
- package/README.md +7 -11
- package/api/commands.ts +3 -0
- package/dist/api/commands.d.ts +1 -1
- package/dist/lib/activity-verbosity.d.ts +1 -5
- package/dist/lib/activity-verbosity.js +15 -42
- package/dist/lib/activity.d.ts +9 -0
- package/dist/lib/activity.js +39 -10
- package/dist/lib/agent-messages.js +7 -3
- package/dist/lib/bindings.d.ts +43 -5
- package/dist/lib/bindings.js +163 -20
- package/dist/lib/bus-api.d.ts +12 -0
- package/dist/lib/bus-api.js +182 -79
- package/dist/lib/bus-follower.d.ts +111 -31
- package/dist/lib/bus-follower.js +1324 -359
- package/dist/lib/bus-leader.d.ts +41 -91
- package/dist/lib/bus-leader.js +969 -637
- package/dist/lib/bus-transport.js +16 -9
- package/dist/lib/bus.d.ts +215 -18
- package/dist/lib/bus.js +1048 -327
- package/dist/lib/channel-posts.d.ts +5 -0
- package/dist/lib/channel-posts.js +434 -261
- package/dist/lib/command-templates.d.ts +26 -5
- package/dist/lib/command-templates.js +92 -176
- package/dist/lib/commands.d.ts +168 -82
- package/dist/lib/commands.js +1409 -291
- package/dist/lib/config.d.ts +1 -8
- package/dist/lib/config.js +34 -76
- package/dist/lib/delivery.d.ts +2 -2
- package/dist/lib/delivery.js +16 -16
- package/dist/lib/extension.js +639 -171
- package/dist/lib/generative-app-worker.mjs +19 -7
- package/dist/lib/generative-apps.d.ts +0 -5
- package/dist/lib/generative-apps.js +61 -49
- package/dist/lib/inbound.d.ts +5 -0
- package/dist/lib/inbound.js +35 -59
- package/dist/lib/journal.d.ts +33 -3
- package/dist/lib/journal.js +2063 -782
- package/dist/lib/lifecycle.d.ts +2 -6
- package/dist/lib/lifecycle.js +75 -58
- package/dist/lib/locks.d.ts +29 -3
- package/dist/lib/locks.js +289 -108
- package/dist/lib/logging.js +50 -14
- package/dist/lib/media.d.ts +22 -1
- package/dist/lib/media.js +46 -28
- package/dist/lib/menu-model.d.ts +19 -5
- package/dist/lib/menu-model.js +53 -11
- package/dist/lib/menu-queue.d.ts +5 -4
- package/dist/lib/menu-queue.js +45 -63
- package/dist/lib/menu-settings.d.ts +5 -4
- package/dist/lib/menu-settings.js +114 -78
- package/dist/lib/menu-status.d.ts +1 -1
- package/dist/lib/menu-status.js +8 -25
- package/dist/lib/menu-thinking.d.ts +6 -1
- package/dist/lib/menu-thinking.js +14 -21
- package/dist/lib/menu.d.ts +8 -61
- package/dist/lib/menu.js +92 -164
- package/dist/lib/model.d.ts +1 -1
- package/dist/lib/model.js +2 -16
- package/dist/lib/outbound-attachments.d.ts +6 -27
- package/dist/lib/outbound-attachments.js +45 -38
- package/dist/lib/outbound-buttons.js +22 -16
- package/dist/lib/outbound-markup.d.ts +0 -2
- package/dist/lib/outbound-markup.js +51 -64
- package/dist/lib/outbound-voice.js +6 -3
- package/dist/lib/outbound.d.ts +6 -6
- package/dist/lib/outbound.js +65 -51
- package/dist/lib/ownership.js +6 -2
- package/dist/lib/paths.d.ts +5 -0
- package/dist/lib/paths.js +37 -21
- package/dist/lib/pi.d.ts +2 -0
- package/dist/lib/pi.js +8 -1
- package/dist/lib/polling.d.ts +21 -37
- package/dist/lib/polling.js +59 -50
- package/dist/lib/preview.js +39 -19
- package/dist/lib/process-identity.d.ts +24 -0
- package/dist/lib/process-identity.js +133 -0
- package/dist/lib/prompts.js +2 -7
- package/dist/lib/queue.d.ts +14 -16
- package/dist/lib/queue.js +252 -127
- package/dist/lib/recovery.d.ts +2 -83
- package/dist/lib/recovery.js +20 -274
- package/dist/lib/replies.d.ts +16 -24
- package/dist/lib/replies.js +99 -116
- package/dist/lib/routing.d.ts +120 -7
- package/dist/lib/routing.js +4667 -1152
- package/dist/lib/runtime.d.ts +1 -11
- package/dist/lib/sections.d.ts +4 -29
- package/dist/lib/sections.js +7 -20
- package/dist/lib/skills.js +4 -3
- package/dist/lib/status.js +86 -57
- package/dist/lib/sync.d.ts +1 -3
- package/dist/lib/sync.js +67 -43
- package/dist/lib/target.d.ts +4 -0
- package/dist/lib/target.js +18 -0
- package/dist/lib/telegram-api.d.ts +18 -13
- package/dist/lib/telegram-api.js +148 -53
- package/dist/lib/text-groups.js +4 -39
- package/dist/lib/thread-cleanup-manager.d.ts +6 -27
- package/dist/lib/thread-cleanup-manager.js +369 -186
- package/dist/lib/thread-display.d.ts +1 -2
- package/dist/lib/thread-display.js +58 -38
- package/dist/lib/thread-naming.d.ts +28 -8
- package/dist/lib/thread-naming.js +175 -66
- package/dist/lib/thread-reconciler.d.ts +42 -16
- package/dist/lib/thread-reconciler.js +90 -50
- package/dist/lib/threads.d.ts +82 -20
- package/dist/lib/threads.js +2247 -855
- package/dist/lib/time-injection.js +1 -2
- package/dist/lib/turns.js +1 -19
- package/dist/lib/updates.d.ts +169 -80
- package/dist/lib/updates.js +2418 -641
- package/dist/lib/voice.d.ts +7 -22
- package/dist/lib/voice.js +7 -22
- package/dist/lib/wire.js +1 -1
- package/dist/lib/workspace-admission.d.ts +3 -1
- package/dist/lib/workspace-admission.js +425 -211
- package/dist/lib/workspace-identity.js +8 -13
- package/dist/lib/workspace-retirement.d.ts +1 -1
- package/dist/lib/workspace-retirement.js +328 -140
- package/dist/lib/workspace-slots.js +8 -4
- package/dist/package.json +1 -1
- package/docs/architecture.md +31 -29
- package/docs/inbound.md +1 -1
- package/docs/multi-instance-bus.md +15 -3
- package/docs/outbound.md +1 -1
- package/docs/public-api.md +29 -1
- package/docs/ui-style.md +25 -8
- package/lib/activity-verbosity.ts +59 -82
- package/lib/activity.ts +146 -102
- package/lib/agent-messages.ts +12 -5
- package/lib/bindings.ts +389 -82
- package/lib/bus-api.ts +263 -102
- package/lib/bus-follower.ts +2440 -601
- package/lib/bus-leader.ts +2209 -1290
- package/lib/bus-transport.ts +46 -12
- package/lib/bus.ts +1673 -444
- package/lib/channel-posts.ts +848 -327
- package/lib/command-templates.ts +152 -265
- package/lib/commands.ts +2398 -555
- package/lib/config.ts +177 -170
- package/lib/delivery.ts +41 -29
- package/lib/extension.ts +1206 -433
- package/lib/generative-app-worker.mjs +19 -7
- package/lib/generative-apps.ts +324 -211
- package/lib/inbound.ts +95 -97
- package/lib/journal.ts +5311 -1763
- package/lib/keyboard.ts +1 -3
- package/lib/lifecycle.ts +128 -78
- package/lib/locks.ts +677 -203
- package/lib/logging.ts +147 -58
- package/lib/media.ts +82 -43
- package/lib/menu-model.ts +85 -18
- package/lib/menu-queue.ts +88 -129
- package/lib/menu-settings.ts +260 -155
- package/lib/menu-status.ts +20 -60
- package/lib/menu-thinking.ts +41 -56
- package/lib/menu.ts +112 -285
- package/lib/model.ts +8 -22
- package/lib/outbound-attachments.ts +156 -125
- package/lib/outbound-buttons.ts +76 -49
- package/lib/outbound-markup.ts +96 -94
- package/lib/outbound-voice.ts +33 -27
- package/lib/outbound.ts +128 -125
- package/lib/ownership.ts +19 -10
- package/lib/paths.ts +148 -49
- package/lib/pi.ts +35 -15
- package/lib/polling.ts +144 -147
- package/lib/preview.ts +93 -40
- package/lib/process-identity.ts +164 -0
- package/lib/prompts.ts +4 -11
- package/lib/queue.ts +516 -301
- package/lib/recovery.ts +50 -454
- package/lib/rendering.ts +1 -3
- package/lib/replies.ts +248 -209
- package/lib/routing.ts +8509 -2186
- package/lib/runtime.ts +1 -12
- package/lib/sections.ts +24 -89
- package/lib/setup.ts +1 -3
- package/lib/skills.ts +9 -4
- package/lib/status.ts +215 -110
- package/lib/sync.ts +448 -361
- package/lib/target.ts +22 -3
- package/lib/telegram-api.ts +302 -127
- package/lib/text-groups.ts +5 -39
- package/lib/thread-cleanup-manager.ts +833 -331
- package/lib/thread-display.ts +213 -97
- package/lib/thread-naming.ts +232 -87
- package/lib/thread-reconciler.ts +155 -72
- package/lib/threads.ts +5318 -1845
- package/lib/time-injection.ts +1 -4
- package/lib/turns.ts +15 -28
- package/lib/updates.ts +5184 -1612
- package/lib/voice.ts +7 -35
- package/lib/wire.ts +1 -1
- package/lib/workspace-admission.ts +996 -416
- package/lib/workspace-identity.ts +14 -17
- package/lib/workspace-retirement.ts +878 -376
- package/lib/workspace-slots.ts +22 -13
- package/package.json +1 -1
- package/scripts/check-downgrade.mjs +0 -472
- package/scripts/measure-workspace.mjs +0 -108
package/dist/lib/commands.js
CHANGED
|
@@ -3,11 +3,44 @@
|
|
|
3
3
|
* Zones: telegram controls, pi agent commands, queue controls
|
|
4
4
|
* Owns Telegram slash-command normalization, bot command metadata, pi-side command registration, and command-initiated session replacement orchestration behind runtime ports
|
|
5
5
|
*/
|
|
6
|
-
import { randomUUID } from "node:crypto";
|
|
6
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
7
|
+
import { addAbortListener } from "node:events";
|
|
8
|
+
import { isDeepStrictEqual } from "node:util";
|
|
7
9
|
import { pairTelegramUserIfNeeded, TELEGRAM_DEFAULT_PROFILE_NAME, } from "./config.js";
|
|
8
|
-
import {
|
|
9
|
-
import { formatTelegramConnectionFailure } from "./status.js";
|
|
10
|
+
import { isPiStaleContextError } from "./pi.js";
|
|
10
11
|
import { createTelegramControlItemBuilder, createTelegramControlQueueController, createTelegramQueueAdmissionReceipt, } from "./queue.js";
|
|
12
|
+
import { escapeHtml } from "./rendering.js";
|
|
13
|
+
import { formatTelegramConnectionFailure, } from "./status.js";
|
|
14
|
+
import { TelegramApiAuthorityError, } from "./telegram-api.js";
|
|
15
|
+
import { acquireTelegramUpdateRouting, carryTelegramUpdateExecutionFence, getTelegramUpdateExecutionFence, inspectTelegramDeferredSource, prepareTelegramDeferredQueueAdmission, reportTelegramQueueAdmission, } from "./updates.js";
|
|
16
|
+
/** Exact singleton operator-private text command original eligible for a held follower plan. */
|
|
17
|
+
export function isTelegramSelectedHeldOriginal(update, target, operator, name) {
|
|
18
|
+
if (!update ||
|
|
19
|
+
typeof update !== "object" ||
|
|
20
|
+
Array.isArray(update) ||
|
|
21
|
+
Object.keys(update).some((key) => !["update_id", "message"].includes(key)))
|
|
22
|
+
return false;
|
|
23
|
+
const value = Reflect.get(update, "message");
|
|
24
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
25
|
+
return false;
|
|
26
|
+
const text = Reflect.get(value, "text"), chat = Reflect.get(value, "chat"), from = Reflect.get(value, "from");
|
|
27
|
+
return (typeof text === "string" &&
|
|
28
|
+
parseTelegramCommand(text)?.name === name &&
|
|
29
|
+
value.caption === undefined &&
|
|
30
|
+
value.media_group_id === undefined &&
|
|
31
|
+
chat?.type === "private" &&
|
|
32
|
+
from?.is_bot === false &&
|
|
33
|
+
from.id === operator &&
|
|
34
|
+
chat.id === operator &&
|
|
35
|
+
Number.isSafeInteger(value.message_id) &&
|
|
36
|
+
value.message_id > 0 &&
|
|
37
|
+
typeof operator === "number" &&
|
|
38
|
+
Number.isSafeInteger(operator) &&
|
|
39
|
+
operator > 0 &&
|
|
40
|
+
target.chatId === operator &&
|
|
41
|
+
Number.isSafeInteger(target.threadId) &&
|
|
42
|
+
target.threadId > 0);
|
|
43
|
+
}
|
|
11
44
|
const TELEGRAM_EXTENSION_COMMAND_REGISTRY_KEY = "__piTelegramCommandRegistry__";
|
|
12
45
|
const TELEGRAM_BOT_COMMAND_NAME_PATTERN = /^[a-z0-9_]{1,32}$/;
|
|
13
46
|
function getOrCreateTelegramCommandRegistry() {
|
|
@@ -60,6 +93,9 @@ export function registerTelegramCommand(registration) {
|
|
|
60
93
|
showInMenu,
|
|
61
94
|
emoji,
|
|
62
95
|
handler: registration.handler,
|
|
96
|
+
selected: registration.selected
|
|
97
|
+
? Object.freeze({ prepare: registration.selected.prepare })
|
|
98
|
+
: undefined,
|
|
63
99
|
};
|
|
64
100
|
registry.commands.set(name, command);
|
|
65
101
|
return () => {
|
|
@@ -75,6 +111,64 @@ export function findTelegramExtensionCommand(name) {
|
|
|
75
111
|
return undefined;
|
|
76
112
|
return getOrCreateTelegramCommandRegistry().commands.get(normalizeTelegramExtensionCommandName(name));
|
|
77
113
|
}
|
|
114
|
+
/** Preparation only: no execution, admission, source freeze or binding effects. Routing owns those later. */
|
|
115
|
+
export async function prepareTelegramSelectedExtensionCommand(command, authority) {
|
|
116
|
+
const registration = findTelegramExtensionCommand(command.name), selected = registration?.selected;
|
|
117
|
+
const prepare = selected?.prepare;
|
|
118
|
+
if (!registration || !selected || typeof prepare !== "function")
|
|
119
|
+
return undefined;
|
|
120
|
+
const input = Object.freeze({ name: registration.name, args: command.args });
|
|
121
|
+
const { assertSourceCurrent, assertRecipientCurrent } = authority;
|
|
122
|
+
const assertRegistrationCurrent = () => {
|
|
123
|
+
if (findTelegramExtensionCommand(input.name) !== registration ||
|
|
124
|
+
registration.selected !== selected ||
|
|
125
|
+
selected.prepare !== prepare)
|
|
126
|
+
throw new Error("Selected extension command registration changed.");
|
|
127
|
+
};
|
|
128
|
+
const assertCurrent = () => {
|
|
129
|
+
assertSourceCurrent();
|
|
130
|
+
assertRecipientCurrent();
|
|
131
|
+
assertRegistrationCurrent();
|
|
132
|
+
};
|
|
133
|
+
try {
|
|
134
|
+
assertCurrent();
|
|
135
|
+
const prepared = await prepare(input);
|
|
136
|
+
assertCurrent();
|
|
137
|
+
if (!prepared || typeof prepared !== "object" || Array.isArray(prepared))
|
|
138
|
+
return undefined;
|
|
139
|
+
const keys = Reflect.ownKeys(prepared), kind = prepared.kind;
|
|
140
|
+
let plan;
|
|
141
|
+
if (kind === "command-only" &&
|
|
142
|
+
keys.length === 2 &&
|
|
143
|
+
keys.includes("kind") &&
|
|
144
|
+
keys.includes("execute")) {
|
|
145
|
+
const execute = prepared.execute;
|
|
146
|
+
if (typeof execute !== "function")
|
|
147
|
+
return undefined;
|
|
148
|
+
plan = { kind, execute };
|
|
149
|
+
}
|
|
150
|
+
else if (kind === "generated-prompt" &&
|
|
151
|
+
keys.length === 2 &&
|
|
152
|
+
keys.includes("kind") &&
|
|
153
|
+
keys.includes("prompt")) {
|
|
154
|
+
const prompt = prepared.prompt;
|
|
155
|
+
if (typeof prompt !== "string" || !prompt.trim())
|
|
156
|
+
return undefined;
|
|
157
|
+
plan = { kind, prompt };
|
|
158
|
+
}
|
|
159
|
+
else
|
|
160
|
+
return undefined;
|
|
161
|
+
// Producer accessors can invalidate authority while the plan is being copied.
|
|
162
|
+
assertCurrent();
|
|
163
|
+
return Object.freeze({
|
|
164
|
+
plan: Object.freeze(plan),
|
|
165
|
+
assertRegistrationCurrent,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
78
172
|
export function clearTelegramExtensionCommands() {
|
|
79
173
|
getOrCreateTelegramCommandRegistry().commands.clear();
|
|
80
174
|
}
|
|
@@ -111,11 +205,11 @@ export function formatTelegramInvalidInstanceName(validationError) {
|
|
|
111
205
|
.split(/;\s+|(?<=\.)\s+(?=[A-Z])/)
|
|
112
206
|
.map((item) => item.trim())
|
|
113
207
|
.filter(Boolean)
|
|
114
|
-
.map((item) => /[.!?]$/.test(item) ? item : `${item}.`)
|
|
208
|
+
.map((item) => (/[.!?]$/.test(item) ? item : `${item}.`))
|
|
115
209
|
.map((item) => item[0].toUpperCase() + item.slice(1));
|
|
116
210
|
return [
|
|
117
211
|
"<b>⚠️ Invalid Thread Display Name:</b>\n",
|
|
118
|
-
...items.map((item) =>
|
|
212
|
+
...items.map((item) => `<code>-</code> ${escapeHtml(item)}`),
|
|
119
213
|
].join("\n");
|
|
120
214
|
}
|
|
121
215
|
export function formatTelegramThreadDisplayNameSavedHeading(name) {
|
|
@@ -176,62 +270,118 @@ export function getTelegramReservedCommandNames() {
|
|
|
176
270
|
...getTelegramExtensionCommands().map((command) => command.name),
|
|
177
271
|
];
|
|
178
272
|
}
|
|
179
|
-
export async function registerTelegramBotCommands(deps) {
|
|
273
|
+
export async function registerTelegramBotCommands(deps, options) {
|
|
274
|
+
const assertAuthority = options?.assertAuthority, send = deps.setMyCommands;
|
|
275
|
+
const capturedOptions = assertAuthority ? { assertAuthority } : undefined;
|
|
276
|
+
const setMyCommands = async (commands) => {
|
|
277
|
+
assertAuthority?.();
|
|
278
|
+
await send(commands, capturedOptions);
|
|
279
|
+
assertAuthority?.();
|
|
280
|
+
};
|
|
281
|
+
assertAuthority?.();
|
|
180
282
|
const extensionCommands = getVisibleTelegramExtensionBotCommands();
|
|
181
283
|
if (extensionCommands.length === 0) {
|
|
182
|
-
await
|
|
284
|
+
await setMyCommands(TELEGRAM_BOT_COMMANDS);
|
|
183
285
|
return;
|
|
184
286
|
}
|
|
185
287
|
const nextCommandIndex = TELEGRAM_BOT_COMMANDS.findIndex((command) => command.command === "next");
|
|
186
288
|
if (nextCommandIndex === -1) {
|
|
187
|
-
await
|
|
289
|
+
await setMyCommands([...TELEGRAM_BOT_COMMANDS, ...extensionCommands]);
|
|
188
290
|
return;
|
|
189
291
|
}
|
|
190
|
-
await
|
|
292
|
+
await setMyCommands([
|
|
191
293
|
...TELEGRAM_BOT_COMMANDS.slice(0, nextCommandIndex + 1),
|
|
192
294
|
...extensionCommands,
|
|
193
295
|
...TELEGRAM_BOT_COMMANDS.slice(nextCommandIndex + 1),
|
|
194
296
|
]);
|
|
195
297
|
}
|
|
196
298
|
export function createTelegramBotCommandRegistrar(deps) {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
299
|
+
// Only an identical local lifetime may share an in-flight sync; unguarded calls remain separate.
|
|
300
|
+
const pending = new Map();
|
|
301
|
+
return (options) => {
|
|
302
|
+
const assertAuthority = options?.assertAuthority;
|
|
303
|
+
try {
|
|
304
|
+
assertAuthority?.();
|
|
305
|
+
}
|
|
306
|
+
catch (error) {
|
|
307
|
+
return Promise.reject(error);
|
|
308
|
+
}
|
|
309
|
+
const joined = pending.get(assertAuthority);
|
|
310
|
+
if (joined)
|
|
311
|
+
return joined;
|
|
201
312
|
let request;
|
|
202
|
-
request = registerTelegramBotCommands(deps).finally(() => {
|
|
203
|
-
if (pending === request)
|
|
204
|
-
pending
|
|
313
|
+
request = registerTelegramBotCommands(deps, assertAuthority ? { assertAuthority } : undefined).finally(() => {
|
|
314
|
+
if (pending.get(assertAuthority) === request)
|
|
315
|
+
pending.delete(assertAuthority);
|
|
205
316
|
});
|
|
206
|
-
pending
|
|
317
|
+
pending.set(assertAuthority, request);
|
|
207
318
|
return request;
|
|
208
319
|
};
|
|
209
320
|
}
|
|
210
321
|
export function createTelegramThreadDisplayNameResetBinding() {
|
|
211
|
-
let current;
|
|
322
|
+
let current, generation = 0;
|
|
212
323
|
return {
|
|
213
|
-
bind(reset) {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
324
|
+
bind(reset) {
|
|
325
|
+
current = reset;
|
|
326
|
+
generation++;
|
|
327
|
+
},
|
|
328
|
+
async reset(target, options) {
|
|
329
|
+
const reset = current, assertAuthority = options?.assertAuthority, capturedGeneration = generation;
|
|
330
|
+
const capturedTarget = {
|
|
331
|
+
chatId: target.chatId,
|
|
332
|
+
...(target.threadId === undefined ? {} : { threadId: target.threadId }),
|
|
333
|
+
};
|
|
334
|
+
if (!reset)
|
|
335
|
+
return {
|
|
336
|
+
ok: false,
|
|
337
|
+
message: "Thread display name reset is unavailable.",
|
|
338
|
+
};
|
|
339
|
+
const assertCurrent = () => {
|
|
340
|
+
assertAuthority?.();
|
|
341
|
+
if (assertAuthority && generation !== capturedGeneration)
|
|
342
|
+
throw new Error("Thread display name reset binding changed.");
|
|
343
|
+
};
|
|
344
|
+
assertCurrent();
|
|
345
|
+
const result = assertAuthority
|
|
346
|
+
? await reset(capturedTarget, { assertAuthority: assertCurrent })
|
|
347
|
+
: await reset(capturedTarget);
|
|
348
|
+
assertCurrent();
|
|
349
|
+
return result;
|
|
218
350
|
},
|
|
219
351
|
};
|
|
220
352
|
}
|
|
221
353
|
export function createTelegramThreadDisplayNameRenameBinding() {
|
|
222
|
-
let current;
|
|
354
|
+
let current, generation = 0;
|
|
223
355
|
return {
|
|
224
356
|
bind(rename) {
|
|
225
357
|
current = rename;
|
|
358
|
+
generation++;
|
|
226
359
|
},
|
|
227
|
-
async rename(target, threadName) {
|
|
228
|
-
|
|
360
|
+
async rename(target, threadName, options) {
|
|
361
|
+
const rename = current, assertAuthority = options?.assertAuthority, capturedGeneration = generation;
|
|
362
|
+
const capturedTarget = {
|
|
363
|
+
chatId: target.chatId,
|
|
364
|
+
...(target.threadId === undefined ? {} : { threadId: target.threadId }),
|
|
365
|
+
};
|
|
366
|
+
if (!rename) {
|
|
229
367
|
return {
|
|
230
368
|
ok: false,
|
|
231
369
|
message: "Thread display naming is unavailable.",
|
|
232
370
|
};
|
|
233
371
|
}
|
|
234
|
-
|
|
372
|
+
const assertCurrent = () => {
|
|
373
|
+
assertAuthority?.();
|
|
374
|
+
if (assertAuthority && generation !== capturedGeneration)
|
|
375
|
+
throw new Error("Thread display naming binding changed.");
|
|
376
|
+
};
|
|
377
|
+
assertCurrent();
|
|
378
|
+
const result = assertAuthority
|
|
379
|
+
? await rename(capturedTarget, threadName, {
|
|
380
|
+
assertAuthority: assertCurrent,
|
|
381
|
+
})
|
|
382
|
+
: await rename(capturedTarget, threadName);
|
|
383
|
+
assertCurrent();
|
|
384
|
+
return result;
|
|
235
385
|
},
|
|
236
386
|
};
|
|
237
387
|
}
|
|
@@ -275,11 +425,15 @@ export function registerTelegramBridgeCommands(pi, deps) {
|
|
|
275
425
|
let intentId;
|
|
276
426
|
// Pi context getters throw after replacement; check plain intent/generation first.
|
|
277
427
|
const isCurrent = () => (!intentId || deps.connectionIntent?.isActive(intentId) !== false) &&
|
|
278
|
-
(sessionGeneration === undefined ||
|
|
428
|
+
(sessionGeneration === undefined ||
|
|
429
|
+
deps.getSessionGeneration?.() === sessionGeneration) &&
|
|
279
430
|
deps.isContextCurrent?.(ctx) !== false;
|
|
280
431
|
if (!isCurrent())
|
|
281
432
|
return;
|
|
282
|
-
if (args
|
|
433
|
+
if (args
|
|
434
|
+
.trim()
|
|
435
|
+
.split(/\s+/)
|
|
436
|
+
.some((word) => /^as=/i.test(word))) {
|
|
283
437
|
ctx.ui.notify("Thread names are configured from Telegram, not from Pi commands.", "warning");
|
|
284
438
|
deps.updateStatus(ctx);
|
|
285
439
|
return;
|
|
@@ -299,7 +453,8 @@ export function registerTelegramBridgeCommands(pi, deps) {
|
|
|
299
453
|
ctx.ui.notify(`Activated profile "${profileName}".`, "info");
|
|
300
454
|
}
|
|
301
455
|
else {
|
|
302
|
-
await (deps.activateDefaultProfileConfig?.(ctx, isCurrent) ??
|
|
456
|
+
await (deps.activateDefaultProfileConfig?.(ctx, isCurrent) ??
|
|
457
|
+
deps.reloadConfig());
|
|
303
458
|
if (!isCurrent())
|
|
304
459
|
return;
|
|
305
460
|
}
|
|
@@ -316,53 +471,17 @@ export function registerTelegramBridgeCommands(pi, deps) {
|
|
|
316
471
|
await deps.promptForConfig(ctx, profileName);
|
|
317
472
|
return;
|
|
318
473
|
}
|
|
319
|
-
|
|
320
|
-
const startWithRecovery = async (options) => {
|
|
474
|
+
const startPolling = async (options) => {
|
|
321
475
|
try {
|
|
322
476
|
return await deps.startPolling(ctx, options);
|
|
323
477
|
}
|
|
324
478
|
catch (error) {
|
|
325
479
|
if (!isCurrent())
|
|
326
480
|
return;
|
|
327
|
-
|
|
328
|
-
throw error;
|
|
329
|
-
deps.recordConnectionEvent?.(error, "polling-start");
|
|
330
|
-
const recovery = await deps.recoverPollingStart(error);
|
|
331
|
-
if (!isCurrent())
|
|
332
|
-
return;
|
|
333
|
-
if (recovery.kind === "unhandled")
|
|
334
|
-
throw error;
|
|
335
|
-
if (recovery.kind === "blocked") {
|
|
336
|
-
return { ok: false, message: recovery.message,
|
|
337
|
-
notice: "Telegram recovery blocked. Check /telegram-status --debug." };
|
|
338
|
-
}
|
|
339
|
-
recoveryUsed = true;
|
|
340
|
-
deps.recordConnectionEvent?.(recovery.message, "recovery");
|
|
341
|
-
try {
|
|
342
|
-
const retry = await deps.startPolling(ctx, options);
|
|
343
|
-
if (!isCurrent())
|
|
344
|
-
return;
|
|
345
|
-
if (!retry)
|
|
346
|
-
return { ok: true, message: "Telegram bridge connected; temporary state recovered." };
|
|
347
|
-
return {
|
|
348
|
-
...retry,
|
|
349
|
-
message: retry.ok
|
|
350
|
-
? "Telegram bridge connected; temporary state recovered."
|
|
351
|
-
: retry.message,
|
|
352
|
-
};
|
|
353
|
-
}
|
|
354
|
-
catch (error) {
|
|
355
|
-
if (!isCurrent())
|
|
356
|
-
return;
|
|
357
|
-
deps.recordConnectionEvent?.(error, "recovery-retry");
|
|
358
|
-
return {
|
|
359
|
-
ok: false,
|
|
360
|
-
notice: "Telegram recovery failed. Restart this Pi instance.",
|
|
361
|
-
};
|
|
362
|
-
}
|
|
481
|
+
throw error;
|
|
363
482
|
}
|
|
364
483
|
};
|
|
365
|
-
let result = await
|
|
484
|
+
let result = await startPolling({ forceFreshLeaderThread: true });
|
|
366
485
|
if (!isCurrent())
|
|
367
486
|
return;
|
|
368
487
|
if (result && !result.ok && result.canTakeover) {
|
|
@@ -374,14 +493,17 @@ export function registerTelegramBridgeCommands(pi, deps) {
|
|
|
374
493
|
deps.updateStatus(ctx);
|
|
375
494
|
return;
|
|
376
495
|
}
|
|
377
|
-
result = await
|
|
496
|
+
result = await startPolling({
|
|
497
|
+
force: true,
|
|
498
|
+
forceFreshLeaderThread: true,
|
|
499
|
+
});
|
|
378
500
|
if (!isCurrent())
|
|
379
501
|
return;
|
|
380
502
|
}
|
|
381
503
|
if (result && !result.ok) {
|
|
382
504
|
if (result.message)
|
|
383
505
|
deps.recordConnectionEvent?.(result.message, "connect-refused");
|
|
384
|
-
ctx.ui.notify(
|
|
506
|
+
ctx.ui.notify(formatTelegramConnectionFailure(result.message), "warning");
|
|
385
507
|
}
|
|
386
508
|
else if (result?.message) {
|
|
387
509
|
ctx.ui.notify(result.message, "info");
|
|
@@ -407,7 +529,8 @@ export function registerTelegramBridgeCommands(pi, deps) {
|
|
|
407
529
|
description: "Stop Telegram and delete current thread in Threaded Mode",
|
|
408
530
|
handler: async (_args, ctx) => {
|
|
409
531
|
const generation = deps.getSessionGeneration?.();
|
|
410
|
-
const isCurrent = () => (generation === undefined ||
|
|
532
|
+
const isCurrent = () => (generation === undefined ||
|
|
533
|
+
deps.getSessionGeneration?.() === generation) &&
|
|
411
534
|
deps.isContextCurrent?.(ctx) !== false;
|
|
412
535
|
if (!isCurrent())
|
|
413
536
|
return;
|
|
@@ -464,6 +587,14 @@ export function isTelegramReservedCommandName(commandName) {
|
|
|
464
587
|
return (commandName !== undefined &&
|
|
465
588
|
TELEGRAM_RESERVED_COMMAND_NAME_SET.has(commandName));
|
|
466
589
|
}
|
|
590
|
+
function isTelegramSessionBusy(ports) {
|
|
591
|
+
return (!ports.isIdle() ||
|
|
592
|
+
ports.hasPendingMessages() ||
|
|
593
|
+
ports.hasActiveTelegramTurn() ||
|
|
594
|
+
ports.hasDispatchPending() ||
|
|
595
|
+
ports.hasQueuedTelegramItems() ||
|
|
596
|
+
ports.isCompactionInProgress());
|
|
597
|
+
}
|
|
467
598
|
function canPairTelegramUserFromCommandMessage(message) {
|
|
468
599
|
return message.chat.type === undefined || message.chat.type === "private";
|
|
469
600
|
}
|
|
@@ -476,16 +607,6 @@ export function getTelegramCommandMessageTarget(message) {
|
|
|
476
607
|
replyToMessageId: message.message_id,
|
|
477
608
|
};
|
|
478
609
|
}
|
|
479
|
-
export function createTelegramCommandControlQueueRuntime(deps) {
|
|
480
|
-
const controlQueueController = createTelegramControlQueueController({
|
|
481
|
-
appendControlItem: deps.appendControlItem,
|
|
482
|
-
dispatchNextQueuedTelegramTurn: deps.dispatchNextQueuedTelegramTurn,
|
|
483
|
-
});
|
|
484
|
-
return createTelegramCommandControlEnqueueAdapter({
|
|
485
|
-
createControlItem: deps.createControlItem,
|
|
486
|
-
enqueueControlItem: controlQueueController.enqueue,
|
|
487
|
-
});
|
|
488
|
-
}
|
|
489
610
|
export function createTelegramCommandControlEnqueueAdapter(deps) {
|
|
490
611
|
return (target, ctx, controlType, statusSummary, execute, admissionReceipts, onQueued) => {
|
|
491
612
|
deps.enqueueControlItem(deps.createControlItem({
|
|
@@ -498,11 +619,15 @@ export function createTelegramCommandControlEnqueueAdapter(deps) {
|
|
|
498
619
|
};
|
|
499
620
|
}
|
|
500
621
|
export function createTelegramCommandTargetQueueRuntime(deps) {
|
|
622
|
+
const { createControlItem, appendControlItem, dispatchNextQueuedTelegramTurn, } = deps;
|
|
623
|
+
const controlQueueController = createTelegramControlQueueController({
|
|
624
|
+
appendControlItem,
|
|
625
|
+
dispatchNextQueuedTelegramTurn,
|
|
626
|
+
});
|
|
501
627
|
return createTelegramCommandTargetRuntime({
|
|
502
|
-
enqueueControlItem:
|
|
503
|
-
createControlItem
|
|
504
|
-
|
|
505
|
-
dispatchNextQueuedTelegramTurn: deps.dispatchNextQueuedTelegramTurn,
|
|
628
|
+
enqueueControlItem: createTelegramCommandControlEnqueueAdapter({
|
|
629
|
+
createControlItem,
|
|
630
|
+
enqueueControlItem: controlQueueController.enqueue,
|
|
506
631
|
}),
|
|
507
632
|
getAdmissionScope: deps.getAdmissionScope,
|
|
508
633
|
getAdmissionJournalBinding: deps.getAdmissionJournalBinding,
|
|
@@ -531,85 +656,70 @@ export function createTelegramCommandTargetRuntime(deps) {
|
|
|
531
656
|
...(journalBindingKey ? { journalBindingKey } : {}),
|
|
532
657
|
}
|
|
533
658
|
: undefined;
|
|
534
|
-
deps.enqueueControlItem(getTelegramCommandMessageTarget(message), ctx, controlType, statusSummary, execute, receipt ? [receipt] : undefined, receipt
|
|
535
|
-
? () => deps.onControlQueued?.(message, receipt)
|
|
536
|
-
: undefined);
|
|
659
|
+
deps.enqueueControlItem(getTelegramCommandMessageTarget(message), ctx, controlType, statusSummary, execute, receipt ? [receipt] : undefined, receipt ? () => deps.onControlQueued?.(message, receipt) : undefined);
|
|
537
660
|
},
|
|
538
|
-
showStatus: (message, ctx) => {
|
|
661
|
+
showStatus: (message, ctx, options) => {
|
|
539
662
|
const target = getTelegramCommandMessageTarget(message);
|
|
540
|
-
return deps.showStatus(target.chatId, target.replyToMessageId, ctx, target.threadId);
|
|
663
|
+
return deps.showStatus(target.chatId, target.replyToMessageId, ctx, target.threadId, options);
|
|
541
664
|
},
|
|
542
|
-
openModelMenu: (message, ctx) => {
|
|
665
|
+
openModelMenu: (message, ctx, options) => {
|
|
543
666
|
const target = getTelegramCommandMessageTarget(message);
|
|
544
|
-
return deps.openModelMenu(target.chatId, target.replyToMessageId, ctx, target.threadId);
|
|
667
|
+
return deps.openModelMenu(target.chatId, target.replyToMessageId, ctx, target.threadId, options);
|
|
545
668
|
},
|
|
546
|
-
openSettingsMenu: async (message, ctx) => {
|
|
669
|
+
openSettingsMenu: async (message, ctx, options) => {
|
|
547
670
|
const target = getTelegramCommandMessageTarget(message);
|
|
548
671
|
if (!deps.openSettingsMenu) {
|
|
549
672
|
await deps.sendTextReply(target.chatId, target.replyToMessageId, formatTelegramInformationHeading("🚫", "Settings menu is unavailable."), { target, parseMode: "HTML" });
|
|
550
673
|
return;
|
|
551
674
|
}
|
|
552
|
-
await deps.openSettingsMenu(target.chatId, target.replyToMessageId, ctx, target.threadId);
|
|
675
|
+
await deps.openSettingsMenu(target.chatId, target.replyToMessageId, ctx, target.threadId, options);
|
|
553
676
|
},
|
|
554
677
|
sendTextReply: async (message, text, options) => {
|
|
555
678
|
const target = getTelegramCommandMessageTarget(message);
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
679
|
+
const assertAuthority = options?.assertAuthority, send = deps.sendTextReply;
|
|
680
|
+
const capturedOptions = { ...options, target };
|
|
681
|
+
assertAuthority?.();
|
|
682
|
+
await send(target.chatId, target.replyToMessageId, text, capturedOptions);
|
|
683
|
+
assertAuthority?.();
|
|
560
684
|
},
|
|
561
685
|
};
|
|
562
686
|
}
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
.replace(/>/g, ">");
|
|
687
|
+
/** Pure intro: built-in controls with registered extension lines before the abort controls. */
|
|
688
|
+
function formatTelegramAppMenuIntroHtml(extensionLines) {
|
|
689
|
+
const line = (name, label) => `${formatTelegramCommandEmojiPrefix(name)}/${name} — ${label}`;
|
|
690
|
+
return [
|
|
691
|
+
"<b>Pi Telegram</b>",
|
|
692
|
+
"",
|
|
693
|
+
line("start", "Open menu / Pair bridge"),
|
|
694
|
+
line("compact", "Compact current session"),
|
|
695
|
+
line("new", "Start a new session"),
|
|
696
|
+
line("continue", "Queue continue prompt"),
|
|
697
|
+
line("next", "Force next turn"),
|
|
698
|
+
...extensionLines,
|
|
699
|
+
line("abort", "Abort Pi"),
|
|
700
|
+
line("stop", "Abort Pi & Clear queue"),
|
|
701
|
+
].join("\n");
|
|
579
702
|
}
|
|
703
|
+
export const TELEGRAM_APP_MENU_INTRO_HTML = formatTelegramAppMenuIntroHtml([]);
|
|
580
704
|
function buildTelegramPromptTemplateMenuHtml(promptTemplates = []) {
|
|
581
705
|
if (promptTemplates.length === 0)
|
|
582
706
|
return "";
|
|
583
707
|
return promptTemplates
|
|
584
|
-
.map((template) => `🧩 /${
|
|
708
|
+
.map((template) => `🧩 /${escapeHtml(template.command)}`)
|
|
585
709
|
.join("\n");
|
|
586
710
|
}
|
|
587
711
|
function buildTelegramExtensionCommandMenuLines() {
|
|
588
712
|
return getTelegramExtensionCommands()
|
|
589
713
|
.filter((command) => command.showInMenu)
|
|
590
714
|
.map((command) => {
|
|
591
|
-
const prefix = `${
|
|
715
|
+
const prefix = `${escapeHtml(command.emoji ?? "")} /${escapeHtml(command.name)}`;
|
|
592
716
|
if (!command.description)
|
|
593
717
|
return prefix;
|
|
594
|
-
return `${prefix} — ${
|
|
718
|
+
return `${prefix} — ${escapeHtml(command.description)}`;
|
|
595
719
|
});
|
|
596
720
|
}
|
|
597
721
|
function buildTelegramAppMenuIntroHtml() {
|
|
598
|
-
|
|
599
|
-
if (extensionLines.length === 0)
|
|
600
|
-
return TELEGRAM_APP_MENU_INTRO_HTML;
|
|
601
|
-
return [
|
|
602
|
-
"<b>Pi Telegram</b>",
|
|
603
|
-
"",
|
|
604
|
-
`${formatTelegramCommandEmojiPrefix("start")}/start — Open menu / Pair bridge`,
|
|
605
|
-
`${formatTelegramCommandEmojiPrefix("compact")}/compact — Compact current session`,
|
|
606
|
-
`${formatTelegramCommandEmojiPrefix("new")}/new — Start a new session`,
|
|
607
|
-
`${formatTelegramCommandEmojiPrefix("continue")}/continue — Queue continue prompt`,
|
|
608
|
-
`${formatTelegramCommandEmojiPrefix("next")}/next — Force next turn`,
|
|
609
|
-
...extensionLines,
|
|
610
|
-
`${formatTelegramCommandEmojiPrefix("abort")}/abort — Abort Pi`,
|
|
611
|
-
`${formatTelegramCommandEmojiPrefix("stop")}/stop — Abort Pi & Clear queue`,
|
|
612
|
-
].join("\n");
|
|
722
|
+
return formatTelegramAppMenuIntroHtml(buildTelegramExtensionCommandMenuLines());
|
|
613
723
|
}
|
|
614
724
|
export function buildTelegramAppMenuHtml(statusHtml, promptTemplates = []) {
|
|
615
725
|
const introHtml = buildTelegramAppMenuIntroHtml();
|
|
@@ -677,9 +787,6 @@ export function buildTelegramCommandAction(commandName) {
|
|
|
677
787
|
}
|
|
678
788
|
return TELEGRAM_COMMAND_ACTIONS[commandName];
|
|
679
789
|
}
|
|
680
|
-
export function getTelegramCommandExecutionMode(action) {
|
|
681
|
-
return action.executionMode;
|
|
682
|
-
}
|
|
683
790
|
function formatTelegramQueuedTurnCount(count) {
|
|
684
791
|
return count === 1 ? "1 queued turn" : `${count} queued turns`;
|
|
685
792
|
}
|
|
@@ -758,13 +865,26 @@ function buildTelegramNewConfirmationReplyMarkup() {
|
|
|
758
865
|
],
|
|
759
866
|
};
|
|
760
867
|
}
|
|
868
|
+
/** Send one confirmation to an exact copied recipient, with authority checks around the issued request. */
|
|
869
|
+
async function sendTelegramConfirmation(target, deps, html, markup, assertAuthority) {
|
|
870
|
+
const recipientTarget = {
|
|
871
|
+
chatId: target.chatId,
|
|
872
|
+
...(target.threadId !== undefined ? { threadId: target.threadId } : {}),
|
|
873
|
+
};
|
|
874
|
+
const options = assertAuthority
|
|
875
|
+
? { target: recipientTarget, assertAuthority }
|
|
876
|
+
: target.threadId !== undefined
|
|
877
|
+
? { target: recipientTarget }
|
|
878
|
+
: undefined;
|
|
879
|
+
assertAuthority?.();
|
|
880
|
+
await deps.sendInteractiveMessage(recipientTarget.chatId, html, "html", markup, options);
|
|
881
|
+
assertAuthority?.();
|
|
882
|
+
}
|
|
761
883
|
function getTelegramNewConfirmationHtml() {
|
|
762
884
|
return "<b>Start a new session?</b>";
|
|
763
885
|
}
|
|
764
|
-
export async function openTelegramNewConfirmation(target, deps) {
|
|
765
|
-
await
|
|
766
|
-
? { target: { chatId: target.chatId, threadId: target.threadId } }
|
|
767
|
-
: undefined);
|
|
886
|
+
export async function openTelegramNewConfirmation(target, deps, assertAuthority) {
|
|
887
|
+
await sendTelegramConfirmation(target, deps, getTelegramNewConfirmationHtml(), buildTelegramNewConfirmationReplyMarkup(), assertAuthority);
|
|
768
888
|
}
|
|
769
889
|
export async function handleTelegramNewConfirmationCallback(query, deps) {
|
|
770
890
|
if (query.data !== "new:confirm" && query.data !== "new:cancel")
|
|
@@ -772,7 +892,7 @@ export async function handleTelegramNewConfirmationCallback(query, deps) {
|
|
|
772
892
|
const chatId = query.message?.chat?.id;
|
|
773
893
|
const messageId = query.message?.message_id;
|
|
774
894
|
if (typeof chatId !== "number" || typeof messageId !== "number") {
|
|
775
|
-
await deps.answerCallbackQuery(query.id, "
|
|
895
|
+
await deps.answerCallbackQuery(query.id, "Interactive message expired");
|
|
776
896
|
return true;
|
|
777
897
|
}
|
|
778
898
|
if (query.data === "new:cancel") {
|
|
@@ -798,10 +918,8 @@ function buildTelegramCompactConfirmationReplyMarkup() {
|
|
|
798
918
|
function getTelegramCompactConfirmationHtml() {
|
|
799
919
|
return "<b>Compact session?</b>";
|
|
800
920
|
}
|
|
801
|
-
async function openTelegramCompactConfirmation(target, deps) {
|
|
802
|
-
await
|
|
803
|
-
? { target: { chatId: target.chatId, threadId: target.threadId } }
|
|
804
|
-
: undefined);
|
|
921
|
+
async function openTelegramCompactConfirmation(target, deps, assertAuthority) {
|
|
922
|
+
await sendTelegramConfirmation(target, deps, getTelegramCompactConfirmationHtml(), buildTelegramCompactConfirmationReplyMarkup(), assertAuthority);
|
|
805
923
|
}
|
|
806
924
|
export async function handleTelegramCompactConfirmationCallback(query, deps) {
|
|
807
925
|
if (query.data !== "compact:confirm" && query.data !== "compact:cancel") {
|
|
@@ -811,7 +929,7 @@ export async function handleTelegramCompactConfirmationCallback(query, deps) {
|
|
|
811
929
|
const chatId = callbackMessage?.chat?.id;
|
|
812
930
|
const messageId = callbackMessage?.message_id;
|
|
813
931
|
if (typeof chatId !== "number" || typeof messageId !== "number") {
|
|
814
|
-
await deps.answerCallbackQuery(query.id, "
|
|
932
|
+
await deps.answerCallbackQuery(query.id, "Interactive message expired");
|
|
815
933
|
return true;
|
|
816
934
|
}
|
|
817
935
|
if (query.data === "compact:cancel") {
|
|
@@ -826,12 +944,7 @@ export async function handleTelegramCompactConfirmationCallback(query, deps) {
|
|
|
826
944
|
return true;
|
|
827
945
|
}
|
|
828
946
|
export async function handleTelegramNewCommand(deps) {
|
|
829
|
-
if (
|
|
830
|
-
deps.hasPendingMessages() ||
|
|
831
|
-
deps.hasActiveTelegramTurn() ||
|
|
832
|
-
deps.hasDispatchPending() ||
|
|
833
|
-
deps.hasQueuedTelegramItems() ||
|
|
834
|
-
deps.isCompactionInProgress()) {
|
|
947
|
+
if (isTelegramSessionBusy(deps)) {
|
|
835
948
|
await deps.sendTextReply(formatTelegramInformationHeading("⏳", "Cannot start a new session while Pi or the Telegram queue is busy. Wait for queued turns to finish or send /abort first."), { parseMode: "HTML" });
|
|
836
949
|
return;
|
|
837
950
|
}
|
|
@@ -842,12 +955,7 @@ export async function handleTelegramNewCommand(deps) {
|
|
|
842
955
|
deps.requestNewSession();
|
|
843
956
|
}
|
|
844
957
|
export async function handleTelegramCompactCommand(deps) {
|
|
845
|
-
if (
|
|
846
|
-
deps.hasPendingMessages() ||
|
|
847
|
-
deps.hasActiveTelegramTurn() ||
|
|
848
|
-
deps.hasDispatchPending() ||
|
|
849
|
-
deps.hasQueuedTelegramItems() ||
|
|
850
|
-
deps.isCompactionInProgress()) {
|
|
958
|
+
if (isTelegramSessionBusy(deps)) {
|
|
851
959
|
await deps.sendTextReply(formatTelegramInformationHeading("⏳", "Cannot compact while Pi or the Telegram queue is busy. Wait for queued turns to finish or send /abort first."), { parseMode: "HTML" });
|
|
852
960
|
return;
|
|
853
961
|
}
|
|
@@ -889,26 +997,12 @@ export async function handleTelegramCompactCommand(deps) {
|
|
|
889
997
|
});
|
|
890
998
|
}
|
|
891
999
|
}
|
|
892
|
-
function isTelegramStaleContextError(error) {
|
|
893
|
-
return (error instanceof Error &&
|
|
894
|
-
(error.message.includes("stale after session") ||
|
|
895
|
-
error.message.includes("stale ctx")));
|
|
896
|
-
}
|
|
897
|
-
export async function handleTelegramStatusCommand(deps) {
|
|
898
|
-
try {
|
|
899
|
-
await deps.showStatus(deps.ctx);
|
|
900
|
-
}
|
|
901
|
-
catch (error) {
|
|
902
|
-
if (!isTelegramStaleContextError(error))
|
|
903
|
-
throw error;
|
|
904
|
-
}
|
|
905
|
-
}
|
|
906
1000
|
export async function handleTelegramModelCommand(deps) {
|
|
907
1001
|
try {
|
|
908
1002
|
await deps.openModelMenu(deps.ctx);
|
|
909
1003
|
}
|
|
910
1004
|
catch (error) {
|
|
911
|
-
if (!
|
|
1005
|
+
if (!isPiStaleContextError(error))
|
|
912
1006
|
throw error;
|
|
913
1007
|
}
|
|
914
1008
|
}
|
|
@@ -976,6 +1070,7 @@ export function createTelegramCommandHandlerTargetRuntime(deps) {
|
|
|
976
1070
|
sendTextReply: deps.sendTextReply,
|
|
977
1071
|
});
|
|
978
1072
|
return createTelegramCommandHandler({
|
|
1073
|
+
assertExecutionCurrent: deps.assertExecutionCurrent,
|
|
979
1074
|
hasAbortHandler: deps.hasAbortHandler,
|
|
980
1075
|
clearPendingModelSwitch: deps.clearPendingModelSwitch,
|
|
981
1076
|
hasQueuedTelegramItems: deps.hasQueuedTelegramItems,
|
|
@@ -990,6 +1085,7 @@ export function createTelegramCommandHandlerTargetRuntime(deps) {
|
|
|
990
1085
|
setCompactionInProgress: deps.setCompactionInProgress,
|
|
991
1086
|
updateStatus: deps.updateStatus,
|
|
992
1087
|
isContextActive: deps.isContextActive,
|
|
1088
|
+
beginCommandEffectWork: deps.beginCommandEffectWork,
|
|
993
1089
|
dispatchNextQueuedTelegramTurn: deps.dispatchNextQueuedTelegramTurn,
|
|
994
1090
|
requestNextDispatchAnnouncement: deps.requestNextDispatchAnnouncement,
|
|
995
1091
|
markActiveTurnNextAbortAnnouncement: deps.markActiveTurnNextAbortAnnouncement,
|
|
@@ -997,6 +1093,7 @@ export function createTelegramCommandHandlerTargetRuntime(deps) {
|
|
|
997
1093
|
startTypingLoop: deps.startTypingLoop,
|
|
998
1094
|
stopTypingLoop: deps.stopTypingLoop,
|
|
999
1095
|
enqueueContinueTurn: deps.enqueueContinueTurn,
|
|
1096
|
+
heldTurn: deps.heldTurn,
|
|
1000
1097
|
compact: deps.compact,
|
|
1001
1098
|
requestNewSession: deps.requestNewSession,
|
|
1002
1099
|
sendInteractiveMessage: deps.sendInteractiveMessage,
|
|
@@ -1020,61 +1117,1009 @@ export function createTelegramCommandHandlerTargetRuntime(deps) {
|
|
|
1020
1117
|
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
1021
1118
|
});
|
|
1022
1119
|
}
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1120
|
+
/**
|
|
1121
|
+
* Captures admission ports now and checks source → execution → recipient; `strict` rechecks the source after the
|
|
1122
|
+
* recipient, for owners whose effects outlive one synchronous step.
|
|
1123
|
+
*/
|
|
1124
|
+
function createSelectedSemanticGuard(admission, assertExecution, strict) {
|
|
1125
|
+
const assertSourceCurrent = admission.assertSourceCurrent, assertRecipientCurrent = admission.assertRecipientCurrent;
|
|
1126
|
+
return () => {
|
|
1127
|
+
assertSourceCurrent();
|
|
1128
|
+
assertExecution();
|
|
1129
|
+
assertRecipientCurrent();
|
|
1130
|
+
if (!strict)
|
|
1131
|
+
return;
|
|
1132
|
+
assertSourceCurrent();
|
|
1133
|
+
assertExecution();
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1136
|
+
/** Wrap one synchronous command effect between semantic checks. */
|
|
1137
|
+
function guardSelectedEffect(assertCurrent) {
|
|
1138
|
+
return (effect) => (...args) => {
|
|
1139
|
+
assertCurrent();
|
|
1140
|
+
const result = effect(...args);
|
|
1141
|
+
assertCurrent();
|
|
1142
|
+
return result;
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
/** One issuance only: later calls return false without assertions or effects, never replay. */
|
|
1146
|
+
function issueSelectedOnce(assertCurrent, run) {
|
|
1147
|
+
let issued = false;
|
|
1148
|
+
return async () => {
|
|
1149
|
+
if (issued)
|
|
1150
|
+
return false;
|
|
1151
|
+
assertCurrent();
|
|
1152
|
+
issued = true;
|
|
1153
|
+
return run();
|
|
1026
1154
|
};
|
|
1027
1155
|
}
|
|
1028
|
-
|
|
1156
|
+
/** Body-free reply anchor: chat, message and Thread only, never the original's admission symbols. */
|
|
1157
|
+
function copySelectedReplyAnchor(original) {
|
|
1029
1158
|
return {
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1159
|
+
chat: { ...original.chat },
|
|
1160
|
+
message_id: original.message_id,
|
|
1161
|
+
message_thread_id: original.message_thread_id,
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1164
|
+
function copySelectedMenuCarrier(original) {
|
|
1165
|
+
const message = Object.fromEntries(Object.entries(original).filter(([key]) => key !== "pi_telegram_source_update_id"));
|
|
1166
|
+
message.chat = { ...original.chat };
|
|
1167
|
+
return message;
|
|
1168
|
+
}
|
|
1169
|
+
export function createTelegramCommandHandler(deps) {
|
|
1170
|
+
const handle = async (commandName, message, ctx, commandArgs) => {
|
|
1171
|
+
return handleTelegramCommandRuntime(commandName, message, ctx, deps, commandArgs);
|
|
1172
|
+
};
|
|
1173
|
+
const heldQueueReceipts = new Map();
|
|
1174
|
+
const heldQueueOwners = new WeakSet();
|
|
1175
|
+
const watchHeldQueueOwner = (signal) => {
|
|
1176
|
+
if (heldQueueOwners.has(signal))
|
|
1177
|
+
return;
|
|
1178
|
+
addAbortListener(signal, () => {
|
|
1179
|
+
// Native owner end is irreversible; keep refusal identity, not its context/carrier closures or a fallback grant.
|
|
1180
|
+
for (const [id, record] of heldQueueReceipts)
|
|
1181
|
+
if (record?.signal === signal)
|
|
1182
|
+
heldQueueReceipts.set(id, null);
|
|
1183
|
+
});
|
|
1184
|
+
heldQueueOwners.add(signal);
|
|
1185
|
+
};
|
|
1186
|
+
const prepareHeldSelectedCommand = (name, readiness, admission, captured, portsCurrent, completion, run, inspectQueuedCompletion) => {
|
|
1187
|
+
const snapshot = readiness.snapshot, raw = snapshot.update.message, target = { ...admission.target };
|
|
1188
|
+
const bind = readiness.bindCarrier, isCurrent = readiness.isCurrent, assertSource = admission.assertSourceCurrent, assertRecipient = admission.assertRecipientCurrent;
|
|
1189
|
+
if (typeof bind !== "function" ||
|
|
1190
|
+
typeof isCurrent !== "function" ||
|
|
1191
|
+
typeof assertSource !== "function" ||
|
|
1192
|
+
typeof assertRecipient !== "function" ||
|
|
1193
|
+
!isCurrent.call(readiness) ||
|
|
1194
|
+
!raw ||
|
|
1195
|
+
typeof raw !== "object" ||
|
|
1196
|
+
Array.isArray(raw) ||
|
|
1197
|
+
Object.keys(snapshot.update).some((key) => !["update_id", "message"].includes(key)))
|
|
1198
|
+
return undefined;
|
|
1199
|
+
const value = raw, chat = value.chat, from = value.from;
|
|
1200
|
+
if (typeof captured.getAllowedUserId !== "function")
|
|
1201
|
+
return undefined;
|
|
1202
|
+
const command = typeof value.text === "string"
|
|
1203
|
+
? parseTelegramCommand(value.text)
|
|
1204
|
+
: undefined, operator = captured.getAllowedUserId();
|
|
1205
|
+
if (!command ||
|
|
1206
|
+
!chat ||
|
|
1207
|
+
!from ||
|
|
1208
|
+
!isTelegramSelectedHeldOriginal(snapshot.update, target, operator, name))
|
|
1209
|
+
return undefined;
|
|
1210
|
+
const source = { ...snapshot.source }, preparedCommand = { ...command };
|
|
1211
|
+
const message = {
|
|
1212
|
+
...structuredClone(value),
|
|
1213
|
+
chat: { ...chat },
|
|
1214
|
+
message_thread_id: target.threadId,
|
|
1215
|
+
};
|
|
1216
|
+
delete message.pi_telegram_source_update_id;
|
|
1217
|
+
let bound, issued = false;
|
|
1218
|
+
const ownerCurrent = () => portsCurrent() &&
|
|
1219
|
+
readiness.bindCarrier === bind &&
|
|
1220
|
+
readiness.isCurrent === isCurrent &&
|
|
1221
|
+
isCurrent.call(readiness) &&
|
|
1222
|
+
captured.getAllowedUserId() === operator;
|
|
1223
|
+
const completionCurrent = () => !!bound &&
|
|
1224
|
+
Object.entries(bound.ports).every(([key, port]) => Reflect.get(bound.completion, key) === port) &&
|
|
1225
|
+
bound.ports.isCurrent.call(bound.completion);
|
|
1226
|
+
const assertSourceCurrent = () => {
|
|
1227
|
+
assertSource.call(admission);
|
|
1228
|
+
if (!ownerCurrent() || !completionCurrent() || !bound)
|
|
1229
|
+
throw new Error(`Held ${name} source owner is unavailable.`);
|
|
1230
|
+
captured.assertExecutionCurrent?.(bound.carrier);
|
|
1231
|
+
const current = inspectTelegramDeferredSource(bound.carrier);
|
|
1232
|
+
if (!current ||
|
|
1233
|
+
current.updateId !== source.updateId ||
|
|
1234
|
+
current.journalBindingKey !== source.journalBindingKey ||
|
|
1235
|
+
current.sourceSha256 !== source.sourceSha256 ||
|
|
1236
|
+
!ownerCurrent() ||
|
|
1237
|
+
!completionCurrent())
|
|
1238
|
+
throw new Error(`Held ${name} original changed.`);
|
|
1239
|
+
};
|
|
1240
|
+
return {
|
|
1241
|
+
get source() {
|
|
1242
|
+
return { ...source };
|
|
1243
|
+
},
|
|
1244
|
+
get command() {
|
|
1245
|
+
return { ...preparedCommand };
|
|
1246
|
+
},
|
|
1247
|
+
bindCarrier(value) {
|
|
1248
|
+
if (!ownerCurrent())
|
|
1249
|
+
return false;
|
|
1250
|
+
if (bound)
|
|
1251
|
+
return value === bound.carrier && completionCurrent();
|
|
1252
|
+
const completion = bind.call(readiness, value);
|
|
1253
|
+
if (!completion ||
|
|
1254
|
+
completion.source.updateId !== source.updateId ||
|
|
1255
|
+
completion.source.journalBindingKey !== source.journalBindingKey ||
|
|
1256
|
+
completion.source.sourceSha256 !== source.sourceSha256 ||
|
|
1257
|
+
!ownerCurrent())
|
|
1258
|
+
return false;
|
|
1259
|
+
const ports = {
|
|
1260
|
+
isCurrent: completion.isCurrent,
|
|
1261
|
+
reportCompleted: completion.reportCompleted,
|
|
1262
|
+
inspectCompletion: completion.inspectCompletion,
|
|
1263
|
+
};
|
|
1264
|
+
if (Object.values(ports).some((port) => typeof port !== "function"))
|
|
1265
|
+
return false;
|
|
1266
|
+
bound = { carrier: value, completion, ports };
|
|
1267
|
+
return completionCurrent();
|
|
1268
|
+
},
|
|
1269
|
+
async execute() {
|
|
1270
|
+
if (issued)
|
|
1271
|
+
return false;
|
|
1272
|
+
assertSourceCurrent();
|
|
1273
|
+
assertRecipient.call(admission);
|
|
1274
|
+
assertSourceCurrent();
|
|
1275
|
+
issued = true;
|
|
1276
|
+
if (completion === "before-effect" &&
|
|
1277
|
+
!bound.ports.reportCompleted.call(bound.completion))
|
|
1278
|
+
return false;
|
|
1279
|
+
return run({
|
|
1280
|
+
message,
|
|
1281
|
+
carrier: bound.carrier,
|
|
1282
|
+
source: { ...source },
|
|
1283
|
+
command: { ...preparedCommand },
|
|
1284
|
+
assertRecipient,
|
|
1285
|
+
reportCompleted: () => bound.ports.reportCompleted.call(bound.completion),
|
|
1286
|
+
assertSemantic() {
|
|
1287
|
+
assertSourceCurrent();
|
|
1288
|
+
assertRecipient.call(admission);
|
|
1289
|
+
},
|
|
1290
|
+
});
|
|
1291
|
+
},
|
|
1292
|
+
inspectCompletion() {
|
|
1293
|
+
return ownerCurrent() && completionCurrent()
|
|
1294
|
+
? completion === "queue-receipt"
|
|
1295
|
+
? inspectQueuedCompletion?.()
|
|
1296
|
+
: bound.ports.inspectCompletion.call(bound.completion)
|
|
1297
|
+
: undefined;
|
|
1298
|
+
},
|
|
1299
|
+
};
|
|
1300
|
+
};
|
|
1301
|
+
const prepareHeldReplyPlan = (ctx, effects, run, replyDelivery = "detached") => {
|
|
1302
|
+
const send = effects.sendTextReply, captured = { ...deps };
|
|
1303
|
+
return {
|
|
1304
|
+
captured,
|
|
1305
|
+
portsCurrent: () => effects.sendTextReply === send,
|
|
1306
|
+
async run(input) {
|
|
1307
|
+
const semantic = guardSelectedEffect(input.assertSemantic);
|
|
1308
|
+
const complete = () => {
|
|
1309
|
+
input.assertSemantic();
|
|
1310
|
+
if (!input.reportCompleted())
|
|
1311
|
+
throw new Error(`Held ${input.command.name} source completion refused.`);
|
|
1312
|
+
input.assertRecipient();
|
|
1313
|
+
};
|
|
1314
|
+
await run(captured, semantic, complete, async (text, options) => {
|
|
1315
|
+
complete();
|
|
1316
|
+
const deliver = async () => {
|
|
1317
|
+
input.assertRecipient();
|
|
1318
|
+
await send.call(effects, input.message.chat.id, input.message.message_id, text, {
|
|
1319
|
+
...options,
|
|
1320
|
+
target: {
|
|
1321
|
+
chatId: input.message.chat.id,
|
|
1322
|
+
threadId: input.message.message_thread_id,
|
|
1323
|
+
},
|
|
1324
|
+
assertAuthority: input.assertRecipient,
|
|
1325
|
+
});
|
|
1326
|
+
input.assertRecipient();
|
|
1327
|
+
};
|
|
1328
|
+
// Transport waiting never changes the already reported semantic completion or licenses another effect.
|
|
1329
|
+
if (replyDelivery === "inline")
|
|
1330
|
+
await deliver();
|
|
1331
|
+
else
|
|
1332
|
+
scheduleTelegramCommandEffect(ctx, input.command.name, "selected-reply", captured, deliver, input.assertRecipient);
|
|
1333
|
+
});
|
|
1334
|
+
return true;
|
|
1335
|
+
},
|
|
1336
|
+
};
|
|
1337
|
+
};
|
|
1338
|
+
const prepareHeldQueuePlan = (ctx, template) => {
|
|
1339
|
+
const captured = { ...deps }, owner = captured.heldTurn, enqueue = owner.enqueue;
|
|
1340
|
+
let retained;
|
|
1341
|
+
const portsCurrent = () => deps.heldTurn === owner &&
|
|
1342
|
+
owner.enqueue === enqueue &&
|
|
1343
|
+
(!template || template.isCurrent()) &&
|
|
1344
|
+
deps.heldTurn === owner &&
|
|
1345
|
+
owner.enqueue === enqueue;
|
|
1346
|
+
return {
|
|
1347
|
+
captured,
|
|
1348
|
+
portsCurrent,
|
|
1349
|
+
async run(input) {
|
|
1350
|
+
input.assertSemantic();
|
|
1351
|
+
if (template && !isDeepStrictEqual(input.command, template.command))
|
|
1352
|
+
return false;
|
|
1353
|
+
const { carrier, source } = input;
|
|
1354
|
+
if (!carrier || !source)
|
|
1355
|
+
return false;
|
|
1356
|
+
const signal = getTelegramUpdateExecutionFence(carrier)?.signal;
|
|
1357
|
+
if (!signal || signal.aborted)
|
|
1358
|
+
return false;
|
|
1359
|
+
const queue = input.queue ?? prepareTelegramDeferredQueueAdmission(carrier);
|
|
1360
|
+
if (!queue || !queue.isCurrent())
|
|
1361
|
+
return false;
|
|
1362
|
+
const ports = {
|
|
1363
|
+
isCurrent: queue.isCurrent,
|
|
1364
|
+
prepareCompletionScope: queue.prepareCompletionScope,
|
|
1365
|
+
inspectCompletion: queue.inspectCompletion,
|
|
1366
|
+
};
|
|
1367
|
+
if (Object.values(ports).some((port) => typeof port !== "function"))
|
|
1368
|
+
return false;
|
|
1369
|
+
const current = () => {
|
|
1370
|
+
if (!portsCurrent() ||
|
|
1371
|
+
!Object.entries(ports).every(([key, value]) => Reflect.get(queue, key) === value) ||
|
|
1372
|
+
!ports.isCurrent.call(queue))
|
|
1373
|
+
return false;
|
|
1374
|
+
try {
|
|
1375
|
+
input.assertRecipient();
|
|
1376
|
+
return portsCurrent() && ports.isCurrent.call(queue);
|
|
1377
|
+
}
|
|
1378
|
+
catch {
|
|
1379
|
+
return false;
|
|
1380
|
+
}
|
|
1381
|
+
};
|
|
1382
|
+
const sameTarget = carrier.message_thread_id === input.message.message_thread_id;
|
|
1383
|
+
const message = carryTelegramUpdateExecutionFence(carrier, {
|
|
1384
|
+
...carrier,
|
|
1385
|
+
...input.message,
|
|
1386
|
+
pi_telegram_source_update_id: source.updateId,
|
|
1387
|
+
...(template ? { text: template.expanded, caption: undefined } : {}),
|
|
1388
|
+
...(sameTarget ? {} : { message_id: 0, reply_to_message: undefined }),
|
|
1389
|
+
});
|
|
1390
|
+
const completionSha256 = createHash("sha256")
|
|
1391
|
+
.update(JSON.stringify([
|
|
1392
|
+
"held-command-v1",
|
|
1393
|
+
randomUUID(),
|
|
1394
|
+
source,
|
|
1395
|
+
input.command,
|
|
1396
|
+
getTelegramCommandMessageTarget(message),
|
|
1397
|
+
]))
|
|
1398
|
+
.digest("hex");
|
|
1399
|
+
const admission = {
|
|
1400
|
+
assertCurrent: input.assertSemantic,
|
|
1401
|
+
report(receipts) {
|
|
1402
|
+
input.assertSemantic();
|
|
1403
|
+
const receipt = receipts[0];
|
|
1404
|
+
if (retained ||
|
|
1405
|
+
receipts.length !== 1 ||
|
|
1406
|
+
!receipt ||
|
|
1407
|
+
receipt.queueKind !== "prompt" ||
|
|
1408
|
+
receipt.journalBindingKey !== source.journalBindingKey ||
|
|
1409
|
+
receipt.sourceUpdateIds.length !== 1 ||
|
|
1410
|
+
receipt.sourceUpdateIds[0] !== source.updateId ||
|
|
1411
|
+
heldQueueReceipts.has(receipt.receiptId) ||
|
|
1412
|
+
!current())
|
|
1413
|
+
throw new Error("Held command needs one exact original queue receipt.");
|
|
1414
|
+
const expected = structuredClone(receipt);
|
|
1415
|
+
retained = {
|
|
1416
|
+
ctx,
|
|
1417
|
+
signal,
|
|
1418
|
+
receipt,
|
|
1419
|
+
queue,
|
|
1420
|
+
completionSha256,
|
|
1421
|
+
current: () => current() && isDeepStrictEqual(receipt, expected),
|
|
1422
|
+
};
|
|
1423
|
+
watchHeldQueueOwner(signal);
|
|
1424
|
+
heldQueueReceipts.set(receipt.receiptId, signal.aborted ? null : retained);
|
|
1425
|
+
if (!reportTelegramQueueAdmission([carrier], [receipt]))
|
|
1426
|
+
throw new Error("Held command queue admission refused.");
|
|
1427
|
+
},
|
|
1428
|
+
};
|
|
1429
|
+
if (template)
|
|
1430
|
+
await enqueue.call(owner, message, ctx, "prompt", admission);
|
|
1431
|
+
else
|
|
1432
|
+
await handleTelegramContinueCommand(message, ctx, {
|
|
1433
|
+
enqueueContinueTurn: (value, context) => enqueue.call(owner, value, context, "continue", admission),
|
|
1434
|
+
});
|
|
1435
|
+
return !!retained && current();
|
|
1436
|
+
},
|
|
1437
|
+
inspectQueuedCompletion() {
|
|
1438
|
+
if (!retained || !retained.current())
|
|
1439
|
+
return undefined;
|
|
1440
|
+
const proof = retained.queue.inspectCompletion(retained.receipt);
|
|
1441
|
+
if (proof && retained.current()) {
|
|
1442
|
+
heldQueueReceipts.delete(retained.receipt.receiptId);
|
|
1443
|
+
return { ...proof };
|
|
1444
|
+
}
|
|
1445
|
+
return undefined;
|
|
1446
|
+
},
|
|
1447
|
+
};
|
|
1448
|
+
};
|
|
1449
|
+
const heldPlans = {
|
|
1450
|
+
status: {
|
|
1451
|
+
completion: "before-effect",
|
|
1452
|
+
effect: "showStatus",
|
|
1453
|
+
prepare(ctx, effects) {
|
|
1454
|
+
const scopedStatus = effects?.showStatus;
|
|
1455
|
+
const captured = {
|
|
1456
|
+
...deps,
|
|
1457
|
+
showStatus: scopedStatus
|
|
1458
|
+
? (message, context, options) => {
|
|
1459
|
+
const address = getTelegramCommandMessageTarget(message);
|
|
1460
|
+
return scopedStatus.call(effects, address.chatId, address.replyToMessageId, context, address.threadId, options);
|
|
1461
|
+
}
|
|
1462
|
+
: deps.showStatus,
|
|
1463
|
+
};
|
|
1464
|
+
return {
|
|
1465
|
+
captured,
|
|
1466
|
+
portsCurrent: () => !effects || effects.showStatus === scopedStatus,
|
|
1467
|
+
async run({ message, command, assertRecipient }) {
|
|
1468
|
+
const recipientOptions = { assertAuthority: assertRecipient };
|
|
1469
|
+
return handleTelegramCommandRuntime(command.name, message, ctx, {
|
|
1470
|
+
...captured,
|
|
1471
|
+
assertExecutionCurrent: assertRecipient,
|
|
1472
|
+
showStatus: (value, context) => captured.showStatus(value, context, recipientOptions),
|
|
1473
|
+
}, command.args);
|
|
1474
|
+
},
|
|
1475
|
+
};
|
|
1476
|
+
},
|
|
1477
|
+
},
|
|
1478
|
+
abort: {
|
|
1479
|
+
completion: "after-semantics",
|
|
1480
|
+
effect: "sendTextReply",
|
|
1481
|
+
prepare(ctx, effects, _command, delivery) {
|
|
1482
|
+
return prepareHeldReplyPlan(ctx, effects, async (captured, semantic, _complete, reply) => {
|
|
1483
|
+
await handleTelegramAbortCommand({
|
|
1484
|
+
hasAbortHandler: semantic(captured.hasAbortHandler),
|
|
1485
|
+
hasActiveTelegramTurn: semantic(captured.hasActiveTelegramTurn),
|
|
1486
|
+
clearPendingModelSwitch: semantic(captured.clearPendingModelSwitch),
|
|
1487
|
+
cancelNextTransitionAnnouncements: captured.cancelNextTransitionAnnouncements &&
|
|
1488
|
+
semantic(captured.cancelNextTransitionAnnouncements),
|
|
1489
|
+
abortCurrentTurn: semantic(captured.abortCurrentTurn),
|
|
1490
|
+
setFoldQueuedPromptsIntoHistory: semantic(captured.setFoldQueuedPromptsIntoHistory),
|
|
1491
|
+
updateStatus: semantic(() => captured.updateStatus(ctx)),
|
|
1492
|
+
sendTextReply: reply,
|
|
1493
|
+
});
|
|
1494
|
+
}, delivery);
|
|
1495
|
+
},
|
|
1496
|
+
},
|
|
1497
|
+
stop: {
|
|
1498
|
+
completion: "after-semantics",
|
|
1499
|
+
effect: "sendTextReply",
|
|
1500
|
+
available: () => typeof deps.cancelNextTransitionAnnouncements === "function" &&
|
|
1501
|
+
typeof deps.clearQueuedTelegramItems === "function",
|
|
1502
|
+
prepare(ctx, effects, _command, delivery) {
|
|
1503
|
+
return prepareHeldReplyPlan(ctx, effects, async (captured, semantic, _complete, reply) => {
|
|
1504
|
+
await handleTelegramStopCommand({
|
|
1505
|
+
hasAbortHandler: semantic(captured.hasAbortHandler),
|
|
1506
|
+
clearPendingModelSwitch: semantic(captured.clearPendingModelSwitch),
|
|
1507
|
+
cancelNextTransitionAnnouncements: semantic(captured.cancelNextTransitionAnnouncements),
|
|
1508
|
+
clearQueuedTelegramItems: semantic(() => captured.clearQueuedTelegramItems(ctx)),
|
|
1509
|
+
setFoldQueuedPromptsIntoHistory: semantic(captured.setFoldQueuedPromptsIntoHistory),
|
|
1510
|
+
abortCurrentTurn: semantic(captured.abortCurrentTurn),
|
|
1511
|
+
updateStatus: semantic(() => captured.updateStatus(ctx)),
|
|
1512
|
+
sendTextReply: reply,
|
|
1513
|
+
});
|
|
1514
|
+
}, delivery);
|
|
1515
|
+
},
|
|
1516
|
+
},
|
|
1517
|
+
next: {
|
|
1518
|
+
completion: "after-semantics",
|
|
1519
|
+
effect: "sendTextReply",
|
|
1520
|
+
available: () => typeof deps.requestNextDispatchAnnouncement === "function" &&
|
|
1521
|
+
typeof deps.markActiveTurnNextAbortAnnouncement === "function",
|
|
1522
|
+
prepare(ctx, effects, _command, delivery) {
|
|
1523
|
+
return prepareHeldReplyPlan(ctx, effects, async (captured, semantic, complete, reply) => {
|
|
1524
|
+
await handleTelegramNextCommand({
|
|
1525
|
+
hasAbortHandler: semantic(captured.hasAbortHandler),
|
|
1526
|
+
isIdle: semantic(() => captured.isIdle(ctx)),
|
|
1527
|
+
hasQueuedItems: semantic(captured.hasQueuedTelegramItems),
|
|
1528
|
+
clearPendingModelSwitch: semantic(captured.clearPendingModelSwitch),
|
|
1529
|
+
abortCurrentTurn: semantic(captured.abortCurrentTurn),
|
|
1530
|
+
dispatchNextQueuedTurn: semantic(() => captured.dispatchNextQueuedTelegramTurn(ctx)),
|
|
1531
|
+
requestNextDispatchAnnouncement: semantic(captured.requestNextDispatchAnnouncement),
|
|
1532
|
+
markActiveTurnNextAbortAnnouncement: semantic(captured.markActiveTurnNextAbortAnnouncement),
|
|
1533
|
+
clearFoldForDispatch: semantic(() => captured.setFoldQueuedPromptsIntoHistory(false)),
|
|
1534
|
+
updateStatus() {
|
|
1535
|
+
semantic(() => captured.updateStatus(ctx))();
|
|
1536
|
+
complete();
|
|
1537
|
+
},
|
|
1538
|
+
sendTextReply: reply,
|
|
1539
|
+
});
|
|
1540
|
+
}, delivery);
|
|
1541
|
+
},
|
|
1542
|
+
},
|
|
1543
|
+
continue: {
|
|
1544
|
+
completion: "queue-receipt",
|
|
1545
|
+
available: () => typeof deps.heldTurn?.enqueue === "function",
|
|
1546
|
+
prepare(ctx) {
|
|
1547
|
+
return prepareHeldQueuePlan(ctx);
|
|
1548
|
+
},
|
|
1549
|
+
},
|
|
1550
|
+
};
|
|
1551
|
+
const getHeldTemplatePlan = (name) => {
|
|
1552
|
+
const owner = deps.heldTurn, templates = owner?.templates, get = templates?.getCommands, expand = templates?.expand;
|
|
1553
|
+
if (!owner ||
|
|
1554
|
+
!templates ||
|
|
1555
|
+
typeof owner.enqueue !== "function" ||
|
|
1556
|
+
typeof get !== "function" ||
|
|
1557
|
+
typeof expand !== "function" ||
|
|
1558
|
+
getTelegramReservedCommandNames().includes(name) ||
|
|
1559
|
+
findTelegramExtensionCommand(name))
|
|
1560
|
+
return undefined;
|
|
1561
|
+
const getIdentity = () => {
|
|
1562
|
+
const matching = get
|
|
1563
|
+
.call(templates)
|
|
1564
|
+
.filter((value) => value.command === name);
|
|
1565
|
+
const selected = matching[0];
|
|
1566
|
+
return matching.length === 1 &&
|
|
1567
|
+
selected &&
|
|
1568
|
+
typeof selected.path === "string" &&
|
|
1569
|
+
selected.path.trim()
|
|
1570
|
+
? { command: selected.command, path: selected.path }
|
|
1571
|
+
: undefined;
|
|
1572
|
+
};
|
|
1573
|
+
let identity;
|
|
1574
|
+
try {
|
|
1575
|
+
identity = getIdentity();
|
|
1576
|
+
}
|
|
1577
|
+
catch {
|
|
1578
|
+
return undefined;
|
|
1579
|
+
}
|
|
1580
|
+
if (!identity)
|
|
1581
|
+
return undefined;
|
|
1582
|
+
const current = () => {
|
|
1583
|
+
if (deps.heldTurn !== owner ||
|
|
1584
|
+
owner.templates !== templates ||
|
|
1585
|
+
templates.getCommands !== get ||
|
|
1586
|
+
templates.expand !== expand ||
|
|
1587
|
+
findTelegramExtensionCommand(name))
|
|
1588
|
+
return false;
|
|
1589
|
+
try {
|
|
1590
|
+
return (isDeepStrictEqual(getIdentity(), identity) &&
|
|
1591
|
+
deps.heldTurn === owner &&
|
|
1592
|
+
owner.templates === templates &&
|
|
1593
|
+
templates.getCommands === get &&
|
|
1594
|
+
templates.expand === expand &&
|
|
1595
|
+
!findTelegramExtensionCommand(name));
|
|
1040
1596
|
}
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
deps.assertExecutionCurrent?.(firstMessage);
|
|
1044
|
-
if (handled)
|
|
1045
|
-
return;
|
|
1046
|
-
if (command && deps.executeExtensionCommand) {
|
|
1047
|
-
const handledByExtension = await deps.executeExtensionCommand(command, messages[0], ctx);
|
|
1048
|
-
deps.assertExecutionCurrent?.(firstMessage);
|
|
1049
|
-
if (handledByExtension)
|
|
1050
|
-
return;
|
|
1597
|
+
catch {
|
|
1598
|
+
return false;
|
|
1051
1599
|
}
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
return;
|
|
1600
|
+
};
|
|
1601
|
+
if (!current())
|
|
1602
|
+
return undefined;
|
|
1603
|
+
return {
|
|
1604
|
+
completion: "queue-receipt",
|
|
1605
|
+
template: true,
|
|
1606
|
+
prepare(ctx, _effects, command) {
|
|
1607
|
+
if (!command || command.name !== name || !current())
|
|
1608
|
+
return undefined;
|
|
1609
|
+
const captured = { ...command };
|
|
1610
|
+
let expanded;
|
|
1611
|
+
try {
|
|
1612
|
+
expanded = expand.call(templates, captured.name, captured.args);
|
|
1061
1613
|
}
|
|
1614
|
+
catch {
|
|
1615
|
+
return undefined;
|
|
1616
|
+
}
|
|
1617
|
+
if (typeof expanded !== "string" || !current())
|
|
1618
|
+
return undefined;
|
|
1619
|
+
return prepareHeldQueuePlan(ctx, {
|
|
1620
|
+
command: captured,
|
|
1621
|
+
expanded,
|
|
1622
|
+
isCurrent() {
|
|
1623
|
+
if (!current())
|
|
1624
|
+
return false;
|
|
1625
|
+
try {
|
|
1626
|
+
return (expand.call(templates, captured.name, captured.args) ===
|
|
1627
|
+
expanded && current());
|
|
1628
|
+
}
|
|
1629
|
+
catch {
|
|
1630
|
+
return false;
|
|
1631
|
+
}
|
|
1632
|
+
},
|
|
1633
|
+
});
|
|
1634
|
+
},
|
|
1635
|
+
};
|
|
1636
|
+
};
|
|
1637
|
+
const getHeldPlan = (name, effects) => {
|
|
1638
|
+
if (!Object.hasOwn(heldPlans, name))
|
|
1639
|
+
return getHeldTemplatePlan(name);
|
|
1640
|
+
const plan = heldPlans[name];
|
|
1641
|
+
if (plan.available?.() === false)
|
|
1642
|
+
return undefined;
|
|
1643
|
+
if ((plan.effect &&
|
|
1644
|
+
effects !== undefined &&
|
|
1645
|
+
(!effects || typeof effects[plan.effect] !== "function")) ||
|
|
1646
|
+
(plan.effect === "sendTextReply" && !effects) ||
|
|
1647
|
+
(plan.effect === "showStatus" &&
|
|
1648
|
+
effects === undefined &&
|
|
1649
|
+
typeof deps.showStatus !== "function"))
|
|
1650
|
+
return undefined;
|
|
1651
|
+
return plan;
|
|
1652
|
+
};
|
|
1653
|
+
const prepareSelectedPlan = (command, messages, ctx, admission) => {
|
|
1654
|
+
const original = messages[0];
|
|
1655
|
+
if (!["status", "abort", "stop", "next"].includes(command.name) ||
|
|
1656
|
+
messages.length !== 1 ||
|
|
1657
|
+
!original ||
|
|
1658
|
+
typeof admission.assertSourceCurrent !== "function" ||
|
|
1659
|
+
typeof admission.assertRecipientCurrent !== "function" ||
|
|
1660
|
+
typeof admission.reportCompleted !== "function")
|
|
1661
|
+
return undefined;
|
|
1662
|
+
const captured = { ...deps }, parsed = { ...command }, message = parsed.name === "status"
|
|
1663
|
+
? copySelectedMenuCarrier(original)
|
|
1664
|
+
: copySelectedReplyAnchor(original);
|
|
1665
|
+
const assertRecipientCurrent = admission.assertRecipientCurrent, report = admission.reportCompleted;
|
|
1666
|
+
const effects = parsed.name === "status"
|
|
1667
|
+
? undefined
|
|
1668
|
+
: {
|
|
1669
|
+
async sendTextReply(_chat, _anchor, text, options) {
|
|
1670
|
+
await captured.sendTextReply(message, text, {
|
|
1671
|
+
parseMode: options.parseMode,
|
|
1672
|
+
assertAuthority: options.assertAuthority,
|
|
1673
|
+
});
|
|
1674
|
+
},
|
|
1675
|
+
};
|
|
1676
|
+
const plan = getHeldPlan(parsed.name, effects);
|
|
1677
|
+
if (!plan ||
|
|
1678
|
+
plan.completion === "queue-receipt" ||
|
|
1679
|
+
(parsed.name !== "status" && typeof captured.sendTextReply !== "function"))
|
|
1680
|
+
return undefined;
|
|
1681
|
+
const prepared = plan.prepare(ctx, effects, parsed, "inline");
|
|
1682
|
+
if (!prepared)
|
|
1683
|
+
return undefined;
|
|
1684
|
+
const semantic = createSelectedSemanticGuard(admission, () => captured.assertExecutionCurrent?.(original), plan.completion === "before-effect");
|
|
1685
|
+
const assertCurrent = () => {
|
|
1686
|
+
semantic();
|
|
1687
|
+
if (!prepared.portsCurrent())
|
|
1688
|
+
throw new Error(`Selected ${parsed.name} plan owner is unavailable.`);
|
|
1689
|
+
};
|
|
1690
|
+
return issueSelectedOnce(assertCurrent, async () => {
|
|
1691
|
+
if (plan.completion === "before-effect" && !report.call(admission))
|
|
1692
|
+
return false;
|
|
1693
|
+
return prepared.run({
|
|
1694
|
+
message,
|
|
1695
|
+
command: parsed,
|
|
1696
|
+
assertSemantic: assertCurrent,
|
|
1697
|
+
assertRecipient: assertRecipientCurrent,
|
|
1698
|
+
reportCompleted: () => report.call(admission),
|
|
1699
|
+
});
|
|
1700
|
+
});
|
|
1701
|
+
};
|
|
1702
|
+
const prepareSelectedHelp = (name) => (command, messages, ctx, admission) => {
|
|
1703
|
+
const original = messages[0];
|
|
1704
|
+
if (command.name !== name ||
|
|
1705
|
+
messages.length !== 1 ||
|
|
1706
|
+
!original ||
|
|
1707
|
+
!canPairTelegramUserFromCommandMessage(original) ||
|
|
1708
|
+
original.from?.id === undefined ||
|
|
1709
|
+
original.from.is_bot)
|
|
1710
|
+
return undefined;
|
|
1711
|
+
const captured = { ...deps }, userId = original.from.id;
|
|
1712
|
+
if (captured.getAllowedUserId() !== userId)
|
|
1713
|
+
return undefined;
|
|
1714
|
+
const message = {
|
|
1715
|
+
...copySelectedReplyAnchor(original),
|
|
1716
|
+
from: { ...original.from },
|
|
1717
|
+
};
|
|
1718
|
+
const assertRecipientCurrent = admission.assertRecipientCurrent, reportCompleted = admission.reportCompleted;
|
|
1719
|
+
const assertAdmissionCurrent = createSelectedSemanticGuard(admission, () => captured.assertExecutionCurrent?.(original), false);
|
|
1720
|
+
const assertSemanticCurrent = () => {
|
|
1721
|
+
assertAdmissionCurrent();
|
|
1722
|
+
if (captured.getAllowedUserId() !== userId)
|
|
1723
|
+
throw new Error(`Selected ${name} operator authority is unavailable.`);
|
|
1724
|
+
};
|
|
1725
|
+
return issueSelectedOnce(assertSemanticCurrent, () => handleTelegramHelpCommand(name, message, ctx, {
|
|
1726
|
+
...captured,
|
|
1727
|
+
persistAllowedUserId: async () => {
|
|
1728
|
+
throw new Error(`Selected ${name} cannot acquire pairing authority.`);
|
|
1729
|
+
},
|
|
1730
|
+
}, { assertSemanticCurrent, assertRecipientCurrent, reportCompleted }));
|
|
1731
|
+
};
|
|
1732
|
+
const prepareSelectedConfirmation = (name) => (command, messages, ctx, admission) => {
|
|
1733
|
+
const original = messages[0], captured = { ...deps };
|
|
1734
|
+
if (command.name !== name ||
|
|
1735
|
+
messages.length !== 1 ||
|
|
1736
|
+
!original ||
|
|
1737
|
+
!captured.sendInteractiveMessage)
|
|
1738
|
+
return undefined;
|
|
1739
|
+
const target = getTelegramCommandMessageTarget(original);
|
|
1740
|
+
const assertRecipientCurrent = admission.assertRecipientCurrent, reportCompleted = admission.reportCompleted;
|
|
1741
|
+
const openConfirmation = name === "new"
|
|
1742
|
+
? openTelegramNewConfirmation
|
|
1743
|
+
: openTelegramCompactConfirmation;
|
|
1744
|
+
const assertSemanticCurrent = createSelectedSemanticGuard(admission, () => captured.assertExecutionCurrent?.(original), true);
|
|
1745
|
+
return issueSelectedOnce(assertSemanticCurrent, async () => {
|
|
1746
|
+
if (!reportCompleted())
|
|
1747
|
+
return false;
|
|
1748
|
+
assertRecipientCurrent();
|
|
1749
|
+
scheduleTelegramCommandEffect(ctx, name, "confirmation-render", captured, () => openConfirmation(target, { sendInteractiveMessage: captured.sendInteractiveMessage }, assertRecipientCurrent), assertRecipientCurrent);
|
|
1750
|
+
return true;
|
|
1751
|
+
});
|
|
1752
|
+
};
|
|
1753
|
+
return Object.assign(handle, {
|
|
1754
|
+
/** Command-only producer execution; Routing must supply the captured plan and released exact-source authority. */
|
|
1755
|
+
prepareSelectedExtensionCommand(prepared, messages, ctx, admission) {
|
|
1756
|
+
const original = messages[0], captured = { ...deps }, plan = prepared.plan;
|
|
1757
|
+
if (messages.length !== 1 ||
|
|
1758
|
+
!original ||
|
|
1759
|
+
plan.kind !== "command-only" ||
|
|
1760
|
+
typeof captured.sendTextReply !== "function" ||
|
|
1761
|
+
typeof admission.reportCompleted !== "function")
|
|
1762
|
+
return undefined;
|
|
1763
|
+
const execute = plan.execute, assertRegistrationCurrent = prepared.assertRegistrationCurrent;
|
|
1764
|
+
const assertSourceCurrent = admission.assertSourceCurrent, recipientCurrent = admission.assertRecipientCurrent;
|
|
1765
|
+
const reportCompleted = admission.reportCompleted;
|
|
1766
|
+
// Only a body-free fixed recipient/anchor enters detached delivery, never the original's admission symbols.
|
|
1767
|
+
const message = copySelectedReplyAnchor(original);
|
|
1768
|
+
const assertRecipientCurrent = () => {
|
|
1769
|
+
if (captured.isContextActive?.(ctx) === false)
|
|
1770
|
+
throw new Error("Selected extension context changed.");
|
|
1771
|
+
assertRegistrationCurrent();
|
|
1772
|
+
recipientCurrent();
|
|
1773
|
+
assertRegistrationCurrent();
|
|
1774
|
+
if (captured.isContextActive?.(ctx) === false)
|
|
1775
|
+
throw new Error("Selected extension context changed.");
|
|
1776
|
+
};
|
|
1777
|
+
const assertCurrent = () => {
|
|
1778
|
+
assertSourceCurrent();
|
|
1779
|
+
captured.assertExecutionCurrent?.(original);
|
|
1780
|
+
assertRecipientCurrent();
|
|
1781
|
+
assertSourceCurrent();
|
|
1782
|
+
captured.assertExecutionCurrent?.(original);
|
|
1783
|
+
};
|
|
1784
|
+
try {
|
|
1785
|
+
assertCurrent();
|
|
1062
1786
|
}
|
|
1063
|
-
|
|
1064
|
-
|
|
1787
|
+
catch {
|
|
1788
|
+
return undefined;
|
|
1789
|
+
}
|
|
1790
|
+
let issued = false, reportAttempted = false;
|
|
1791
|
+
const execution = Object.freeze({
|
|
1792
|
+
assertCurrent,
|
|
1793
|
+
reportCompleted() {
|
|
1794
|
+
if (!issued || reportAttempted)
|
|
1795
|
+
return false;
|
|
1796
|
+
// Lost/refused reports never authorize another completion attempt or producer execution.
|
|
1797
|
+
reportAttempted = true;
|
|
1798
|
+
assertCurrent();
|
|
1799
|
+
return reportCompleted();
|
|
1800
|
+
},
|
|
1801
|
+
reply(text) {
|
|
1802
|
+
assertRecipientCurrent();
|
|
1803
|
+
if (typeof text !== "string")
|
|
1804
|
+
throw new Error("Selected extension reply requires text.");
|
|
1805
|
+
scheduleTelegramCommandEffect(ctx, "extension", "selected-reply", captured, () => captured.sendTextReply(message, text, {
|
|
1806
|
+
assertAuthority: assertRecipientCurrent,
|
|
1807
|
+
}), assertRecipientCurrent);
|
|
1808
|
+
},
|
|
1809
|
+
});
|
|
1810
|
+
return async () => {
|
|
1811
|
+
if (issued)
|
|
1812
|
+
return;
|
|
1813
|
+
issued = true;
|
|
1814
|
+
assertCurrent();
|
|
1815
|
+
await execute(execution);
|
|
1816
|
+
// Return, rejection and reply delivery are deliberately not semantic completion reports.
|
|
1817
|
+
};
|
|
1818
|
+
},
|
|
1819
|
+
/** Selected help/start require the existing authenticated owner; cold pairing remains ordinary first contact. */
|
|
1820
|
+
prepareSelectedStartCommand: prepareSelectedHelp("start"),
|
|
1821
|
+
prepareSelectedHelpCommand: prepareSelectedHelp("help"),
|
|
1822
|
+
/** Selected new handles only confirmation; the later authenticated callback owns replacement admission. */
|
|
1823
|
+
prepareSelectedNewCommand: prepareSelectedConfirmation("new"),
|
|
1824
|
+
/** Selected compaction handles only confirmation; the later authenticated callback owns actual compaction. */
|
|
1825
|
+
prepareSelectedCompactCommand: prepareSelectedConfirmation("compact"),
|
|
1826
|
+
/** Bare naming completes only after the exact dialog owner confirms current publication, not delivery alone. */
|
|
1827
|
+
prepareSelectedNameDialogCommand(command, messages, ctx, admission) {
|
|
1828
|
+
const original = messages[0], captured = { ...deps };
|
|
1829
|
+
if (command.name !== "name" ||
|
|
1830
|
+
command.args.trim() ||
|
|
1831
|
+
messages.length !== 1 ||
|
|
1832
|
+
!original ||
|
|
1833
|
+
!captured.openThreadNameDialog)
|
|
1834
|
+
return undefined;
|
|
1835
|
+
const message = copySelectedReplyAnchor(original);
|
|
1836
|
+
const assertSourceCurrent = admission.assertSourceCurrent, assertRecipientCurrent = admission.assertRecipientCurrent;
|
|
1837
|
+
const reportCompleted = admission.reportCompleted;
|
|
1838
|
+
const assertSemanticCurrent = createSelectedSemanticGuard(admission, () => captured.assertExecutionCurrent?.(original), true);
|
|
1839
|
+
return issueSelectedOnce(assertSemanticCurrent, async () => {
|
|
1840
|
+
const publication = await captured.openThreadNameDialog(message, ctx, {
|
|
1841
|
+
assertSemanticCurrent,
|
|
1842
|
+
assertRecipientCurrent,
|
|
1843
|
+
});
|
|
1844
|
+
assertSemanticCurrent();
|
|
1845
|
+
const assertPublished = publication?.assertPublished;
|
|
1846
|
+
if (typeof assertPublished !== "function")
|
|
1847
|
+
throw new Error("Selected name dialog publication is unconfirmed.");
|
|
1848
|
+
assertPublished();
|
|
1849
|
+
assertSemanticCurrent();
|
|
1850
|
+
assertPublished();
|
|
1851
|
+
assertSourceCurrent();
|
|
1852
|
+
captured.assertExecutionCurrent?.(original);
|
|
1853
|
+
if (!reportCompleted())
|
|
1854
|
+
throw new Error("Selected name source completion refused.");
|
|
1855
|
+
return true;
|
|
1856
|
+
});
|
|
1857
|
+
},
|
|
1858
|
+
/** Explicit naming retains source authority until its owner result; bare dialogs use their own publication leaf. */
|
|
1859
|
+
prepareSelectedNameCommand(command, messages, ctx, admission) {
|
|
1860
|
+
const original = messages[0], threadName = command.args.trim(), captured = { ...deps };
|
|
1861
|
+
if (command.name !== "name" ||
|
|
1862
|
+
messages.length !== 1 ||
|
|
1863
|
+
!original ||
|
|
1864
|
+
!threadName ||
|
|
1865
|
+
(/^[A-Z]$/.test(threadName)
|
|
1866
|
+
? !captured.resetCurrentThreadName
|
|
1867
|
+
: !captured.renameCurrentThread))
|
|
1868
|
+
return undefined;
|
|
1869
|
+
const message = copySelectedReplyAnchor(original);
|
|
1870
|
+
const assertRecipientCurrent = admission.assertRecipientCurrent, reportCompleted = admission.reportCompleted;
|
|
1871
|
+
const assertSemanticCurrent = createSelectedSemanticGuard(admission, () => captured.assertExecutionCurrent?.(original), true);
|
|
1872
|
+
return issueSelectedOnce(assertSemanticCurrent, async () => {
|
|
1873
|
+
await handleTelegramNameCommand(message, ctx, threadName, captured, {
|
|
1874
|
+
assertSemanticCurrent,
|
|
1875
|
+
assertRecipientCurrent,
|
|
1876
|
+
reportCompleted,
|
|
1877
|
+
});
|
|
1878
|
+
return true;
|
|
1879
|
+
});
|
|
1065
1880
|
},
|
|
1881
|
+
/** Genuine leader originals capture their warm queue owner before release; no hold copy or semantic-completion report. */
|
|
1882
|
+
prepareSelectedQueueCommand(command, messages, ctx, admission) {
|
|
1883
|
+
const original = messages[0], parsed = { ...command };
|
|
1884
|
+
if (messages.length !== 1 ||
|
|
1885
|
+
!original ||
|
|
1886
|
+
typeof admission.assertSourceCurrent !== "function" ||
|
|
1887
|
+
typeof admission.assertRecipientCurrent !== "function")
|
|
1888
|
+
return undefined;
|
|
1889
|
+
const raw = Reflect.get(original, "text") ?? Reflect.get(original, "caption"), operator = deps.getAllowedUserId();
|
|
1890
|
+
const actual = typeof raw === "string" ? parseTelegramCommand(raw) : undefined;
|
|
1891
|
+
if (!isDeepStrictEqual(actual, parsed) ||
|
|
1892
|
+
typeof operator !== "number" ||
|
|
1893
|
+
original.chat.type !== "private" ||
|
|
1894
|
+
original.chat.id !== operator ||
|
|
1895
|
+
original.from?.id !== operator ||
|
|
1896
|
+
original.from.is_bot ||
|
|
1897
|
+
admission.target.chatId !== operator ||
|
|
1898
|
+
!Number.isSafeInteger(admission.target.threadId) ||
|
|
1899
|
+
admission.target.threadId < 1)
|
|
1900
|
+
return undefined;
|
|
1901
|
+
const plan = getHeldPlan(parsed.name);
|
|
1902
|
+
if (!plan || plan.completion !== "queue-receipt")
|
|
1903
|
+
return undefined;
|
|
1904
|
+
const source = inspectTelegramDeferredSource(original), queue = prepareTelegramDeferredQueueAdmission(original);
|
|
1905
|
+
if (!source || source.completionSha256 || !queue?.isCurrent())
|
|
1906
|
+
return undefined;
|
|
1907
|
+
const prepared = plan.prepare(ctx, undefined, parsed);
|
|
1908
|
+
if (!prepared)
|
|
1909
|
+
return undefined;
|
|
1910
|
+
const target = { ...admission.target }, originalProjection = structuredClone(copySelectedMenuCarrier(original));
|
|
1911
|
+
const message = structuredClone(originalProjection);
|
|
1912
|
+
message.message_thread_id = target.threadId;
|
|
1913
|
+
const assertSource = admission.assertSourceCurrent, assertRecipient = admission.assertRecipientCurrent;
|
|
1914
|
+
const assertCurrent = () => {
|
|
1915
|
+
assertSource.call(admission);
|
|
1916
|
+
prepared.captured.assertExecutionCurrent?.(original);
|
|
1917
|
+
assertRecipient.call(admission);
|
|
1918
|
+
if (deps.getAllowedUserId() !== operator ||
|
|
1919
|
+
!queue.isCurrent() ||
|
|
1920
|
+
!prepared.portsCurrent() ||
|
|
1921
|
+
!isDeepStrictEqual(copySelectedMenuCarrier(original), originalProjection) ||
|
|
1922
|
+
!isDeepStrictEqual(inspectTelegramDeferredSource(original), source))
|
|
1923
|
+
throw new Error("Selected queue command source/owner changed.");
|
|
1924
|
+
assertSource.call(admission);
|
|
1925
|
+
prepared.captured.assertExecutionCurrent?.(original);
|
|
1926
|
+
};
|
|
1927
|
+
const execute = issueSelectedOnce(assertCurrent, async () => {
|
|
1928
|
+
const release = acquireTelegramUpdateRouting(original);
|
|
1929
|
+
try {
|
|
1930
|
+
assertCurrent();
|
|
1931
|
+
return await prepared.run({
|
|
1932
|
+
message,
|
|
1933
|
+
carrier: original,
|
|
1934
|
+
source: { ...source },
|
|
1935
|
+
queue,
|
|
1936
|
+
command: { ...parsed },
|
|
1937
|
+
assertSemantic: assertCurrent,
|
|
1938
|
+
assertRecipient,
|
|
1939
|
+
reportCompleted() {
|
|
1940
|
+
throw new Error("Queue admission cannot report semantic source removal.");
|
|
1941
|
+
},
|
|
1942
|
+
});
|
|
1943
|
+
}
|
|
1944
|
+
finally {
|
|
1945
|
+
release();
|
|
1946
|
+
}
|
|
1947
|
+
});
|
|
1948
|
+
return {
|
|
1949
|
+
get source() {
|
|
1950
|
+
return { ...source };
|
|
1951
|
+
},
|
|
1952
|
+
get command() {
|
|
1953
|
+
return { ...parsed };
|
|
1954
|
+
},
|
|
1955
|
+
execute,
|
|
1956
|
+
inspectCompletion() {
|
|
1957
|
+
try {
|
|
1958
|
+
assertRecipient.call(admission);
|
|
1959
|
+
if (deps.getAllowedUserId() !== operator ||
|
|
1960
|
+
!queue.isCurrent() ||
|
|
1961
|
+
!prepared.portsCurrent())
|
|
1962
|
+
return undefined;
|
|
1963
|
+
const proof = prepared.inspectQueuedCompletion?.();
|
|
1964
|
+
assertRecipient.call(admission);
|
|
1965
|
+
return deps.getAllowedUserId() === operator &&
|
|
1966
|
+
queue.isCurrent() &&
|
|
1967
|
+
prepared.portsCurrent()
|
|
1968
|
+
? proof
|
|
1969
|
+
: undefined;
|
|
1970
|
+
}
|
|
1971
|
+
catch {
|
|
1972
|
+
return undefined;
|
|
1973
|
+
}
|
|
1974
|
+
},
|
|
1975
|
+
};
|
|
1976
|
+
},
|
|
1977
|
+
/** Selected leaders reuse registry semantics; native source/recipient admission and inline transport remain role-owned. */
|
|
1978
|
+
prepareSelectedCommand: prepareSelectedPlan,
|
|
1979
|
+
/** Existing native publication boundary; unrelated/legacy receipts keep their ordinary owner. */
|
|
1980
|
+
prepareHeldQueueReceipt: ((receipt, queueOwner, context, isWorkerCurrent) => {
|
|
1981
|
+
const record = heldQueueReceipts.get(receipt.receiptId);
|
|
1982
|
+
if (record === null)
|
|
1983
|
+
throw new Error("Held queue receipt publication authority changed.");
|
|
1984
|
+
if (!record)
|
|
1985
|
+
return undefined;
|
|
1986
|
+
if (context !== record.ctx ||
|
|
1987
|
+
!isWorkerCurrent() ||
|
|
1988
|
+
!record.current() ||
|
|
1989
|
+
!isDeepStrictEqual(record.receipt, receipt))
|
|
1990
|
+
throw new Error("Held queue receipt publication authority changed.");
|
|
1991
|
+
const scope = record.queue.prepareCompletionScope(record.receipt, queueOwner, record.completionSha256);
|
|
1992
|
+
if (!scope || !isWorkerCurrent() || !record.current())
|
|
1993
|
+
throw new Error("Held queue receipt original scope is unavailable.");
|
|
1994
|
+
return [scope];
|
|
1995
|
+
}),
|
|
1996
|
+
/** Pure registry/effect availability; never reads a source or activates future recipient authority. */
|
|
1997
|
+
canPrepareHeldCommand(name, effects) {
|
|
1998
|
+
return getHeldPlan(name, effects) !== undefined;
|
|
1999
|
+
},
|
|
2000
|
+
/** Compile one registry plan from a saved original; no admission, recipient activation or update-handler replay. */
|
|
2001
|
+
prepareHeldCommand(name, readiness, ctx, admission, effects) {
|
|
2002
|
+
const plan = getHeldPlan(name, effects);
|
|
2003
|
+
if (!plan)
|
|
2004
|
+
return undefined;
|
|
2005
|
+
const value = plan.template
|
|
2006
|
+
? readiness.snapshot.update.message
|
|
2007
|
+
: undefined;
|
|
2008
|
+
const command = value &&
|
|
2009
|
+
typeof value === "object" &&
|
|
2010
|
+
typeof Reflect.get(value, "text") === "string"
|
|
2011
|
+
? parseTelegramCommand(Reflect.get(value, "text"))
|
|
2012
|
+
: undefined;
|
|
2013
|
+
const prepared = plan.prepare(ctx, effects, command);
|
|
2014
|
+
if (!prepared)
|
|
2015
|
+
return undefined;
|
|
2016
|
+
return prepareHeldSelectedCommand(name, readiness, admission, prepared.captured, prepared.portsCurrent, plan.completion, prepared.run, prepared.inspectQueuedCompletion);
|
|
2017
|
+
},
|
|
2018
|
+
/** Warm menu-only issuance; completion reporting is not a receipt or a durable removal ACK. */
|
|
2019
|
+
prepareSelectedMenuCommand(command, messages, ctx, admission) {
|
|
2020
|
+
const action = buildTelegramCommandAction(command.name);
|
|
2021
|
+
if (messages.length !== 1 ||
|
|
2022
|
+
!messages[0] ||
|
|
2023
|
+
!["status", "model", "thinking", "queue", "settings"].includes(action.kind) ||
|
|
2024
|
+
(action.kind === "settings" && !deps.openSettingsMenu))
|
|
2025
|
+
return undefined;
|
|
2026
|
+
if (action.kind === "status")
|
|
2027
|
+
return prepareSelectedPlan(command, messages, ctx, admission);
|
|
2028
|
+
const original = messages[0], name = command.name, args = command.args;
|
|
2029
|
+
if (typeof admission.assertSourceCurrent !== "function" ||
|
|
2030
|
+
typeof admission.assertRecipientCurrent !== "function" ||
|
|
2031
|
+
typeof admission.reportCompleted !== "function")
|
|
2032
|
+
return undefined;
|
|
2033
|
+
const assertSourceCurrent = admission.assertSourceCurrent, assertRecipientCurrent = admission.assertRecipientCurrent;
|
|
2034
|
+
const reportCompleted = admission.reportCompleted, assertExecutionCurrent = deps.assertExecutionCurrent;
|
|
2035
|
+
const assertIssuanceCurrent = () => {
|
|
2036
|
+
assertSourceCurrent.call(admission);
|
|
2037
|
+
assertExecutionCurrent?.call(deps, original);
|
|
2038
|
+
assertRecipientCurrent();
|
|
2039
|
+
// Recipient observations can revoke the source too; never complete from an earlier source sample.
|
|
2040
|
+
assertSourceCurrent.call(admission);
|
|
2041
|
+
assertExecutionCurrent?.call(deps, original);
|
|
2042
|
+
};
|
|
2043
|
+
// Preparation proves branch availability, not the future released recipient's active authority.
|
|
2044
|
+
// Detached rendering must not inherit the original admission/fence or a mutable target carrier.
|
|
2045
|
+
const message = copySelectedMenuCarrier(original);
|
|
2046
|
+
const menuDeps = { ...deps };
|
|
2047
|
+
const recipientOptions = { assertAuthority: assertRecipientCurrent };
|
|
2048
|
+
const recipientDeps = {
|
|
2049
|
+
...deps,
|
|
2050
|
+
assertExecutionCurrent: assertRecipientCurrent,
|
|
2051
|
+
showStatus: (value, context) => menuDeps.showStatus(value, context, recipientOptions),
|
|
2052
|
+
openModelMenu: (value, context) => menuDeps.openModelMenu(value, context, recipientOptions),
|
|
2053
|
+
openThinkingMenu: (value, context) => menuDeps.openThinkingMenu(value, context, recipientOptions),
|
|
2054
|
+
openQueueMenu: (value, context) => menuDeps.openQueueMenu(value, context, recipientOptions),
|
|
2055
|
+
openSettingsMenu: menuDeps.openSettingsMenu &&
|
|
2056
|
+
((value, context) => menuDeps.openSettingsMenu(value, context, recipientOptions)),
|
|
2057
|
+
};
|
|
2058
|
+
return issueSelectedOnce(assertIssuanceCurrent, async () => reportCompleted.call(admission) &&
|
|
2059
|
+
handleTelegramCommandRuntime(name, message, ctx, recipientDeps, args));
|
|
2060
|
+
},
|
|
2061
|
+
});
|
|
2062
|
+
}
|
|
2063
|
+
export function createTelegramCommandOrPromptDispatcher(deps) {
|
|
2064
|
+
const dispatchPromptTemplate = async (command, messages, ctx) => {
|
|
2065
|
+
const first = messages[0];
|
|
2066
|
+
if (!first)
|
|
2067
|
+
return false;
|
|
2068
|
+
deps.assertExecutionCurrent?.(first);
|
|
2069
|
+
const expanded = command && deps.expandPromptTemplateCommand?.(command.name, command.args);
|
|
2070
|
+
if (expanded === undefined)
|
|
2071
|
+
return false;
|
|
2072
|
+
deps.assertExecutionCurrent?.(first);
|
|
2073
|
+
const replaced = deps.replaceMessageText(first, expanded);
|
|
2074
|
+
deps.assertExecutionCurrent?.(first);
|
|
2075
|
+
await deps.enqueueTurn([replaced, ...messages.slice(1)], ctx);
|
|
2076
|
+
return true;
|
|
2077
|
+
};
|
|
2078
|
+
return async (messages, ctx) => {
|
|
2079
|
+
const firstMessage = messages[0];
|
|
2080
|
+
if (!firstMessage)
|
|
2081
|
+
return;
|
|
2082
|
+
if (deps.shouldIgnoreMessages?.(messages))
|
|
2083
|
+
return;
|
|
2084
|
+
deps.assertExecutionCurrent?.(firstMessage);
|
|
2085
|
+
if (await deps.consumeThreadNameInput?.(messages, ctx)) {
|
|
2086
|
+
deps.assertExecutionCurrent?.(firstMessage);
|
|
2087
|
+
return;
|
|
2088
|
+
}
|
|
2089
|
+
const command = parseTelegramCommand(deps.extractRawText(messages));
|
|
2090
|
+
const handled = await deps.handleCommand(command?.name, firstMessage, ctx, command?.args);
|
|
2091
|
+
deps.assertExecutionCurrent?.(firstMessage);
|
|
2092
|
+
if (handled)
|
|
2093
|
+
return;
|
|
2094
|
+
if (command && deps.executeExtensionCommand) {
|
|
2095
|
+
const handledByExtension = await deps.executeExtensionCommand(command, messages[0], ctx);
|
|
2096
|
+
deps.assertExecutionCurrent?.(firstMessage);
|
|
2097
|
+
if (handledByExtension)
|
|
2098
|
+
return;
|
|
2099
|
+
}
|
|
2100
|
+
if (await dispatchPromptTemplate(command, messages, ctx))
|
|
2101
|
+
return;
|
|
2102
|
+
deps.assertExecutionCurrent?.(firstMessage);
|
|
2103
|
+
await deps.enqueueTurn(messages, ctx);
|
|
1066
2104
|
};
|
|
1067
2105
|
}
|
|
1068
2106
|
function scheduleTelegramCommandEffect(ctx, command, phase, deps, effect, assertExecutionCurrent) {
|
|
2107
|
+
const work = deps.beginCommandEffectWork?.();
|
|
2108
|
+
let started = false, outcome = "settled";
|
|
1069
2109
|
void Promise.resolve()
|
|
1070
2110
|
.then(async () => {
|
|
1071
2111
|
if (deps.isContextActive?.(ctx) === false)
|
|
1072
2112
|
return;
|
|
1073
2113
|
assertExecutionCurrent?.();
|
|
2114
|
+
started = true;
|
|
1074
2115
|
await effect();
|
|
1075
2116
|
assertExecutionCurrent?.();
|
|
1076
2117
|
})
|
|
1077
2118
|
.catch((error) => {
|
|
2119
|
+
// A failed started effect cannot prove non-issuance; never turn its uncertainty into known idle.
|
|
2120
|
+
if (started &&
|
|
2121
|
+
!(error instanceof TelegramApiAuthorityError && !error.requestIssued))
|
|
2122
|
+
outcome = "unconfirmed";
|
|
1078
2123
|
try {
|
|
1079
2124
|
deps.recordRuntimeEvent?.("telegram-command", error, {
|
|
1080
2125
|
command,
|
|
@@ -1084,9 +2129,129 @@ function scheduleTelegramCommandEffect(ctx, command, phase, deps, effect, assert
|
|
|
1084
2129
|
catch {
|
|
1085
2130
|
// Effect diagnostics cannot create an unhandled detached Promise.
|
|
1086
2131
|
}
|
|
1087
|
-
})
|
|
2132
|
+
})
|
|
2133
|
+
.finally(() => work?.settle(outcome));
|
|
2134
|
+
}
|
|
2135
|
+
async function handleTelegramHelpCommand(commandName, message, ctx, deps, admission) {
|
|
2136
|
+
const assertSemanticCurrent = admission?.assertSemanticCurrent ??
|
|
2137
|
+
(() => deps.assertExecutionCurrent?.(message));
|
|
2138
|
+
admission?.assertSemanticCurrent();
|
|
2139
|
+
if (message.from?.id !== undefined &&
|
|
2140
|
+
canPairTelegramUserFromCommandMessage(message)) {
|
|
2141
|
+
const allowed = await pairTelegramUserIfNeeded(message.from.id, {
|
|
2142
|
+
allowedUserId: deps.getAllowedUserId(),
|
|
2143
|
+
ctx: undefined,
|
|
2144
|
+
persistAllowedUserId: deps.persistAllowedUserId,
|
|
2145
|
+
updateStatus: () => deps.updateStatus(ctx),
|
|
2146
|
+
assertExecutionCurrent: assertSemanticCurrent,
|
|
2147
|
+
});
|
|
2148
|
+
if (!allowed)
|
|
2149
|
+
return false;
|
|
2150
|
+
}
|
|
2151
|
+
if (admission) {
|
|
2152
|
+
assertSemanticCurrent();
|
|
2153
|
+
if (!admission.reportCompleted())
|
|
2154
|
+
return false;
|
|
2155
|
+
admission.assertRecipientCurrent();
|
|
2156
|
+
}
|
|
2157
|
+
const assertEffectCurrent = admission?.assertRecipientCurrent ??
|
|
2158
|
+
(() => deps.assertExecutionCurrent?.(message));
|
|
2159
|
+
const options = admission && {
|
|
2160
|
+
assertAuthority: admission.assertRecipientCurrent,
|
|
2161
|
+
};
|
|
2162
|
+
const isContextActive = () => deps.isContextActive?.(ctx) !== false;
|
|
2163
|
+
scheduleTelegramCommandEffect(ctx, commandName, "menu-render", deps, async () => {
|
|
2164
|
+
let forumBootstrapMessage;
|
|
2165
|
+
if (commandName === "start" && deps.handleForumBootstrap) {
|
|
2166
|
+
forumBootstrapMessage = await deps.handleForumBootstrap(message, ctx, options);
|
|
2167
|
+
}
|
|
2168
|
+
if (!isContextActive())
|
|
2169
|
+
return;
|
|
2170
|
+
assertEffectCurrent();
|
|
2171
|
+
if (forumBootstrapMessage) {
|
|
2172
|
+
await deps.sendTextReply(message, forumBootstrapMessage, options);
|
|
2173
|
+
assertEffectCurrent();
|
|
2174
|
+
}
|
|
2175
|
+
if (!isContextActive())
|
|
2176
|
+
return;
|
|
2177
|
+
assertEffectCurrent();
|
|
2178
|
+
await deps.showStatus(message, ctx, options);
|
|
2179
|
+
}, assertEffectCurrent);
|
|
2180
|
+
scheduleTelegramCommandEffect(ctx, commandName, "bot-command-sync", deps, () => deps.registerBotCommands(options), assertEffectCurrent);
|
|
2181
|
+
return true;
|
|
2182
|
+
}
|
|
2183
|
+
async function handleTelegramNameCommand(message, ctx, requestedName, deps, admission) {
|
|
2184
|
+
const threadName = requestedName.trim();
|
|
2185
|
+
const reply = async (text) => {
|
|
2186
|
+
if (admission) {
|
|
2187
|
+
admission.assertSemanticCurrent();
|
|
2188
|
+
if (!admission.reportCompleted())
|
|
2189
|
+
throw new Error("Selected name source completion refused.");
|
|
2190
|
+
admission.assertRecipientCurrent();
|
|
2191
|
+
await deps.sendTextReply(message, text, {
|
|
2192
|
+
parseMode: "HTML",
|
|
2193
|
+
assertAuthority: admission.assertRecipientCurrent,
|
|
2194
|
+
});
|
|
2195
|
+
admission.assertRecipientCurrent();
|
|
2196
|
+
}
|
|
2197
|
+
else {
|
|
2198
|
+
deps.assertExecutionCurrent?.(message);
|
|
2199
|
+
await deps.sendTextReply(message, text, { parseMode: "HTML" });
|
|
2200
|
+
deps.assertExecutionCurrent?.(message);
|
|
2201
|
+
}
|
|
2202
|
+
};
|
|
2203
|
+
if (!threadName) {
|
|
2204
|
+
if (deps.openThreadNameDialog)
|
|
2205
|
+
await deps.openThreadNameDialog(message, ctx);
|
|
2206
|
+
else
|
|
2207
|
+
await reply(formatTelegramInformationHeading("🏷️", "Usage: /name Navigator"));
|
|
2208
|
+
return;
|
|
2209
|
+
}
|
|
2210
|
+
if (/^[A-Z]$/.test(threadName) && deps.resetCurrentThreadName) {
|
|
2211
|
+
admission?.assertSemanticCurrent();
|
|
2212
|
+
const target = getTelegramCommandMessageTarget(message);
|
|
2213
|
+
const result = admission
|
|
2214
|
+
? await deps.resetCurrentThreadName(target, {
|
|
2215
|
+
assertAuthority: admission.assertSemanticCurrent,
|
|
2216
|
+
})
|
|
2217
|
+
: await deps.resetCurrentThreadName(target);
|
|
2218
|
+
admission?.assertSemanticCurrent();
|
|
2219
|
+
await reply(result.ok && !result.message
|
|
2220
|
+
? formatTelegramAutomaticThreadDisplayNameRestoredHeading(result.threadName ?? threadName)
|
|
2221
|
+
: formatTelegramInformationHeading(result.ok ? "✅" : "⚠️", result.message ?? "Thread display name reset failed."));
|
|
2222
|
+
return;
|
|
2223
|
+
}
|
|
2224
|
+
admission?.assertSemanticCurrent();
|
|
2225
|
+
const validationError = deps.validateThreadName?.(threadName);
|
|
2226
|
+
admission?.assertSemanticCurrent();
|
|
2227
|
+
if (validationError) {
|
|
2228
|
+
await reply(formatTelegramInvalidInstanceName(validationError));
|
|
2229
|
+
return;
|
|
2230
|
+
}
|
|
2231
|
+
if (!deps.renameCurrentThread) {
|
|
2232
|
+
await reply(formatTelegramInformationHeading("🚫", "Thread display naming is unavailable."));
|
|
2233
|
+
return;
|
|
2234
|
+
}
|
|
2235
|
+
if (admission)
|
|
2236
|
+
admission.assertSemanticCurrent();
|
|
2237
|
+
else
|
|
2238
|
+
deps.assertExecutionCurrent?.(message);
|
|
2239
|
+
const target = getTelegramCommandMessageTarget(message);
|
|
2240
|
+
const result = admission
|
|
2241
|
+
? await deps.renameCurrentThread(target, threadName, {
|
|
2242
|
+
assertAuthority: admission.assertSemanticCurrent,
|
|
2243
|
+
})
|
|
2244
|
+
: await deps.renameCurrentThread(target, threadName);
|
|
2245
|
+
if (admission)
|
|
2246
|
+
admission.assertSemanticCurrent();
|
|
2247
|
+
else
|
|
2248
|
+
deps.assertExecutionCurrent?.(message);
|
|
2249
|
+
await reply(result.ok && !result.message
|
|
2250
|
+
? formatTelegramThreadDisplayNameSavedHeading(result.threadName ?? threadName)
|
|
2251
|
+
: formatTelegramInformationHeading(result.ok ? "✅" : "⚠️", result.message ?? "Thread display name update failed."));
|
|
1088
2252
|
}
|
|
1089
2253
|
async function handleTelegramCommandRuntime(commandName, message, ctx, deps, commandArgs = "") {
|
|
2254
|
+
deps.assertExecutionCurrent?.(message);
|
|
1090
2255
|
const assertExecutionCurrentFor = (nextMessage) => () => deps.assertExecutionCurrent?.(nextMessage);
|
|
1091
2256
|
const sendReplyFor = (nextMessage) => async (text, options) => {
|
|
1092
2257
|
deps.assertExecutionCurrent?.(nextMessage);
|
|
@@ -1107,40 +2272,7 @@ async function handleTelegramCommandRuntime(commandName, message, ctx, deps, com
|
|
|
1107
2272
|
sendTextReply: sendReplyFor(nextMessage),
|
|
1108
2273
|
});
|
|
1109
2274
|
},
|
|
1110
|
-
handleName:
|
|
1111
|
-
const threadName = requestedName.trim();
|
|
1112
|
-
if (!threadName) {
|
|
1113
|
-
if (deps.openThreadNameDialog) {
|
|
1114
|
-
await deps.openThreadNameDialog(nextMessage, _commandCtx);
|
|
1115
|
-
}
|
|
1116
|
-
else {
|
|
1117
|
-
await sendReplyFor(nextMessage)(formatTelegramInformationHeading("🏷️", "Usage: /name Navigator"), { parseMode: "HTML" });
|
|
1118
|
-
}
|
|
1119
|
-
return;
|
|
1120
|
-
}
|
|
1121
|
-
if (/^[A-Z]$/.test(threadName) && deps.resetCurrentThreadName) {
|
|
1122
|
-
const result = await deps.resetCurrentThreadName(getTelegramCommandMessageTarget(nextMessage));
|
|
1123
|
-
await sendReplyFor(nextMessage)(result.ok && !result.message
|
|
1124
|
-
? formatTelegramAutomaticThreadDisplayNameRestoredHeading(result.threadName ?? threadName)
|
|
1125
|
-
: formatTelegramInformationHeading(result.ok ? "✅" : "⚠️", result.message ?? "Thread display name reset failed."), { parseMode: "HTML" });
|
|
1126
|
-
return;
|
|
1127
|
-
}
|
|
1128
|
-
const validationError = deps.validateThreadName?.(threadName);
|
|
1129
|
-
if (validationError) {
|
|
1130
|
-
await sendReplyFor(nextMessage)(formatTelegramInvalidInstanceName(validationError), { parseMode: "HTML" });
|
|
1131
|
-
return;
|
|
1132
|
-
}
|
|
1133
|
-
if (!deps.renameCurrentThread) {
|
|
1134
|
-
await sendReplyFor(nextMessage)(formatTelegramInformationHeading("🚫", "Thread display naming is unavailable."), { parseMode: "HTML" });
|
|
1135
|
-
return;
|
|
1136
|
-
}
|
|
1137
|
-
deps.assertExecutionCurrent?.(nextMessage);
|
|
1138
|
-
const result = await deps.renameCurrentThread(getTelegramCommandMessageTarget(nextMessage), threadName);
|
|
1139
|
-
deps.assertExecutionCurrent?.(nextMessage);
|
|
1140
|
-
await sendReplyFor(nextMessage)(result.ok && !result.message
|
|
1141
|
-
? formatTelegramThreadDisplayNameSavedHeading(result.threadName ?? threadName)
|
|
1142
|
-
: formatTelegramInformationHeading(result.ok ? "✅" : "⚠️", result.message ?? "Thread display name update failed."), { parseMode: "HTML" });
|
|
1143
|
-
},
|
|
2275
|
+
handleName: (nextMessage, commandCtx, requestedName) => handleTelegramNameCommand(nextMessage, commandCtx, requestedName, deps),
|
|
1144
2276
|
handleAbort: async (nextMessage, commandCtx) => {
|
|
1145
2277
|
await handleTelegramAbortCommand({
|
|
1146
2278
|
hasAbortHandler: deps.hasAbortHandler,
|
|
@@ -1243,34 +2375,7 @@ async function handleTelegramCommandRuntime(commandName, message, ctx, deps, com
|
|
|
1243
2375
|
}
|
|
1244
2376
|
: undefined,
|
|
1245
2377
|
handleHelp: async (nextMessage, nextCommandName, commandCtx) => {
|
|
1246
|
-
|
|
1247
|
-
canPairTelegramUserFromCommandMessage(nextMessage)) {
|
|
1248
|
-
const allowed = await pairTelegramUserIfNeeded(nextMessage.from.id, {
|
|
1249
|
-
allowedUserId: deps.getAllowedUserId(),
|
|
1250
|
-
ctx: undefined,
|
|
1251
|
-
persistAllowedUserId: deps.persistAllowedUserId,
|
|
1252
|
-
updateStatus: updateStatusFor(commandCtx),
|
|
1253
|
-
assertExecutionCurrent: assertExecutionCurrentFor(nextMessage),
|
|
1254
|
-
});
|
|
1255
|
-
if (!allowed)
|
|
1256
|
-
return;
|
|
1257
|
-
}
|
|
1258
|
-
const isContextActive = () => deps.isContextActive?.(commandCtx) !== false;
|
|
1259
|
-
scheduleTelegramCommandEffect(commandCtx, nextCommandName, "menu-render", deps, async () => {
|
|
1260
|
-
let forumBootstrapMessage;
|
|
1261
|
-
if (nextCommandName === "start" && deps.handleForumBootstrap) {
|
|
1262
|
-
forumBootstrapMessage = await deps.handleForumBootstrap(nextMessage, commandCtx);
|
|
1263
|
-
}
|
|
1264
|
-
if (!isContextActive())
|
|
1265
|
-
return;
|
|
1266
|
-
if (forumBootstrapMessage) {
|
|
1267
|
-
await deps.sendTextReply(nextMessage, forumBootstrapMessage);
|
|
1268
|
-
}
|
|
1269
|
-
if (!isContextActive())
|
|
1270
|
-
return;
|
|
1271
|
-
await deps.showStatus(nextMessage, commandCtx);
|
|
1272
|
-
}, assertExecutionCurrentFor(nextMessage));
|
|
1273
|
-
scheduleTelegramCommandEffect(commandCtx, nextCommandName, "bot-command-sync", deps, deps.registerBotCommands, assertExecutionCurrentFor(nextMessage));
|
|
2378
|
+
await handleTelegramHelpCommand(nextCommandName, nextMessage, commandCtx, deps);
|
|
1274
2379
|
},
|
|
1275
2380
|
}, commandArgs);
|
|
1276
2381
|
}
|
|
@@ -1282,7 +2387,8 @@ function delayTelegramSessionAction(delayMs) {
|
|
|
1282
2387
|
}
|
|
1283
2388
|
export async function settleTelegramSessionReplacement(deps) {
|
|
1284
2389
|
const now = deps.now ?? Date.now;
|
|
1285
|
-
const sleep = deps.sleep ??
|
|
2390
|
+
const sleep = deps.sleep ??
|
|
2391
|
+
((delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs)));
|
|
1286
2392
|
while (deps.isCurrent?.() !== false) {
|
|
1287
2393
|
const intent = await deps.getIntent();
|
|
1288
2394
|
if (!intent || intent.sourceSessionId === deps.sessionId)
|
|
@@ -1295,7 +2401,7 @@ export async function settleTelegramSessionReplacement(deps) {
|
|
|
1295
2401
|
await sleep(100);
|
|
1296
2402
|
continue;
|
|
1297
2403
|
}
|
|
1298
|
-
if (!await deps.clearIntent(intent))
|
|
2404
|
+
if (!(await deps.clearIntent(intent)))
|
|
1299
2405
|
return "failed";
|
|
1300
2406
|
do {
|
|
1301
2407
|
const delivered = await deps.editSuccess(intent);
|
|
@@ -1319,8 +2425,7 @@ function createTelegramSessionReplacementSettlementRuntime(deps) {
|
|
|
1319
2425
|
return;
|
|
1320
2426
|
void settleTelegramSessionReplacement({
|
|
1321
2427
|
...resolved,
|
|
1322
|
-
isCurrent: () => currentGeneration === generation &&
|
|
1323
|
-
resolved.isCurrent?.() !== false,
|
|
2428
|
+
isCurrent: () => currentGeneration === generation && resolved.isCurrent?.() !== false,
|
|
1324
2429
|
}).then(deps.onResult, deps.onError);
|
|
1325
2430
|
},
|
|
1326
2431
|
};
|
|
@@ -1329,8 +2434,10 @@ export function createTelegramSessionActionAssembly(deps) {
|
|
|
1329
2434
|
const now = deps.now ?? Date.now;
|
|
1330
2435
|
const report = (error) => deps.recordRuntimeEvent?.("new-session", error);
|
|
1331
2436
|
const sendTerminalResult = async (target, result) => {
|
|
1332
|
-
const text = result === "success"
|
|
1333
|
-
|
|
2437
|
+
const text = result === "success"
|
|
2438
|
+
? "<b>🆕 New session started.</b>"
|
|
2439
|
+
: result === "cancelled"
|
|
2440
|
+
? "<b>🚫 New session cancelled.</b>"
|
|
1334
2441
|
: "<b>⚠️ New session failed.</b>";
|
|
1335
2442
|
const deadline = now() + 10_000;
|
|
1336
2443
|
do {
|
|
@@ -1346,9 +2453,12 @@ export function createTelegramSessionActionAssembly(deps) {
|
|
|
1346
2453
|
const action = createTelegramSessionActionRuntime({
|
|
1347
2454
|
registerCommand: deps.registerCommand,
|
|
1348
2455
|
sendUserMessage: deps.sendUserMessage,
|
|
1349
|
-
notifyResult(target, result) {
|
|
2456
|
+
notifyResult(target, result) {
|
|
2457
|
+
return sendTerminalResult(target, result);
|
|
2458
|
+
},
|
|
1350
2459
|
async prepareReplacement(ctx, updateId, target) {
|
|
1351
|
-
const follower = !deps.ownsPersistence() &&
|
|
2460
|
+
const follower = !deps.ownsPersistence() &&
|
|
2461
|
+
typeof target.threadId === "number" &&
|
|
1352
2462
|
deps.follower?.isRegisteredFor(target)
|
|
1353
2463
|
? deps.follower
|
|
1354
2464
|
: undefined;
|
|
@@ -1375,15 +2485,16 @@ export function createTelegramSessionActionAssembly(deps) {
|
|
|
1375
2485
|
target: binding ? { ...binding.target } : { chatId: target.chatId },
|
|
1376
2486
|
messageId: target.messageId,
|
|
1377
2487
|
...(binding?.slot ? { slot: binding.slot } : {}),
|
|
1378
|
-
...(binding?.manualThreadName ?? binding?.threadName
|
|
1379
|
-
? { threadName: binding.manualThreadName ?? binding.threadName }
|
|
2488
|
+
...((binding?.manualThreadName ?? binding?.threadName)
|
|
2489
|
+
? { threadName: binding.manualThreadName ?? binding.threadName }
|
|
2490
|
+
: {}),
|
|
1380
2491
|
createdAtMs,
|
|
1381
2492
|
expiresAtMs: createdAtMs + deps.handoffTtlMs,
|
|
1382
2493
|
...(follower ? { sourceInstanceId: follower.instanceId } : {}),
|
|
1383
2494
|
};
|
|
1384
|
-
if (!await (follower
|
|
2495
|
+
if (!(await (follower
|
|
1385
2496
|
? follower.requestSessionReplacement("publish", intent)
|
|
1386
|
-
: deps.store.commitSessionReplacementIntent(intent, deps.ownsPersistence))) {
|
|
2497
|
+
: deps.store.commitSessionReplacementIntent(intent, deps.ownsPersistence)))) {
|
|
1387
2498
|
throw new Error("Telegram session replacement intent was not persisted.");
|
|
1388
2499
|
}
|
|
1389
2500
|
},
|
|
@@ -1395,23 +2506,30 @@ export function createTelegramSessionActionAssembly(deps) {
|
|
|
1395
2506
|
if (!sessionId)
|
|
1396
2507
|
return undefined;
|
|
1397
2508
|
return {
|
|
1398
|
-
async getIntent() {
|
|
2509
|
+
async getIntent() {
|
|
2510
|
+
await deps.store.refresh?.();
|
|
2511
|
+
return deps.store.getSessionReplacementIntent();
|
|
2512
|
+
},
|
|
1399
2513
|
hasSuccessorContinuity(intent) {
|
|
1400
2514
|
if (intent.continuity === "classic-chat")
|
|
1401
2515
|
return true;
|
|
1402
|
-
if (deps.store.getWorkspaceBindingByTarget(intent.target, sessionId)
|
|
1403
|
-
intent.cwd)
|
|
2516
|
+
if (deps.store.getWorkspaceBindingByTarget(intent.target, sessionId)
|
|
2517
|
+
?.cwd !== intent.cwd)
|
|
1404
2518
|
return false;
|
|
1405
2519
|
// A follower successor claims only after its own re-registration is live.
|
|
1406
|
-
return intent.sourceInstanceId === undefined ||
|
|
1407
|
-
deps.
|
|
2520
|
+
return (intent.sourceInstanceId === undefined ||
|
|
2521
|
+
deps.ownsPersistence() ||
|
|
2522
|
+
deps.follower?.isRegisteredFor(intent.target) === true);
|
|
2523
|
+
},
|
|
2524
|
+
editSuccess(intent) {
|
|
2525
|
+
return deps.sendResult(intent.target, "<b>🆕 New session started.</b>");
|
|
1408
2526
|
},
|
|
1409
|
-
editSuccess(intent) { return deps.sendResult(intent.target, "<b>🆕 New session started.</b>"); },
|
|
1410
2527
|
async clearIntent(intent) {
|
|
1411
|
-
if (intent.sourceInstanceId === undefined ||
|
|
2528
|
+
if (intent.sourceInstanceId === undefined ||
|
|
2529
|
+
deps.ownsPersistence()) {
|
|
1412
2530
|
return deps.store.removeSessionReplacementIntent(intent, deps.ownsPersistence);
|
|
1413
2531
|
}
|
|
1414
|
-
return await deps.follower?.requestSessionReplacement("settle", intent) ?? false;
|
|
2532
|
+
return ((await deps.follower?.requestSessionReplacement("settle", intent)) ?? false);
|
|
1415
2533
|
},
|
|
1416
2534
|
profileName: deps.getProfileName() ?? "default",
|
|
1417
2535
|
cwd: ctx.cwd,
|