@llblab/pi-telegram 0.51.5 → 0.52.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +10 -8
- package/BACKLOG.md +1 -8
- package/CHANGELOG.md +23 -0
- package/LICENSE +21 -0
- package/README.md +16 -6
- package/dist/lib/activity-verbosity.d.ts +1 -5
- package/dist/lib/activity-verbosity.js +5 -7
- package/dist/lib/bindings.d.ts +9 -2
- package/dist/lib/bindings.js +22 -13
- package/dist/lib/bus-api.js +4 -2
- package/dist/lib/bus-follower.d.ts +55 -57
- package/dist/lib/bus-follower.js +213 -140
- package/dist/lib/bus-leader.d.ts +21 -2
- package/dist/lib/bus-leader.js +181 -25
- package/dist/lib/bus-transport.d.ts +4 -1
- package/dist/lib/bus-transport.js +28 -4
- package/dist/lib/bus.d.ts +52 -56
- package/dist/lib/bus.js +78 -246
- package/dist/lib/channel-posts.d.ts +1 -3
- package/dist/lib/channel-posts.js +23 -26
- package/dist/lib/command-templates.d.ts +0 -5
- package/dist/lib/command-templates.js +5 -5
- package/dist/lib/commands.d.ts +12 -26
- package/dist/lib/commands.js +152 -98
- package/dist/lib/config.d.ts +0 -17
- package/dist/lib/config.js +12 -14
- package/dist/lib/delivery.d.ts +0 -4
- package/dist/lib/delivery.js +2 -5
- package/dist/lib/extension.js +281 -61
- package/dist/lib/generative-apps.d.ts +0 -19
- package/dist/lib/inbound.d.ts +0 -2
- package/dist/lib/inbound.js +2 -2
- package/dist/lib/journal.d.ts +177 -16
- package/dist/lib/journal.js +1115 -249
- package/dist/lib/keyboard.d.ts +0 -2
- package/dist/lib/keyboard.js +2 -2
- package/dist/lib/lifecycle.d.ts +57 -2
- package/dist/lib/lifecycle.js +110 -5
- package/dist/lib/locks.d.ts +100 -12
- package/dist/lib/locks.js +380 -14
- package/dist/lib/logging.d.ts +30 -12
- package/dist/lib/logging.js +129 -72
- package/dist/lib/media.d.ts +28 -5
- package/dist/lib/media.js +26 -8
- package/dist/lib/menu-model.d.ts +0 -11
- package/dist/lib/menu-model.js +8 -8
- package/dist/lib/menu-settings.d.ts +0 -18
- package/dist/lib/menu-settings.js +18 -18
- package/dist/lib/menu-status.d.ts +4 -4
- package/dist/lib/menu-status.js +12 -7
- package/dist/lib/menu-thinking.d.ts +1 -1
- package/dist/lib/menu-thinking.js +1 -1
- package/dist/lib/menu.d.ts +1 -0
- package/dist/lib/menu.js +4 -3
- package/dist/lib/model.js +2 -4
- package/dist/lib/outbound-attachments.d.ts +0 -1
- package/dist/lib/outbound-attachments.js +1 -1
- package/dist/lib/outbound-buttons.js +1 -8
- package/dist/lib/outbound-markup.d.ts +1 -6
- package/dist/lib/outbound-markup.js +4 -21
- package/dist/lib/outbound.d.ts +0 -8
- package/dist/lib/outbound.js +5 -5
- package/dist/lib/paths.d.ts +50 -7
- package/dist/lib/paths.js +155 -17
- package/dist/lib/pi.d.ts +1 -0
- package/dist/lib/pi.js +2 -1
- package/dist/lib/polling.d.ts +0 -5
- package/dist/lib/polling.js +5 -5
- package/dist/lib/preview.d.ts +0 -3
- package/dist/lib/preview.js +3 -3
- package/dist/lib/prompt-templates.d.ts +0 -1
- package/dist/lib/prompt-templates.js +1 -1
- package/dist/lib/queue.d.ts +3 -15
- package/dist/lib/queue.js +69 -8
- package/dist/lib/recovery.d.ts +29 -9
- package/dist/lib/recovery.js +104 -33
- package/dist/lib/replies.d.ts +0 -4
- package/dist/lib/replies.js +1 -1
- package/dist/lib/routing.d.ts +73 -8
- package/dist/lib/routing.js +2206 -307
- package/dist/lib/sections.d.ts +0 -2
- package/dist/lib/sections.js +1 -1
- package/dist/lib/status.d.ts +46 -9
- package/dist/lib/status.js +179 -27
- package/dist/lib/sync.d.ts +0 -5
- package/dist/lib/sync.js +9 -8
- package/dist/lib/telegram-api.d.ts +11 -3
- package/dist/lib/telegram-api.js +34 -23
- package/dist/lib/thread-cleanup-manager.d.ts +1 -0
- package/dist/lib/thread-cleanup-manager.js +18 -21
- package/dist/lib/thread-naming.d.ts +32 -5
- package/dist/lib/thread-naming.js +191 -7
- package/dist/lib/thread-reconciler.d.ts +2 -0
- package/dist/lib/thread-reconciler.js +1 -1
- package/dist/lib/threads.d.ts +292 -59
- package/dist/lib/threads.js +1664 -373
- package/dist/lib/turns.d.ts +0 -1
- package/dist/lib/turns.js +1 -1
- package/dist/lib/updates.d.ts +184 -28
- package/dist/lib/updates.js +1032 -114
- package/dist/lib/wire.d.ts +12 -0
- package/dist/lib/wire.js +21 -0
- package/dist/lib/workspace-admission.d.ts +25 -17
- package/dist/lib/workspace-admission.js +95 -20
- package/dist/lib/workspace-identity.d.ts +20 -0
- package/dist/lib/workspace-identity.js +103 -0
- package/dist/lib/workspace-retirement.d.ts +45 -10
- package/dist/lib/workspace-retirement.js +196 -64
- package/dist/package.json +6 -6
- package/dist/skills/generated-control-surface/SKILL.md +3 -3
- package/dist/skills/telegram-bridge/references/diagnosis.md +3 -3
- package/docs/README.md +1 -1
- package/docs/activity.md +1 -1
- package/docs/architecture.md +158 -39
- package/docs/callback-namespaces.md +1 -1
- package/docs/delivery.md +1 -1
- package/docs/generative-apps.md +2 -2
- package/docs/inbound.md +1 -1
- package/docs/multi-instance-bus.md +127 -19
- package/docs/public-api.md +5 -3
- package/docs/ui-style.md +26 -8
- package/docs/updates.md +1 -1
- package/lib/activity-verbosity.ts +5 -9
- package/lib/bindings.ts +23 -10
- package/lib/bus-api.ts +5 -2
- package/lib/bus-follower.ts +251 -226
- package/lib/bus-leader.ts +178 -31
- package/lib/bus-transport.ts +30 -8
- package/lib/bus.ts +110 -334
- package/lib/channel-posts.ts +29 -29
- package/lib/command-templates.ts +5 -5
- package/lib/commands.ts +150 -112
- package/lib/config.ts +12 -17
- package/lib/delivery.ts +2 -8
- package/lib/extension.ts +282 -72
- package/lib/generative-apps.ts +0 -22
- package/lib/inbound.ts +2 -2
- package/lib/journal.ts +1182 -326
- package/lib/keyboard.ts +2 -2
- package/lib/lifecycle.ts +141 -5
- package/lib/locks.ts +402 -23
- package/lib/logging.ts +151 -101
- package/lib/media.ts +51 -9
- package/lib/menu-model.ts +8 -8
- package/lib/menu-settings.ts +18 -18
- package/lib/menu-status.ts +11 -0
- package/lib/menu-thinking.ts +2 -2
- package/lib/menu.ts +5 -1
- package/lib/model.ts +2 -4
- package/lib/outbound-attachments.ts +1 -1
- package/lib/outbound-buttons.ts +1 -10
- package/lib/outbound-markup.ts +5 -24
- package/lib/outbound.ts +5 -5
- package/lib/paths.ts +177 -17
- package/lib/pi.ts +5 -2
- package/lib/polling.ts +5 -5
- package/lib/preview.ts +3 -3
- package/lib/prompt-templates.ts +1 -1
- package/lib/queue.ts +67 -9
- package/lib/recovery.ts +104 -44
- package/lib/replies.ts +1 -1
- package/lib/routing.ts +1955 -375
- package/lib/sections.ts +1 -1
- package/lib/status.ts +185 -36
- package/lib/sync.ts +8 -8
- package/lib/telegram-api.ts +48 -33
- package/lib/thread-cleanup-manager.ts +24 -21
- package/lib/thread-naming.ts +267 -9
- package/lib/thread-reconciler.ts +3 -1
- package/lib/threads.ts +1718 -491
- package/lib/turns.ts +1 -1
- package/lib/updates.ts +1072 -151
- package/lib/wire.ts +28 -0
- package/lib/workspace-admission.ts +97 -47
- package/lib/workspace-identity.ts +147 -0
- package/lib/workspace-retirement.ts +234 -102
- package/package.json +6 -6
- package/scripts/audit-exports.mjs +100 -0
- package/scripts/check-downgrade.mjs +80 -43
- package/skills/generated-control-surface/SKILL.md +3 -3
- package/skills/telegram-bridge/references/diagnosis.md +3 -3
package/lib/channel-posts.ts
CHANGED
|
@@ -8,12 +8,18 @@ import { chmodSync, closeSync, constants, createReadStream, fstatSync, lstatSync
|
|
|
8
8
|
readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
9
9
|
import { lstat } from "node:fs/promises";
|
|
10
10
|
import { basename, dirname } from "node:path";
|
|
11
|
+
import { getTelegramJournalPublicationPaths } from "./paths.ts";
|
|
11
12
|
import { createHash, randomUUID } from "node:crypto";
|
|
12
13
|
|
|
13
14
|
import { Type } from "@sinclair/typebox";
|
|
14
15
|
|
|
15
16
|
import { renameTelegramPathWithRetry, withTelegramFileTransaction } from "./locks.ts";
|
|
16
17
|
import type { ExtensionAPI } from "./pi.ts";
|
|
18
|
+
import {
|
|
19
|
+
isWireRecord as isRecord,
|
|
20
|
+
hasOnlyWireKeys as hasOnlyKeys,
|
|
21
|
+
isNonNegativeWireInteger as isSafeTime,
|
|
22
|
+
} from "./wire.ts";
|
|
17
23
|
|
|
18
24
|
const CHANNEL_POST_JOURNAL_VERSION = 1;
|
|
19
25
|
const DEFAULT_MAX_RECORDS = 256;
|
|
@@ -31,12 +37,12 @@ export interface TelegramChannelPostMediaIntent {
|
|
|
31
37
|
sha256: string;
|
|
32
38
|
}
|
|
33
39
|
|
|
34
|
-
|
|
40
|
+
const TELEGRAM_CHANNEL_POST_MEDIA_MAX_BYTES: Record<TelegramChannelPostMediaKind, number> = {
|
|
35
41
|
photo: 10 * 1024 * 1024,
|
|
36
42
|
video: 50 * 1024 * 1024,
|
|
37
43
|
};
|
|
38
|
-
|
|
39
|
-
|
|
44
|
+
const TELEGRAM_CHANNEL_POST_CAPTION_MAX_LENGTH = 1024;
|
|
45
|
+
const TELEGRAM_CHANNEL_POST_MEDIA_FILE_NAME_MAX_LENGTH = 255;
|
|
40
46
|
|
|
41
47
|
/** Safe, content-free local validation failure for channel media publication intent. */
|
|
42
48
|
export class TelegramChannelPostValidationError extends Error {
|
|
@@ -195,6 +201,7 @@ interface TelegramChannelPostJournalFile {
|
|
|
195
201
|
|
|
196
202
|
export interface TelegramChannelPostJournalStoreOptions {
|
|
197
203
|
path: string;
|
|
204
|
+
runtimeDir?: string;
|
|
198
205
|
profileName: string;
|
|
199
206
|
tokenSha256: string;
|
|
200
207
|
maxRecords?: number;
|
|
@@ -329,19 +336,6 @@ export function registerTelegramChannelPostListTool(
|
|
|
329
336
|
});
|
|
330
337
|
}
|
|
331
338
|
|
|
332
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
333
|
-
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
function hasOnlyKeys(value: Record<string, unknown>, keys: readonly string[]): boolean {
|
|
337
|
-
const allowed = new Set(keys);
|
|
338
|
-
return Object.keys(value).every(key => allowed.has(key));
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
function isSafeTime(value: unknown): value is number {
|
|
342
|
-
return Number.isSafeInteger(value) && (value as number) >= 0;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
339
|
function normalizeChannel(value: unknown): TelegramChannelPostAddress {
|
|
346
340
|
if (Number.isSafeInteger(value) && (value as number) < 0) return value as number;
|
|
347
341
|
if (typeof value === "string" && /^@[A-Za-z0-9_]{5,32}$/u.test(value)) {
|
|
@@ -454,9 +448,24 @@ function validateRecord(value: unknown): TelegramChannelPostRecord {
|
|
|
454
448
|
throw new TelegramChannelPostJournalError("invalid", "Telegram channel post journal contains an invalid state.");
|
|
455
449
|
}
|
|
456
450
|
|
|
451
|
+
function parseTelegramChannelPostJournalFile(value: unknown, profileName: string, tokenSha256: string): TelegramChannelPostJournalFile {
|
|
452
|
+
if (!isRecord(value) || !hasOnlyKeys(value, ["version", "profile", "tokenSha256", "records"]) ||
|
|
453
|
+
value.version !== CHANNEL_POST_JOURNAL_VERSION || value.profile !== profileName ||
|
|
454
|
+
value.tokenSha256 !== tokenSha256 || !Array.isArray(value.records)) {
|
|
455
|
+
throw new TelegramChannelPostJournalError("conflict", "Telegram channel post journal identity or schema does not match.");
|
|
456
|
+
}
|
|
457
|
+
const records = value.records.map(validateRecord);
|
|
458
|
+
if (new Set(records.map(record => record.operationId)).size !== records.length) {
|
|
459
|
+
throw new TelegramChannelPostJournalError("invalid", "Telegram channel post journal contains duplicate operation IDs.");
|
|
460
|
+
}
|
|
461
|
+
return { version: CHANNEL_POST_JOURNAL_VERSION, profile: profileName, tokenSha256, records };
|
|
462
|
+
}
|
|
463
|
+
|
|
457
464
|
export function createTelegramChannelPostJournalStore(
|
|
458
465
|
options: TelegramChannelPostJournalStoreOptions,
|
|
459
466
|
): TelegramChannelPostJournalStore {
|
|
467
|
+
options = { ...options };
|
|
468
|
+
const publicationPaths = getTelegramJournalPublicationPaths(options.path, options.runtimeDir);
|
|
460
469
|
const maxRecords = options.maxRecords ?? DEFAULT_MAX_RECORDS;
|
|
461
470
|
const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
462
471
|
const now = options.getNowMs ?? Date.now;
|
|
@@ -491,17 +500,7 @@ export function createTelegramChannelPostJournalStore(
|
|
|
491
500
|
throw new TelegramChannelPostJournalError("conflict", "Telegram channel post journal changed during inspection.");
|
|
492
501
|
}
|
|
493
502
|
const value = JSON.parse(readFileSync(fd, "utf8")) as unknown;
|
|
494
|
-
|
|
495
|
-
value.version !== CHANNEL_POST_JOURNAL_VERSION || value.profile !== options.profileName ||
|
|
496
|
-
value.tokenSha256 !== options.tokenSha256 || !Array.isArray(value.records)) {
|
|
497
|
-
throw new TelegramChannelPostJournalError("conflict", "Telegram channel post journal identity or schema does not match.");
|
|
498
|
-
}
|
|
499
|
-
const records = value.records.map(validateRecord);
|
|
500
|
-
if (new Set(records.map(record => record.operationId)).size !== records.length) {
|
|
501
|
-
throw new TelegramChannelPostJournalError("invalid", "Telegram channel post journal contains duplicate operation IDs.");
|
|
502
|
-
}
|
|
503
|
-
return { version: CHANNEL_POST_JOURNAL_VERSION, profile: options.profileName,
|
|
504
|
-
tokenSha256: options.tokenSha256, records };
|
|
503
|
+
return parseTelegramChannelPostJournalFile(value, options.profileName, options.tokenSha256);
|
|
505
504
|
} catch (error) {
|
|
506
505
|
if (error instanceof TelegramChannelPostJournalError) throw error;
|
|
507
506
|
throw new TelegramChannelPostJournalError("io", "Could not read Telegram channel post journal.", error);
|
|
@@ -517,8 +516,9 @@ export function createTelegramChannelPostJournalStore(
|
|
|
517
516
|
if (Buffer.byteLength(serialized) > maxBytes) {
|
|
518
517
|
throw new TelegramChannelPostJournalError("capacity", "Telegram channel post journal byte limit reached.");
|
|
519
518
|
}
|
|
520
|
-
const temporaryPath = `${
|
|
519
|
+
const temporaryPath = `${publicationPaths.temporaryBasePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
521
520
|
mkdirSync(dirname(options.path), { recursive: true, mode: 0o700 });
|
|
521
|
+
mkdirSync(dirname(temporaryPath), { recursive: true, mode: 0o700 });
|
|
522
522
|
try {
|
|
523
523
|
writeFileSync(temporaryPath, serialized, { encoding: "utf8", mode: 0o600 });
|
|
524
524
|
chmodSync(temporaryPath, 0o600);
|
|
@@ -532,7 +532,7 @@ export function createTelegramChannelPostJournalStore(
|
|
|
532
532
|
};
|
|
533
533
|
const mutate = <T>(operation: (file: TelegramChannelPostJournalFile) => T): T => {
|
|
534
534
|
try {
|
|
535
|
-
return withTelegramFileTransaction(
|
|
535
|
+
return withTelegramFileTransaction(publicationPaths.transactionPath, () => operation(read()));
|
|
536
536
|
} catch (error) {
|
|
537
537
|
if (error instanceof TelegramChannelPostJournalError) throw error;
|
|
538
538
|
throw new TelegramChannelPostJournalError("io", "Telegram channel post journal mutation failed.", error);
|
package/lib/command-templates.ts
CHANGED
|
@@ -121,7 +121,7 @@ function normalizeCommandTemplateDefaults(
|
|
|
121
121
|
return normalized;
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
|
|
124
|
+
function resolveInheritedDefaultReferences(
|
|
125
125
|
ownDefaults: Record<string, unknown> | undefined,
|
|
126
126
|
inheritedDefaults: Record<string, unknown> | undefined,
|
|
127
127
|
runtimeValues: Record<string, unknown> = {},
|
|
@@ -142,7 +142,7 @@ export function resolveInheritedDefaultReferences(
|
|
|
142
142
|
return resolved;
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
|
|
145
|
+
function resolveCommandTemplateRepeat(
|
|
146
146
|
value: number | string | undefined,
|
|
147
147
|
values: Record<string, unknown> = {},
|
|
148
148
|
): number | undefined {
|
|
@@ -367,7 +367,7 @@ function pad(value: number, width: number): string {
|
|
|
367
367
|
return String(value).padStart(width, "0");
|
|
368
368
|
}
|
|
369
369
|
|
|
370
|
-
|
|
370
|
+
function getCommandTemplateRepeatDefaults(
|
|
371
371
|
index: number,
|
|
372
372
|
repeat: number,
|
|
373
373
|
): Record<string, string> {
|
|
@@ -516,7 +516,7 @@ function parseCommandTemplatePlaceholderContent(
|
|
|
516
516
|
};
|
|
517
517
|
}
|
|
518
518
|
|
|
519
|
-
|
|
519
|
+
function getCommandTemplateDefaults(
|
|
520
520
|
config: CommandTemplateConfig | undefined,
|
|
521
521
|
): Record<string, string> {
|
|
522
522
|
const normalizedConfig = config
|
|
@@ -588,7 +588,7 @@ export function splitCommandTemplate(input: string): string[] {
|
|
|
588
588
|
return words;
|
|
589
589
|
}
|
|
590
590
|
|
|
591
|
-
|
|
591
|
+
function expandCommandTemplateExecutable(
|
|
592
592
|
command: string,
|
|
593
593
|
cwd: string,
|
|
594
594
|
): string {
|
package/lib/commands.ts
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
import type * as Pi from "./pi.ts";
|
|
15
15
|
import type { ExtensionAPI, ExtensionCommandContext } from "./pi.ts";
|
|
16
16
|
import { escapeHtml } from "./rendering.ts";
|
|
17
|
-
import type
|
|
17
|
+
import { formatTelegramConnectionFailure, type TelegramBridgeStatusLineOptions } from "./status.ts";
|
|
18
18
|
import type { TelegramSessionReplacementIntent } from "./threads.ts";
|
|
19
19
|
import {
|
|
20
20
|
createTelegramControlItemBuilder,
|
|
@@ -91,11 +91,11 @@ function getOrCreateTelegramCommandRegistry(): TelegramExtensionCommandRegistry
|
|
|
91
91
|
return registry;
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
|
|
94
|
+
function normalizeTelegramExtensionCommandName(name: string): string {
|
|
95
95
|
return name.trim().replace(/^\/+/, "").toLowerCase();
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
|
|
98
|
+
function isTelegramExtensionCommandName(name: string): boolean {
|
|
99
99
|
return TELEGRAM_BOT_COMMAND_NAME_PATTERN.test(name);
|
|
100
100
|
}
|
|
101
101
|
|
|
@@ -144,7 +144,7 @@ export function registerTelegramCommand(
|
|
|
144
144
|
};
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
-
|
|
147
|
+
function getTelegramExtensionCommands(): RegisteredTelegramExtensionCommand[] {
|
|
148
148
|
return Array.from(
|
|
149
149
|
getOrCreateTelegramCommandRegistry().commands.values(),
|
|
150
150
|
).sort((a, b) => a.order - b.order || a.name.localeCompare(b.name));
|
|
@@ -181,7 +181,7 @@ export const TELEGRAM_COMMAND_EMOJI = {
|
|
|
181
181
|
|
|
182
182
|
export type TelegramCommandEmojiName = keyof typeof TELEGRAM_COMMAND_EMOJI;
|
|
183
183
|
|
|
184
|
-
|
|
184
|
+
function getTelegramCommandEmoji(
|
|
185
185
|
command: TelegramCommandEmojiName,
|
|
186
186
|
): string {
|
|
187
187
|
return TELEGRAM_COMMAND_EMOJI[command];
|
|
@@ -235,12 +235,12 @@ export function formatTelegramAutomaticThreadDisplayNameRestoredHeading(
|
|
|
235
235
|
return `<b>✅ Automatic Thread display name restored as <i>${escapeHtml(name)}</i>.</b>`;
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
-
|
|
238
|
+
const TELEGRAM_COMPACTION_STARTED_TEXT =
|
|
239
239
|
formatTelegramInformationHeading(
|
|
240
240
|
getTelegramCommandEmoji("compact"),
|
|
241
241
|
"Compaction started.",
|
|
242
242
|
);
|
|
243
|
-
|
|
243
|
+
const TELEGRAM_COMPACTION_COMPLETED_TEXT =
|
|
244
244
|
formatTelegramInformationHeading("✅", "Compaction completed.");
|
|
245
245
|
export const TELEGRAM_COMPACTION_STARTED_MARKDOWN =
|
|
246
246
|
`**${formatTelegramCommandEmojiPrefix("compact")}Compaction started.**`;
|
|
@@ -254,7 +254,7 @@ function formatTelegramBotCommandDescription(
|
|
|
254
254
|
return `${formatTelegramCommandEmojiPrefix(command)}${description}`;
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
-
|
|
257
|
+
const TELEGRAM_BUILTIN_BOT_COMMANDS: readonly TelegramBotCommandDefinition[] =
|
|
258
258
|
[
|
|
259
259
|
{
|
|
260
260
|
command: "start",
|
|
@@ -399,14 +399,24 @@ export interface TelegramBridgeCommandRegistrationDeps {
|
|
|
399
399
|
recoverPollingStart?: (
|
|
400
400
|
error: unknown,
|
|
401
401
|
) => Promise<TelegramPollingStartRecoveryResult>;
|
|
402
|
+
recordConnectionEvent?: (error: unknown, phase: string) => void;
|
|
402
403
|
getDisconnectThreadName?: () => string | undefined;
|
|
403
404
|
queueAgentConnectionContext?: (connected: boolean) => void;
|
|
404
405
|
updateStatus: (ctx: ExtensionCommandContext) => void;
|
|
406
|
+
isContextCurrent?: (ctx: ExtensionCommandContext) => boolean;
|
|
407
|
+
getSessionGeneration?: () => number;
|
|
408
|
+
connectionIntent?: {
|
|
409
|
+
begin(cwd: string, profileName?: string): string;
|
|
410
|
+
finish(id: string): void;
|
|
411
|
+
isActive(id: string): boolean;
|
|
412
|
+
cancel(): void;
|
|
413
|
+
};
|
|
405
414
|
getProfileNames?: () => string[];
|
|
406
|
-
activateDefaultProfileConfig?: (ctx: ExtensionCommandContext) => Promise<void>;
|
|
415
|
+
activateDefaultProfileConfig?: (ctx: ExtensionCommandContext, isCurrent: () => boolean) => Promise<void>;
|
|
407
416
|
activateProfileConfig?: (
|
|
408
417
|
ctx: ExtensionCommandContext,
|
|
409
418
|
profileName: string,
|
|
419
|
+
isCurrent: () => boolean,
|
|
410
420
|
) => Promise<boolean>;
|
|
411
421
|
}
|
|
412
422
|
|
|
@@ -504,6 +514,14 @@ export function registerTelegramBridgeCommands(
|
|
|
504
514
|
pi.registerCommand("telegram-connect", {
|
|
505
515
|
description: "<profile> — Start Telegram bridge",
|
|
506
516
|
handler: async (args, ctx) => {
|
|
517
|
+
const sessionGeneration = deps.getSessionGeneration?.();
|
|
518
|
+
let intentId: string | undefined;
|
|
519
|
+
// Pi context getters throw after replacement; check plain intent/generation first.
|
|
520
|
+
const isCurrent = () =>
|
|
521
|
+
(!intentId || deps.connectionIntent?.isActive(intentId) !== false) &&
|
|
522
|
+
(sessionGeneration === undefined || deps.getSessionGeneration?.() === sessionGeneration) &&
|
|
523
|
+
deps.isContextCurrent?.(ctx) !== false;
|
|
524
|
+
if (!isCurrent()) return;
|
|
507
525
|
if (args.trim().split(/\s+/).some((word) => /^as=/i.test(word))) {
|
|
508
526
|
ctx.ui.notify(
|
|
509
527
|
"Thread names are configured from Telegram, not from Pi commands.",
|
|
@@ -513,121 +531,141 @@ export function registerTelegramBridgeCommands(
|
|
|
513
531
|
return;
|
|
514
532
|
}
|
|
515
533
|
const profileName = parseTelegramProfileArg(args);
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
if (
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
534
|
+
intentId = deps.connectionIntent?.begin(ctx.cwd, profileName);
|
|
535
|
+
try {
|
|
536
|
+
if (profileName && deps.activateProfileConfig) {
|
|
537
|
+
const ok = await deps.activateProfileConfig(ctx, profileName, isCurrent);
|
|
538
|
+
if (!isCurrent()) return;
|
|
539
|
+
if (!ok) {
|
|
540
|
+
ctx.ui.notify(`Profile "${profileName}" not found.`, "error");
|
|
541
|
+
deps.updateStatus(ctx);
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
ctx.ui.notify(`Activated profile "${profileName}".`, "info");
|
|
545
|
+
} else {
|
|
546
|
+
await (deps.activateDefaultProfileConfig?.(ctx, isCurrent) ?? deps.reloadConfig());
|
|
547
|
+
if (!isCurrent()) return;
|
|
522
548
|
}
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
);
|
|
536
|
-
deps.updateStatus(ctx);
|
|
549
|
+
if (!deps.hasBotToken()) {
|
|
550
|
+
const botTokenDiagnostic = deps.getBotTokenDiagnostic?.();
|
|
551
|
+
if (botTokenDiagnostic) ctx.ui.notify(botTokenDiagnostic, "error");
|
|
552
|
+
const profileNames = deps.getProfileNames?.() ?? [];
|
|
553
|
+
if (!profileName && profileNames.length > 0) {
|
|
554
|
+
ctx.ui.notify(
|
|
555
|
+
`No default Telegram profile configured. Available profiles: ${profileNames.join(", ")}. Use /telegram-connect <profileName> or /telegram-setup to create a default profile.`,
|
|
556
|
+
"info",
|
|
557
|
+
);
|
|
558
|
+
deps.updateStatus(ctx);
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
await deps.promptForConfig(ctx, profileName);
|
|
537
562
|
return;
|
|
538
563
|
}
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
const startWithRecovery = async (
|
|
544
|
-
options: TelegramBridgeCommandStartPollingOptions,
|
|
545
|
-
): Promise<void | TelegramBridgeCommandStartPollingResult> => {
|
|
546
|
-
try {
|
|
547
|
-
return await deps.startPolling(ctx, options);
|
|
548
|
-
} catch (error) {
|
|
549
|
-
if (!deps.recoverPollingStart || recoveryUsed) throw error;
|
|
550
|
-
const recovery = await deps.recoverPollingStart(error);
|
|
551
|
-
if (recovery.kind === "unhandled") throw error;
|
|
552
|
-
if (recovery.kind === "blocked") {
|
|
553
|
-
return { ok: false, message: recovery.message };
|
|
554
|
-
}
|
|
555
|
-
recoveryUsed = true;
|
|
564
|
+
let recoveryUsed = false;
|
|
565
|
+
const startWithRecovery = async (
|
|
566
|
+
options: TelegramBridgeCommandStartPollingOptions,
|
|
567
|
+
): Promise<void | (TelegramBridgeCommandStartPollingResult & { notice?: string })> => {
|
|
556
568
|
try {
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
569
|
+
return await deps.startPolling(ctx, options);
|
|
570
|
+
} catch (error) {
|
|
571
|
+
if (!isCurrent()) return;
|
|
572
|
+
if (!deps.recoverPollingStart || recoveryUsed) throw error;
|
|
573
|
+
deps.recordConnectionEvent?.(error, "polling-start");
|
|
574
|
+
const recovery = await deps.recoverPollingStart(error);
|
|
575
|
+
if (!isCurrent()) return;
|
|
576
|
+
if (recovery.kind === "unhandled") throw error;
|
|
577
|
+
if (recovery.kind === "blocked") {
|
|
578
|
+
return { ok: false, message: recovery.message,
|
|
579
|
+
notice: "Telegram recovery blocked. Check /telegram-status --debug." };
|
|
580
|
+
}
|
|
581
|
+
recoveryUsed = true;
|
|
582
|
+
deps.recordConnectionEvent?.(recovery.message, "recovery");
|
|
583
|
+
try {
|
|
584
|
+
const retry = await deps.startPolling(ctx, options);
|
|
585
|
+
if (!isCurrent()) return;
|
|
586
|
+
if (!retry) return { ok: true, message: "Telegram bridge connected; temporary state recovered." };
|
|
587
|
+
return {
|
|
588
|
+
...retry,
|
|
589
|
+
message: retry.ok
|
|
590
|
+
? "Telegram bridge connected; temporary state recovered."
|
|
591
|
+
: retry.message,
|
|
592
|
+
};
|
|
593
|
+
} catch (error) {
|
|
594
|
+
if (!isCurrent()) return;
|
|
595
|
+
deps.recordConnectionEvent?.(error, "recovery-retry");
|
|
596
|
+
return {
|
|
597
|
+
ok: false,
|
|
598
|
+
notice: "Telegram recovery failed. Restart this Pi instance.",
|
|
599
|
+
};
|
|
560
600
|
}
|
|
561
|
-
return {
|
|
562
|
-
...retry,
|
|
563
|
-
message: retry.ok
|
|
564
|
-
? `${recovery.message} ${retry.message ?? "Telegram bridge connected."}`
|
|
565
|
-
: retry.message,
|
|
566
|
-
};
|
|
567
|
-
} catch {
|
|
568
|
-
return {
|
|
569
|
-
ok: false,
|
|
570
|
-
message:
|
|
571
|
-
"Telegram temporary state was recovered, but the bridge could not restart. Restart this Pi instance and run /telegram-connect again.",
|
|
572
|
-
};
|
|
573
601
|
}
|
|
602
|
+
};
|
|
603
|
+
let result = await startWithRecovery({ forceFreshLeaderThread: true });
|
|
604
|
+
if (!isCurrent()) return;
|
|
605
|
+
if (result && !result.ok && result.canTakeover) {
|
|
606
|
+
const confirmed = await ctx.ui.confirm(
|
|
607
|
+
formatTelegramTakeoverTitle(ctx),
|
|
608
|
+
formatTelegramTakeoverPrompt(ctx, result.owner),
|
|
609
|
+
);
|
|
610
|
+
if (!isCurrent()) return;
|
|
611
|
+
if (!confirmed) {
|
|
612
|
+
ctx.ui.notify("Telegram bridge takeover cancelled.", "info");
|
|
613
|
+
deps.updateStatus(ctx);
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
result = await startWithRecovery({ force: true, forceFreshLeaderThread: true });
|
|
617
|
+
if (!isCurrent()) return;
|
|
574
618
|
}
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
const confirmed = await ctx.ui.confirm(
|
|
581
|
-
formatTelegramTakeoverTitle(ctx),
|
|
582
|
-
formatTelegramTakeoverPrompt(ctx, result.owner),
|
|
583
|
-
);
|
|
584
|
-
if (!confirmed) {
|
|
585
|
-
ctx.ui.notify("Telegram bridge takeover cancelled.", "info");
|
|
586
|
-
deps.updateStatus(ctx);
|
|
587
|
-
return;
|
|
619
|
+
if (result && !result.ok) {
|
|
620
|
+
if (result.message) deps.recordConnectionEvent?.(result.message, "connect-refused");
|
|
621
|
+
ctx.ui.notify(result.notice ?? formatTelegramConnectionFailure(result.message), "warning");
|
|
622
|
+
} else if (result?.message) {
|
|
623
|
+
ctx.ui.notify(result.message, "info");
|
|
588
624
|
}
|
|
589
|
-
result
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
deps.queueAgentConnectionContext?.(true);
|
|
625
|
+
if (!result || result.ok) deps.queueAgentConnectionContext?.(true);
|
|
626
|
+
deps.updateStatus(ctx);
|
|
627
|
+
} catch (error) {
|
|
628
|
+
if (!isCurrent()) return;
|
|
629
|
+
deps.recordConnectionEvent?.(error, "connect");
|
|
630
|
+
ctx.ui.notify(formatTelegramConnectionFailure(error), "warning");
|
|
631
|
+
deps.updateStatus(ctx);
|
|
632
|
+
} finally {
|
|
633
|
+
if (intentId) deps.connectionIntent?.finish(intentId);
|
|
599
634
|
}
|
|
600
|
-
deps.updateStatus(ctx);
|
|
601
635
|
},
|
|
602
636
|
});
|
|
603
637
|
pi.registerCommand("telegram-disconnect", {
|
|
604
638
|
description: "Stop Telegram and delete current thread in Threaded Mode",
|
|
605
639
|
handler: async (_args, ctx) => {
|
|
606
|
-
const
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
if (!confirmed) {
|
|
613
|
-
ctx.ui.notify("Telegram disconnect cancelled.", "info");
|
|
614
|
-
deps.updateStatus(ctx);
|
|
615
|
-
return;
|
|
616
|
-
}
|
|
617
|
-
}
|
|
640
|
+
const generation = deps.getSessionGeneration?.();
|
|
641
|
+
const isCurrent = () =>
|
|
642
|
+
(generation === undefined || deps.getSessionGeneration?.() === generation) &&
|
|
643
|
+
deps.isContextCurrent?.(ctx) !== false;
|
|
644
|
+
if (!isCurrent()) return;
|
|
645
|
+
deps.connectionIntent?.cancel();
|
|
618
646
|
try {
|
|
647
|
+
const threadName = deps.getDisconnectThreadName?.();
|
|
648
|
+
if (threadName) {
|
|
649
|
+
const confirmed = await ctx.ui.confirm(
|
|
650
|
+
ctx.ui.theme.fg("accent", "pi-telegram"),
|
|
651
|
+
`Delete Telegram thread ${ctx.ui.theme.fg("warning", threadName)} and disconnect this Pi session?`,
|
|
652
|
+
);
|
|
653
|
+
if (!isCurrent()) return;
|
|
654
|
+
if (!confirmed) {
|
|
655
|
+
ctx.ui.notify("Telegram disconnect cancelled.", "info");
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
619
659
|
const message = await deps.stopPolling();
|
|
660
|
+
if (!isCurrent()) return;
|
|
620
661
|
if (message) ctx.ui.notify(message, "info");
|
|
621
662
|
deps.queueAgentConnectionContext?.(false);
|
|
622
663
|
} catch (error) {
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
"warning",
|
|
627
|
-
);
|
|
628
|
-
throw error;
|
|
664
|
+
deps.recordConnectionEvent?.(error, "disconnect");
|
|
665
|
+
if (!isCurrent()) return;
|
|
666
|
+
ctx.ui.notify("Telegram disconnect incomplete; keep Pi open. Check /telegram-status --debug.", "warning");
|
|
629
667
|
} finally {
|
|
630
|
-
deps.updateStatus(ctx);
|
|
668
|
+
if (isCurrent()) deps.updateStatus(ctx);
|
|
631
669
|
}
|
|
632
670
|
},
|
|
633
671
|
});
|
|
@@ -1466,7 +1504,7 @@ export async function handleTelegramNextCommand(deps: {
|
|
|
1466
1504
|
deps.updateStatus();
|
|
1467
1505
|
}
|
|
1468
1506
|
|
|
1469
|
-
|
|
1507
|
+
async function handleTelegramContinueCommand<TMessage, TContext>(
|
|
1470
1508
|
message: TMessage,
|
|
1471
1509
|
ctx: TContext,
|
|
1472
1510
|
deps: {
|
|
@@ -1492,7 +1530,7 @@ function dispatchNextQueuedTelegramTurnAfterCompact(
|
|
|
1492
1530
|
deps.dispatchNextQueuedTelegramTurn();
|
|
1493
1531
|
}
|
|
1494
1532
|
|
|
1495
|
-
|
|
1533
|
+
function buildTelegramNewConfirmationReplyMarkup(): TelegramCompactConfirmationReplyMarkup {
|
|
1496
1534
|
return {
|
|
1497
1535
|
inline_keyboard: [
|
|
1498
1536
|
[
|
|
@@ -1503,7 +1541,7 @@ export function buildTelegramNewConfirmationReplyMarkup(): TelegramCompactConfir
|
|
|
1503
1541
|
};
|
|
1504
1542
|
}
|
|
1505
1543
|
|
|
1506
|
-
|
|
1544
|
+
function getTelegramNewConfirmationHtml(): string {
|
|
1507
1545
|
return "<b>Start a new session?</b>";
|
|
1508
1546
|
}
|
|
1509
1547
|
|
|
@@ -1550,7 +1588,7 @@ export async function handleTelegramNewConfirmationCallback<TContext>(
|
|
|
1550
1588
|
return true;
|
|
1551
1589
|
}
|
|
1552
1590
|
|
|
1553
|
-
|
|
1591
|
+
function buildTelegramCompactConfirmationReplyMarkup(): TelegramCompactConfirmationReplyMarkup {
|
|
1554
1592
|
return {
|
|
1555
1593
|
inline_keyboard: [
|
|
1556
1594
|
[
|
|
@@ -1561,11 +1599,11 @@ export function buildTelegramCompactConfirmationReplyMarkup(): TelegramCompactCo
|
|
|
1561
1599
|
};
|
|
1562
1600
|
}
|
|
1563
1601
|
|
|
1564
|
-
|
|
1602
|
+
function getTelegramCompactConfirmationHtml(): string {
|
|
1565
1603
|
return "<b>Compact session?</b>";
|
|
1566
1604
|
}
|
|
1567
1605
|
|
|
1568
|
-
|
|
1606
|
+
async function openTelegramCompactConfirmation(
|
|
1569
1607
|
target: TelegramCommandMessageTarget,
|
|
1570
1608
|
deps: TelegramCompactConfirmationDeps,
|
|
1571
1609
|
): Promise<void> {
|
|
@@ -2335,7 +2373,7 @@ export const TELEGRAM_INTERNAL_COMMAND_DESCRIPTION =
|
|
|
2335
2373
|
export const TELEGRAM_INTERNAL_MANUAL_USE_MESSAGE =
|
|
2336
2374
|
"This internal Telegram command cannot be run manually.";
|
|
2337
2375
|
|
|
2338
|
-
|
|
2376
|
+
function delayTelegramSessionAction(delayMs: number): Promise<void> {
|
|
2339
2377
|
return new Promise<void>((resolve) => setTimeout(resolve, delayMs));
|
|
2340
2378
|
}
|
|
2341
2379
|
|
|
@@ -2395,7 +2433,7 @@ export async function settleTelegramSessionReplacement(
|
|
|
2395
2433
|
return "stale";
|
|
2396
2434
|
}
|
|
2397
2435
|
|
|
2398
|
-
|
|
2436
|
+
function createTelegramSessionReplacementSettlementRuntime<TContext>(deps: {
|
|
2399
2437
|
resolve: (ctx: TContext) => TelegramSessionReplacementSettlementDeps | undefined;
|
|
2400
2438
|
onResult?: (result: "none" | "settled" | "expired" | "failed" | "stale") => void;
|
|
2401
2439
|
onError?: (error: unknown) => void;
|