@llblab/pi-telegram 0.19.2 → 0.20.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 +5 -3
- package/BACKLOG.md +15 -2
- package/CHANGELOG.md +10 -0
- package/README.md +57 -57
- package/docs/architecture.md +4 -1
- package/docs/public-api.md +1 -1
- package/index.ts +67 -12
- package/lib/bindings.ts +61 -11
- package/lib/bus-follower.ts +27 -8
- package/lib/bus-leader.ts +26 -42
- package/lib/bus.ts +19 -22
- package/lib/commands.ts +41 -8
- package/lib/config.ts +165 -26
- package/lib/locks.ts +78 -25
- package/lib/{runtime-log.ts → logs.ts} +58 -27
- package/lib/media.ts +97 -4
- package/lib/outbound-buttons.ts +1 -1
- package/lib/outbound.ts +5 -8
- package/lib/paths.ts +77 -0
- package/lib/prompt-templates.ts +3 -1
- package/lib/prompts.ts +1 -1
- package/lib/queue.ts +32 -1
- package/lib/routing.ts +106 -41
- package/lib/status.ts +6 -1
- package/lib/sync.ts +22 -17
- package/lib/telegram-api.ts +13 -14
- package/lib/threads.ts +142 -55
- package/lib/turns.ts +47 -13
- package/package.json +2 -2
- /package/{banner.png → screenshot.png} +0 -0
package/lib/telegram-api.ts
CHANGED
|
@@ -10,8 +10,8 @@ import { randomUUID } from "node:crypto";
|
|
|
10
10
|
import { createWriteStream, openAsBlob } from "node:fs";
|
|
11
11
|
import { mkdir, readdir, stat, unlink, writeFile } from "node:fs/promises";
|
|
12
12
|
import { request as requestHttps } from "node:https";
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { resolveTelegramTempDir } from "./paths.ts";
|
|
15
15
|
import { Readable, Transform } from "node:stream";
|
|
16
16
|
import { pipeline } from "node:stream/promises";
|
|
17
17
|
|
|
@@ -34,10 +34,7 @@ export function getTelegramInboundFileByteLimitFromEnv(
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
function getTelegramApiTempDir(): string {
|
|
37
|
-
|
|
38
|
-
? resolve(process.env.PI_CODING_AGENT_DIR)
|
|
39
|
-
: join(homedir(), ".pi", "agent");
|
|
40
|
-
return join(agentDir, "tmp", "telegram");
|
|
37
|
+
return resolveTelegramTempDir();
|
|
41
38
|
}
|
|
42
39
|
const TELEGRAM_TEMP_FILE_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
43
40
|
const TELEGRAM_INBOUND_FILE_MAX_BYTES = getTelegramInboundFileByteLimitFromEnv(
|
|
@@ -47,10 +44,7 @@ const TELEGRAM_INBOUND_FILE_MAX_BYTES = getTelegramInboundFileByteLimitFromEnv(
|
|
|
47
44
|
);
|
|
48
45
|
|
|
49
46
|
export type TelegramNetworkFamilyPolicy =
|
|
50
|
-
| "
|
|
51
|
-
| "ipv4"
|
|
52
|
-
| "ipv6"
|
|
53
|
-
| "ipv4-fallback";
|
|
47
|
+
"auto" | "ipv4" | "ipv6" | "ipv4-fallback";
|
|
54
48
|
|
|
55
49
|
const TELEGRAM_NETWORK_FAMILY_ENV = "PI_TELEGRAM_NETWORK_FAMILY";
|
|
56
50
|
const TELEGRAM_NETWORK_FAMILY_VALUES = new Set<TelegramNetworkFamilyPolicy>([
|
|
@@ -1123,10 +1117,15 @@ export function createTelegramAssistantDraftSender(deps: {
|
|
|
1123
1117
|
if (text === undefined || deps.getAssistantRenderingMode() === "rich") {
|
|
1124
1118
|
return sendNativeDraft(chatId, draftId, text, options);
|
|
1125
1119
|
}
|
|
1126
|
-
return deps.sendMessageDraft(
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1120
|
+
return deps.sendMessageDraft(
|
|
1121
|
+
chatId,
|
|
1122
|
+
draftId,
|
|
1123
|
+
deps.renderMarkdownToHtmlDraft(text),
|
|
1124
|
+
{
|
|
1125
|
+
...options,
|
|
1126
|
+
parse_mode: "HTML",
|
|
1127
|
+
},
|
|
1128
|
+
);
|
|
1130
1129
|
};
|
|
1131
1130
|
}
|
|
1132
1131
|
|
package/lib/threads.ts
CHANGED
|
@@ -7,11 +7,15 @@
|
|
|
7
7
|
import { randomUUID } from "node:crypto";
|
|
8
8
|
import { existsSync } from "node:fs";
|
|
9
9
|
import { chmod, mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
10
|
-
import {
|
|
11
|
-
import { dirname, join, resolve } from "node:path";
|
|
10
|
+
import { dirname } from "node:path";
|
|
12
11
|
|
|
12
|
+
import type { TelegramApiCallOptions } from "./telegram-api.ts";
|
|
13
13
|
import type { TelegramTarget } from "./target.ts";
|
|
14
14
|
import * as ThreadReconciler from "./thread-reconciler.ts";
|
|
15
|
+
import {
|
|
16
|
+
resolveAgentDir,
|
|
17
|
+
resolveTelegramProfileTempFilePath,
|
|
18
|
+
} from "./paths.ts";
|
|
15
19
|
|
|
16
20
|
export interface TelegramThreadNameInput {
|
|
17
21
|
seed: string;
|
|
@@ -22,18 +26,18 @@ export interface TelegramThreadNameInput {
|
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
export type TelegramTopicTargetStatus =
|
|
25
|
-
| "
|
|
26
|
-
| "offline"
|
|
27
|
-
| "stale"
|
|
28
|
-
| "pending"
|
|
29
|
-
| "starting"
|
|
30
|
-
| "failed";
|
|
29
|
+
"active" | "offline" | "stale" | "pending" | "starting" | "failed";
|
|
31
30
|
|
|
32
31
|
export type TelegramTopicSyncStatus = "open" | "closed" | "deleted" | "unknown";
|
|
33
32
|
|
|
34
33
|
export type TelegramThreadOwner =
|
|
35
|
-
| {
|
|
36
|
-
|
|
34
|
+
| {
|
|
35
|
+
kind: "leader";
|
|
36
|
+
cwd?: string;
|
|
37
|
+
instanceId?: string;
|
|
38
|
+
telegramProfile?: string;
|
|
39
|
+
}
|
|
40
|
+
| { kind: "manual-follower"; instanceId: string; telegramProfile?: string }
|
|
37
41
|
| { kind: "pending-topic"; chatId: number; threadId: number }
|
|
38
42
|
| { kind: "legacy"; key: string };
|
|
39
43
|
|
|
@@ -200,6 +204,7 @@ export interface TelegramTopicTargetStore {
|
|
|
200
204
|
getIdentityByProfileKey: (
|
|
201
205
|
profileKey: string,
|
|
202
206
|
) => TelegramThreadIdentityRecord | undefined;
|
|
207
|
+
forgetIdentityByProfileKey: (profileKey: string) => boolean;
|
|
203
208
|
upsert: (record: TelegramTopicTargetRecord) => TelegramTopicTargetRecord;
|
|
204
209
|
markOfflineByInstanceId: (instanceId: string) => number;
|
|
205
210
|
markStaleByTarget: (
|
|
@@ -224,7 +229,7 @@ export interface TelegramTopicTargetStore {
|
|
|
224
229
|
}
|
|
225
230
|
|
|
226
231
|
export interface TelegramTopicTargetStoreOptions {
|
|
227
|
-
path: string;
|
|
232
|
+
path: string | (() => string);
|
|
228
233
|
getNowMs?: () => number;
|
|
229
234
|
}
|
|
230
235
|
|
|
@@ -236,7 +241,9 @@ export interface TelegramTopicTargetProvisionerDeps {
|
|
|
236
241
|
| "getByProfileKey"
|
|
237
242
|
| "getActiveByInstanceId"
|
|
238
243
|
| "getIdentityByProfileKey"
|
|
244
|
+
| "forgetIdentityByProfileKey"
|
|
239
245
|
| "upsert"
|
|
246
|
+
| "markStaleByTarget"
|
|
240
247
|
| "allocateSlot"
|
|
241
248
|
| "claimReusableTarget"
|
|
242
249
|
| "upsertPendingProvision"
|
|
@@ -246,6 +253,7 @@ export interface TelegramTopicTargetProvisionerDeps {
|
|
|
246
253
|
callApi: <TResponse>(
|
|
247
254
|
method: string,
|
|
248
255
|
body: Record<string, unknown>,
|
|
256
|
+
options?: TelegramApiCallOptions,
|
|
249
257
|
) => Promise<TResponse>;
|
|
250
258
|
topicNameTemplate?: string;
|
|
251
259
|
getNowMs?: () => number;
|
|
@@ -288,12 +296,6 @@ interface TelegramTopicResult {
|
|
|
288
296
|
message_thread_id?: number;
|
|
289
297
|
}
|
|
290
298
|
|
|
291
|
-
function getAgentDir(): string {
|
|
292
|
-
return process.env.PI_CODING_AGENT_DIR
|
|
293
|
-
? resolve(process.env.PI_CODING_AGENT_DIR)
|
|
294
|
-
: join(homedir(), ".pi", "agent");
|
|
295
|
-
}
|
|
296
|
-
|
|
297
299
|
function hashString(value: string): number {
|
|
298
300
|
let hash = 2166136261;
|
|
299
301
|
for (let index = 0; index < value.length; index += 1) {
|
|
@@ -339,22 +341,39 @@ export function createTelegramThreadName(
|
|
|
339
341
|
);
|
|
340
342
|
}
|
|
341
343
|
|
|
342
|
-
export function getTelegramStatePath(
|
|
343
|
-
|
|
344
|
+
export function getTelegramStatePath(
|
|
345
|
+
agentDir = resolveAgentDir(),
|
|
346
|
+
profileName?: string,
|
|
347
|
+
): string {
|
|
348
|
+
return resolveTelegramProfileTempFilePath(
|
|
349
|
+
"state",
|
|
350
|
+
"json",
|
|
351
|
+
agentDir,
|
|
352
|
+
profileName,
|
|
353
|
+
);
|
|
344
354
|
}
|
|
345
355
|
|
|
346
|
-
export function getTelegramTopicTargetsPath(
|
|
347
|
-
|
|
356
|
+
export function getTelegramTopicTargetsPath(
|
|
357
|
+
agentDir = resolveAgentDir(),
|
|
358
|
+
profileName?: string,
|
|
359
|
+
): string {
|
|
360
|
+
return getTelegramStatePath(agentDir, profileName);
|
|
348
361
|
}
|
|
349
362
|
|
|
350
363
|
export function getTelegramThreadOwnerKey(owner: TelegramThreadOwner): string {
|
|
351
364
|
switch (owner.kind) {
|
|
352
|
-
case "leader":
|
|
353
|
-
|
|
365
|
+
case "leader": {
|
|
366
|
+
const base = owner.cwd
|
|
354
367
|
? `cwd:${owner.cwd}`
|
|
355
368
|
: `leader:${owner.instanceId ?? "default"}`;
|
|
369
|
+
return owner.telegramProfile
|
|
370
|
+
? `profile:${owner.telegramProfile}:${base}`
|
|
371
|
+
: base;
|
|
372
|
+
}
|
|
356
373
|
case "manual-follower":
|
|
357
|
-
return
|
|
374
|
+
return owner.telegramProfile
|
|
375
|
+
? `profile:${owner.telegramProfile}:manual:${owner.instanceId}`
|
|
376
|
+
: `manual:${owner.instanceId}`;
|
|
358
377
|
case "pending-topic":
|
|
359
378
|
return `topic:${owner.chatId}:${owner.threadId}`;
|
|
360
379
|
case "legacy":
|
|
@@ -365,6 +384,16 @@ export function getTelegramThreadOwnerKey(owner: TelegramThreadOwner): string {
|
|
|
365
384
|
export function getTelegramThreadOwnerFromProfileKey(
|
|
366
385
|
profileKey: string,
|
|
367
386
|
): TelegramThreadOwner {
|
|
387
|
+
if (profileKey.startsWith("profile:")) {
|
|
388
|
+
const [, telegramProfile, ownerKind, ...rest] = profileKey.split(":");
|
|
389
|
+
const value = rest.join(":");
|
|
390
|
+
if (ownerKind === "cwd")
|
|
391
|
+
return { kind: "leader", cwd: value, telegramProfile };
|
|
392
|
+
if (ownerKind === "leader")
|
|
393
|
+
return { kind: "leader", instanceId: value, telegramProfile };
|
|
394
|
+
if (ownerKind === "manual")
|
|
395
|
+
return { kind: "manual-follower", instanceId: value, telegramProfile };
|
|
396
|
+
}
|
|
368
397
|
if (profileKey.startsWith("cwd:"))
|
|
369
398
|
return { kind: "leader", cwd: profileKey.slice(4) };
|
|
370
399
|
if (profileKey.startsWith("manual:")) {
|
|
@@ -444,7 +473,9 @@ function cloneRecord(
|
|
|
444
473
|
};
|
|
445
474
|
}
|
|
446
475
|
|
|
447
|
-
function getPersistedThreadName(
|
|
476
|
+
function getPersistedThreadName(
|
|
477
|
+
record: Record<string, unknown>,
|
|
478
|
+
): string | undefined {
|
|
448
479
|
const value =
|
|
449
480
|
typeof record.threadName === "string"
|
|
450
481
|
? record.threadName
|
|
@@ -561,9 +592,8 @@ function normalizeIdentityRecord(
|
|
|
561
592
|
};
|
|
562
593
|
const persistedThreadName = getPersistedThreadName(record);
|
|
563
594
|
if (persistedThreadName) {
|
|
564
|
-
const threadName =
|
|
565
|
-
persistedThreadName
|
|
566
|
-
);
|
|
595
|
+
const threadName =
|
|
596
|
+
normalizeTelegramTopicTargetThreadName(persistedThreadName);
|
|
567
597
|
if (threadName) identity.threadName = threadName;
|
|
568
598
|
}
|
|
569
599
|
if (typeof record.slot === "string" && /^[A-Z]$/.test(record.slot)) {
|
|
@@ -841,6 +871,7 @@ export function createTelegramTopicTargetStore(
|
|
|
841
871
|
let pendingProvisions: TelegramThreadPendingProvision[] = [];
|
|
842
872
|
let syncObservations: TelegramTopicSyncObservation[] = [];
|
|
843
873
|
let loaded = false;
|
|
874
|
+
let loadedPath: string | undefined;
|
|
844
875
|
let dirty = false;
|
|
845
876
|
let statusSnapshot: {
|
|
846
877
|
runtime?: Record<string, unknown>;
|
|
@@ -866,8 +897,26 @@ export function createTelegramTopicTargetStore(
|
|
|
866
897
|
});
|
|
867
898
|
};
|
|
868
899
|
|
|
900
|
+
const getPath = () =>
|
|
901
|
+
typeof options.path === "function" ? options.path() : options.path;
|
|
902
|
+
const resetForPath = (path: string) => {
|
|
903
|
+
if (loadedPath === path) return;
|
|
904
|
+
botState = { threadMode: "unknown" };
|
|
905
|
+
records = new Map();
|
|
906
|
+
identities = new Map();
|
|
907
|
+
reservations = [];
|
|
908
|
+
pendingProvisions = [];
|
|
909
|
+
syncObservations = [];
|
|
910
|
+
statusSnapshot = {};
|
|
911
|
+
loaded = false;
|
|
912
|
+
dirty = false;
|
|
913
|
+
loadedPath = path;
|
|
914
|
+
};
|
|
915
|
+
|
|
869
916
|
const loadFromDisk = async () => {
|
|
870
|
-
|
|
917
|
+
const path = getPath();
|
|
918
|
+
resetForPath(path);
|
|
919
|
+
if (!existsSync(path)) {
|
|
871
920
|
botState = { threadMode: "unknown" };
|
|
872
921
|
records = new Map();
|
|
873
922
|
identities = new Map();
|
|
@@ -877,7 +926,7 @@ export function createTelegramTopicTargetStore(
|
|
|
877
926
|
loaded = true;
|
|
878
927
|
return;
|
|
879
928
|
}
|
|
880
|
-
const content = await readFile(
|
|
929
|
+
const content = await readFile(path, "utf8");
|
|
881
930
|
const file = parseTopicTargetFile(JSON.parse(content));
|
|
882
931
|
botState = file.bot;
|
|
883
932
|
records = new Map(
|
|
@@ -920,9 +969,11 @@ export function createTelegramTopicTargetStore(
|
|
|
920
969
|
await loadFromDisk();
|
|
921
970
|
},
|
|
922
971
|
async persist() {
|
|
972
|
+
const path = getPath();
|
|
973
|
+
if (loadedPath !== path && !dirty) resetForPath(path);
|
|
923
974
|
if (!loaded && !dirty) await loadFromDisk();
|
|
924
|
-
await mkdir(dirname(
|
|
925
|
-
const tempPath = `${
|
|
975
|
+
await mkdir(dirname(path), { recursive: true });
|
|
976
|
+
const tempPath = `${path}.${process.pid}.${Date.now()}.${randomUUID()}.tmp`;
|
|
926
977
|
const nowMs = getNowMs();
|
|
927
978
|
reservations = reservations.filter(
|
|
928
979
|
(reservation) =>
|
|
@@ -967,8 +1018,8 @@ export function createTelegramTopicTargetStore(
|
|
|
967
1018
|
mode: 0o600,
|
|
968
1019
|
});
|
|
969
1020
|
await chmod(tempPath, 0o600);
|
|
970
|
-
await rename(tempPath,
|
|
971
|
-
await chmod(
|
|
1021
|
+
await rename(tempPath, path);
|
|
1022
|
+
await chmod(path, 0o600);
|
|
972
1023
|
loaded = true;
|
|
973
1024
|
dirty = false;
|
|
974
1025
|
},
|
|
@@ -1060,6 +1111,7 @@ export function createTelegramTopicTargetStore(
|
|
|
1060
1111
|
dirty = true;
|
|
1061
1112
|
},
|
|
1062
1113
|
setStatusSnapshot(snapshot) {
|
|
1114
|
+
if (!loadedPath) loadedPath = getPath();
|
|
1063
1115
|
statusSnapshot = { ...snapshot };
|
|
1064
1116
|
},
|
|
1065
1117
|
getByProfileKey(profileKey) {
|
|
@@ -1085,6 +1137,17 @@ export function createTelegramTopicTargetStore(
|
|
|
1085
1137
|
const identity = identities.get(ownerKey) ?? identities.get(profileKey);
|
|
1086
1138
|
return identity ? cloneIdentityRecord(identity) : undefined;
|
|
1087
1139
|
},
|
|
1140
|
+
forgetIdentityByProfileKey(profileKey) {
|
|
1141
|
+
const ownerKey = getTelegramThreadOwnerKey(
|
|
1142
|
+
getTelegramThreadOwnerFromProfileKey(profileKey),
|
|
1143
|
+
);
|
|
1144
|
+
const removedOwner = identities.delete(ownerKey);
|
|
1145
|
+
const removedProfile = identities.delete(profileKey);
|
|
1146
|
+
if (!removedOwner && !removedProfile) return false;
|
|
1147
|
+
loaded = true;
|
|
1148
|
+
dirty = true;
|
|
1149
|
+
return true;
|
|
1150
|
+
},
|
|
1088
1151
|
upsert(record) {
|
|
1089
1152
|
const next = cloneRecord(record);
|
|
1090
1153
|
const nextOwnerKey = getRecordOwnerKey(next);
|
|
@@ -1316,9 +1379,7 @@ function getGraphemeSegments(value: string): string[] {
|
|
|
1316
1379
|
}
|
|
1317
1380
|
|
|
1318
1381
|
export function getTelegramTopicIdentityName(threadName: string): string {
|
|
1319
|
-
return getGraphemeSegments(
|
|
1320
|
-
normalizeTelegramTopicTargetThreadName(threadName),
|
|
1321
|
-
)
|
|
1382
|
+
return getGraphemeSegments(normalizeTelegramTopicTargetThreadName(threadName))
|
|
1322
1383
|
.join("")
|
|
1323
1384
|
.trim();
|
|
1324
1385
|
}
|
|
@@ -1332,7 +1393,7 @@ const TELEGRAM_THREAD_NAME_PALETTE: Record<string, readonly string[]> = {
|
|
|
1332
1393
|
F: ["Falcon", "Fjord", "Flint", "Forest", "Fable"],
|
|
1333
1394
|
G: ["Grove", "Glade", "Glyph", "Garnet", "Gale"],
|
|
1334
1395
|
H: ["Harbor", "Hawk", "Hazel", "Helix", "Haven"],
|
|
1335
|
-
I: ["Iris", "Ivory", "Iron", "Isle", "
|
|
1396
|
+
I: ["Iris", "Ivory", "Iron", "Isle", "Idea"],
|
|
1336
1397
|
J: ["Jade", "Juno", "Jolt", "Jewel", "Jasper"],
|
|
1337
1398
|
K: ["Kite", "Karma", "Kernel", "Kodiak", "Kelp"],
|
|
1338
1399
|
L: ["Lumen", "Laurel", "Lynx", "Lotus", "Lagoon"],
|
|
@@ -1363,7 +1424,10 @@ export function chooseTelegramThreadName(input: {
|
|
|
1363
1424
|
const index = input.getRandom
|
|
1364
1425
|
? Math.max(
|
|
1365
1426
|
0,
|
|
1366
|
-
Math.min(
|
|
1427
|
+
Math.min(
|
|
1428
|
+
names.length - 1,
|
|
1429
|
+
Math.floor(input.getRandom() * names.length),
|
|
1430
|
+
),
|
|
1367
1431
|
)
|
|
1368
1432
|
: getTelegramThreadNameEntropyIndex(input.entropy, names.length);
|
|
1369
1433
|
return names[index];
|
|
@@ -1505,6 +1569,7 @@ export interface TelegramPromoteFollowerBindingToLeaderDeps {
|
|
|
1505
1569
|
store: TelegramTopicTargetStore;
|
|
1506
1570
|
instanceId: string;
|
|
1507
1571
|
cwd?: string;
|
|
1572
|
+
telegramProfile?: string;
|
|
1508
1573
|
target?: TelegramTarget;
|
|
1509
1574
|
slot?: string;
|
|
1510
1575
|
threadName?: string;
|
|
@@ -1529,6 +1594,7 @@ export async function promoteTelegramFollowerBindingToLeader(
|
|
|
1529
1594
|
kind: "leader",
|
|
1530
1595
|
cwd: deps.cwd,
|
|
1531
1596
|
instanceId: deps.instanceId,
|
|
1597
|
+
...(deps.telegramProfile ? { telegramProfile: deps.telegramProfile } : {}),
|
|
1532
1598
|
};
|
|
1533
1599
|
const record = deps.store.upsert({
|
|
1534
1600
|
profileKey: getTelegramThreadOwnerKey(owner),
|
|
@@ -1537,11 +1603,13 @@ export async function promoteTelegramFollowerBindingToLeader(
|
|
|
1537
1603
|
status: "active",
|
|
1538
1604
|
createdAtMs: existing?.createdAtMs ?? nowMs,
|
|
1539
1605
|
updatedAtMs: nowMs,
|
|
1540
|
-
...(existing?.threadName ?? deps.threadName
|
|
1606
|
+
...((existing?.threadName ?? deps.threadName)
|
|
1541
1607
|
? { threadName: existing?.threadName ?? deps.threadName }
|
|
1542
1608
|
: {}),
|
|
1543
1609
|
instanceId: deps.instanceId,
|
|
1544
|
-
...(existing?.slot ?? deps.slot
|
|
1610
|
+
...((existing?.slot ?? deps.slot)
|
|
1611
|
+
? { slot: existing?.slot ?? deps.slot }
|
|
1612
|
+
: {}),
|
|
1545
1613
|
...(existing?.syncStatus ? { syncStatus: existing.syncStatus } : {}),
|
|
1546
1614
|
...(existing?.lastSyncObservedAtMs !== undefined
|
|
1547
1615
|
? { lastSyncObservedAtMs: existing.lastSyncObservedAtMs }
|
|
@@ -1559,12 +1627,12 @@ export interface TelegramOwnTopicProvisionDeps {
|
|
|
1559
1627
|
getAllowedUserId: () => number | undefined;
|
|
1560
1628
|
instanceId: string;
|
|
1561
1629
|
cwd?: string;
|
|
1630
|
+
telegramProfile?: string;
|
|
1562
1631
|
getNowMs?: () => number;
|
|
1563
1632
|
getRandom?: () => number;
|
|
1564
1633
|
getCurrentLeaderEpoch?: () => number | string | undefined;
|
|
1565
1634
|
getThreadReconciliationMachineState?: () =>
|
|
1566
|
-
|
|
1567
|
-
| undefined;
|
|
1635
|
+
ThreadReconciler.ThreadReconciliationMachineState | undefined;
|
|
1568
1636
|
recordThreadReconciliationPlan?: (
|
|
1569
1637
|
plan: ThreadReconciler.ThreadReconciliationPlan,
|
|
1570
1638
|
) => void;
|
|
@@ -1596,7 +1664,12 @@ export async function provisionOwnBusTopic(
|
|
|
1596
1664
|
deps: TelegramOwnTopicProvisionDeps,
|
|
1597
1665
|
): Promise<TelegramOwnTopicProvisionResult | undefined> {
|
|
1598
1666
|
const chatId = deps.getAllowedUserId();
|
|
1599
|
-
let profileKey =
|
|
1667
|
+
let profileKey = getTelegramThreadOwnerKey({
|
|
1668
|
+
kind: "leader",
|
|
1669
|
+
cwd: deps.cwd,
|
|
1670
|
+
instanceId: deps.instanceId,
|
|
1671
|
+
telegramProfile: deps.telegramProfile,
|
|
1672
|
+
});
|
|
1600
1673
|
if (typeof chatId !== "number") return undefined;
|
|
1601
1674
|
await deps.store.load();
|
|
1602
1675
|
const reservationCleanupPorts = {
|
|
@@ -1678,11 +1751,12 @@ export async function provisionOwnBusTopic(
|
|
|
1678
1751
|
},
|
|
1679
1752
|
);
|
|
1680
1753
|
const nowMs = Date.now();
|
|
1681
|
-
const currentLeaderOwner: TelegramThreadOwner =
|
|
1682
|
-
"leader
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1754
|
+
const currentLeaderOwner: TelegramThreadOwner = {
|
|
1755
|
+
kind: "leader",
|
|
1756
|
+
cwd: deps.cwd,
|
|
1757
|
+
instanceId: deps.instanceId,
|
|
1758
|
+
...(deps.telegramProfile ? { telegramProfile: deps.telegramProfile } : {}),
|
|
1759
|
+
};
|
|
1686
1760
|
const recordsBeforePreviousLeaderCleanup = deps.store.list();
|
|
1687
1761
|
const previousLeaderCleanupPlan = ThreadReconciler.planThreadReconciliation({
|
|
1688
1762
|
nowMs,
|
|
@@ -2108,8 +2182,20 @@ export function createTelegramTopicTargetProvisioner(
|
|
|
2108
2182
|
const getRandom = deps.getRandom;
|
|
2109
2183
|
return async (request) => {
|
|
2110
2184
|
normalizeCurrentThreadNameSlots(deps.store);
|
|
2111
|
-
|
|
2112
|
-
const
|
|
2185
|
+
let existing = deps.store.getByProfileKey(request.profileKey);
|
|
2186
|
+
const isManualFollowerRequest = request.owner?.kind === "manual-follower";
|
|
2187
|
+
if (isManualFollowerRequest && existing && isCurrentThreadRecord(existing)) {
|
|
2188
|
+
deps.store.markStaleByTarget(
|
|
2189
|
+
existing.target,
|
|
2190
|
+
"unknown",
|
|
2191
|
+
"Manual follower runtime was replaced before reconnect.",
|
|
2192
|
+
);
|
|
2193
|
+
deps.store.forgetIdentityByProfileKey(request.profileKey);
|
|
2194
|
+
existing = undefined;
|
|
2195
|
+
}
|
|
2196
|
+
const identity = isManualFollowerRequest && !existing
|
|
2197
|
+
? undefined
|
|
2198
|
+
: deps.store.getIdentityByProfileKey(request.profileKey);
|
|
2113
2199
|
const nowMs = getNowMs();
|
|
2114
2200
|
if (existing && isCurrentThreadRecord(existing)) {
|
|
2115
2201
|
const slot = existing.slot ?? deps.store.allocateSlot(request.profileKey);
|
|
@@ -2166,7 +2252,9 @@ export function createTelegramTopicTargetProvisioner(
|
|
|
2166
2252
|
(candidateThreadName ? undefined : identity?.slot) ??
|
|
2167
2253
|
deps.store.allocateSlot(
|
|
2168
2254
|
request.profileKey,
|
|
2169
|
-
|
|
2255
|
+
isManualFollowerRequest
|
|
2256
|
+
? request.preferredSlot
|
|
2257
|
+
: request.preferredSlot ?? preferredNameSlot,
|
|
2170
2258
|
);
|
|
2171
2259
|
const requestThreadName =
|
|
2172
2260
|
candidateThreadName &&
|
|
@@ -2197,15 +2285,14 @@ export function createTelegramTopicTargetProvisioner(
|
|
|
2197
2285
|
name: getTelegramTopicName(
|
|
2198
2286
|
{
|
|
2199
2287
|
...request,
|
|
2200
|
-
...(requestThreadName
|
|
2201
|
-
? { threadName: requestThreadName }
|
|
2202
|
-
: {}),
|
|
2288
|
+
...(requestThreadName ? { threadName: requestThreadName } : {}),
|
|
2203
2289
|
},
|
|
2204
2290
|
deps.topicNameTemplate ??
|
|
2205
2291
|
(requestThreadName ? "{threadName}" : "{slot}"),
|
|
2206
2292
|
slot,
|
|
2207
2293
|
),
|
|
2208
2294
|
},
|
|
2295
|
+
{ maxAttempts: 1 },
|
|
2209
2296
|
);
|
|
2210
2297
|
threadId = topic.message_thread_id;
|
|
2211
2298
|
if (typeof threadId !== "number" || !Number.isInteger(threadId)) {
|
package/lib/turns.ts
CHANGED
|
@@ -8,12 +8,14 @@ import { readFile } from "node:fs/promises";
|
|
|
8
8
|
import { basename, dirname, join } from "node:path";
|
|
9
9
|
|
|
10
10
|
import {
|
|
11
|
+
appendTelegramForwardContext,
|
|
11
12
|
appendTelegramReplyContext,
|
|
13
|
+
buildTelegramReplyContextBlock,
|
|
12
14
|
collectTelegramMessageIds,
|
|
13
15
|
downloadTelegramMessageFiles,
|
|
16
|
+
extractTelegramForwardContextText,
|
|
14
17
|
extractTelegramMessagesPromptText,
|
|
15
18
|
extractTelegramMessagesText,
|
|
16
|
-
extractTelegramReplyContextText,
|
|
17
19
|
formatTelegramHistoryText,
|
|
18
20
|
guessMediaType,
|
|
19
21
|
type DownloadedTelegramMessageFile,
|
|
@@ -132,8 +134,14 @@ function appendTelegramAttachmentSection(
|
|
|
132
134
|
return `${prefix}${header}\n${items.map((item) => `- ${item}`).join("\n")}`;
|
|
133
135
|
}
|
|
134
136
|
|
|
137
|
+
function appendTelegramSourceContext(text: string, sourceContext: string | undefined): string {
|
|
138
|
+
if (!sourceContext) return text;
|
|
139
|
+
return text ? `${text}\n\n${sourceContext}` : sourceContext;
|
|
140
|
+
}
|
|
141
|
+
|
|
135
142
|
function appendTelegramPromptText(prompt: string, rawText: string): string {
|
|
136
143
|
if (!rawText) return prompt;
|
|
144
|
+
if (rawText.startsWith("\n")) return `${prompt}${rawText}`;
|
|
137
145
|
return `${prompt} ${rawText}`;
|
|
138
146
|
}
|
|
139
147
|
|
|
@@ -160,6 +168,7 @@ export function buildTelegramTurnPrompt(options: {
|
|
|
160
168
|
files: DownloadedTelegramTurnFile[];
|
|
161
169
|
promptFiles?: DownloadedTelegramTurnFile[];
|
|
162
170
|
handlerOutputs?: string[];
|
|
171
|
+
sourceContext?: string;
|
|
163
172
|
historyTurns?: Pick<PendingTelegramTurn, "historyText">[];
|
|
164
173
|
timeLine?: string | null;
|
|
165
174
|
voiceContext?: Record<string, string>;
|
|
@@ -181,6 +190,7 @@ export function buildTelegramTurnPrompt(options: {
|
|
|
181
190
|
}
|
|
182
191
|
const promptFiles = options.promptFiles ?? options.files;
|
|
183
192
|
prompt = appendTelegramAttachmentSection(prompt, promptFiles);
|
|
193
|
+
prompt = appendTelegramSourceContext(prompt, options.sourceContext);
|
|
184
194
|
prompt = appendTelegramListSection(
|
|
185
195
|
prompt,
|
|
186
196
|
"outputs",
|
|
@@ -368,6 +378,7 @@ export interface BuildTelegramPromptTurnOptions {
|
|
|
368
378
|
files: DownloadedTelegramTurnFile[];
|
|
369
379
|
promptFiles?: DownloadedTelegramTurnFile[];
|
|
370
380
|
handlerOutputs?: string[];
|
|
381
|
+
sourceContext?: string;
|
|
371
382
|
timeLine?: string | null;
|
|
372
383
|
readBinaryFile: (path: string) => Promise<Uint8Array>;
|
|
373
384
|
inferImageMimeType: (path: string) => string | undefined;
|
|
@@ -399,6 +410,7 @@ export interface TelegramPromptTurnRuntimeBuilderDeps<
|
|
|
399
410
|
isVoiceReplyModeConfigured?: () => boolean;
|
|
400
411
|
/** Returns the visible thread label for a message target, used to add thread context to the prompt prefix. */
|
|
401
412
|
getTelegramThreadLabel?: (message: { chat: { id: number }; message_thread_id?: number }) => string | undefined;
|
|
413
|
+
getAllowedUserId?: () => number | undefined;
|
|
402
414
|
}
|
|
403
415
|
|
|
404
416
|
export function createTelegramPromptTurnRuntimeBuilder<
|
|
@@ -413,8 +425,18 @@ export function createTelegramPromptTurnRuntimeBuilder<
|
|
|
413
425
|
) => Promise<PendingTelegramTurn> {
|
|
414
426
|
return async (messages, historyTurns = [], ctx) => {
|
|
415
427
|
const rawText = extractTelegramMessagesText(messages);
|
|
416
|
-
const
|
|
417
|
-
|
|
428
|
+
const firstMessage = messages[0];
|
|
429
|
+
const replyFiles = firstMessage?.reply_to_message
|
|
430
|
+
? await downloadTelegramMessageFiles(
|
|
431
|
+
[firstMessage.reply_to_message as typeof firstMessage],
|
|
432
|
+
{ downloadFile: deps.downloadFile },
|
|
433
|
+
)
|
|
434
|
+
: [];
|
|
435
|
+
const replyContext = firstMessage
|
|
436
|
+
? buildTelegramReplyContextBlock(firstMessage, replyFiles)
|
|
437
|
+
: "";
|
|
438
|
+
const forwardContext = firstMessage
|
|
439
|
+
? extractTelegramForwardContextText(firstMessage, deps.getAllowedUserId?.())
|
|
418
440
|
: "";
|
|
419
441
|
const files = await downloadTelegramMessageFiles(messages, {
|
|
420
442
|
downloadFile: deps.downloadFile,
|
|
@@ -422,10 +444,18 @@ export function createTelegramPromptTurnRuntimeBuilder<
|
|
|
422
444
|
const processed = deps.processAttachments
|
|
423
445
|
? await deps.processAttachments(files, rawText, ctx as TContext)
|
|
424
446
|
: { rawText, promptFiles: files };
|
|
425
|
-
const
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
447
|
+
const sourceBlocks: string[] = [];
|
|
448
|
+
let promptRawText = processed.rawText;
|
|
449
|
+
if (forwardContext) {
|
|
450
|
+
sourceBlocks.push(
|
|
451
|
+
`[forward|${forwardContext.replace(/:\s+/g, ":")}]${
|
|
452
|
+
processed.rawText ? ` ${processed.rawText}` : ""
|
|
453
|
+
}`,
|
|
454
|
+
);
|
|
455
|
+
promptRawText = "";
|
|
456
|
+
}
|
|
457
|
+
if (replyContext) sourceBlocks.push(replyContext);
|
|
458
|
+
const sourceContext = sourceBlocks.join("\n\n");
|
|
429
459
|
// Compute voice mode once and pass it to both the turn builder and the prompt contribution helper
|
|
430
460
|
const voiceReplyMode = deps.getVoiceReplyMode?.();
|
|
431
461
|
const chatId = messages[0]?.chat.id;
|
|
@@ -433,7 +463,6 @@ export function createTelegramPromptTurnRuntimeBuilder<
|
|
|
433
463
|
deps.resolveTimeLine && chatId !== undefined
|
|
434
464
|
? deps.resolveTimeLine(chatId)
|
|
435
465
|
: null;
|
|
436
|
-
const firstMessage = messages[0];
|
|
437
466
|
const threadLabel = firstMessage
|
|
438
467
|
? deps.getTelegramThreadLabel?.(firstMessage)
|
|
439
468
|
: undefined;
|
|
@@ -443,7 +472,8 @@ export function createTelegramPromptTurnRuntimeBuilder<
|
|
|
443
472
|
messages,
|
|
444
473
|
historyTurns,
|
|
445
474
|
queueOrder: deps.allocateQueueOrder(),
|
|
446
|
-
rawText:
|
|
475
|
+
rawText: promptRawText,
|
|
476
|
+
sourceContext,
|
|
447
477
|
statusText: processed.rawText,
|
|
448
478
|
files,
|
|
449
479
|
promptFiles: processed.promptFiles,
|
|
@@ -495,6 +525,7 @@ export async function buildTelegramPromptTurn(
|
|
|
495
525
|
files: options.files,
|
|
496
526
|
promptFiles: options.promptFiles,
|
|
497
527
|
handlerOutputs: options.handlerOutputs,
|
|
528
|
+
sourceContext: options.sourceContext,
|
|
498
529
|
historyTurns: options.historyTurns,
|
|
499
530
|
timeLine: options.timeLine,
|
|
500
531
|
voiceContext: showVoiceContext
|
|
@@ -534,10 +565,13 @@ export async function buildTelegramPromptTurn(
|
|
|
534
565
|
laneOrder: options.queueOrder,
|
|
535
566
|
queuedAttachments: [],
|
|
536
567
|
content,
|
|
537
|
-
historyText:
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
568
|
+
historyText: appendTelegramSourceContext(
|
|
569
|
+
formatTelegramHistoryText(
|
|
570
|
+
options.rawText,
|
|
571
|
+
options.promptFiles ?? options.files,
|
|
572
|
+
options.handlerOutputs,
|
|
573
|
+
),
|
|
574
|
+
options.sourceContext,
|
|
541
575
|
),
|
|
542
576
|
statusSummary: formatTelegramTurnStatusSummary(
|
|
543
577
|
options.statusText ?? options.rawText,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llblab/pi-telegram",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"BACKLOG.md",
|
|
43
43
|
"CHANGELOG.md",
|
|
44
44
|
"docs/",
|
|
45
|
-
"
|
|
45
|
+
"screenshot.png"
|
|
46
46
|
],
|
|
47
47
|
"exports": {
|
|
48
48
|
".": "./index.ts",
|
|
File without changes
|