@llblab/pi-telegram 0.22.1 → 0.23.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 +24 -13
- package/BACKLOG.md +1 -51
- package/CHANGELOG.md +41 -19
- package/README.md +3 -1
- package/docs/README.md +1 -1
- package/docs/activity.md +8 -0
- package/docs/architecture.md +16 -15
- package/docs/locks.md +21 -15
- package/docs/multi-instance-bus.md +17 -16
- package/docs/outbound.md +16 -0
- package/docs/public-api.md +9 -4
- package/index.ts +76 -65
- package/lib/activity.ts +100 -3
- package/lib/bindings.ts +86 -15
- package/lib/bus-follower.ts +205 -17
- package/lib/bus-leader.ts +333 -244
- package/lib/bus.ts +82 -19
- package/lib/commands.ts +28 -4
- package/lib/config.ts +44 -33
- package/lib/media.ts +102 -1
- package/lib/menu-settings.ts +4 -1
- package/lib/outbound-attachments.ts +150 -1
- package/lib/outbound.ts +106 -1
- package/lib/polling.ts +5 -0
- package/lib/queue.ts +41 -48
- package/lib/routing.ts +88 -1
- package/lib/sync.ts +40 -3
- package/lib/telegram-api.ts +77 -13
- package/lib/text-groups.ts +183 -16
- package/lib/thread-reconciler.ts +69 -93
- package/lib/threads.ts +131 -66
- package/lib/turns.ts +102 -13
- package/lib/updates.ts +2 -0
- package/package.json +3 -2
package/lib/threads.ts
CHANGED
|
@@ -44,7 +44,13 @@ export interface TelegramThreadNameInput {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
export type TelegramTopicTargetStatus =
|
|
47
|
-
|
|
47
|
+
| "active"
|
|
48
|
+
| "offline"
|
|
49
|
+
| "stale"
|
|
50
|
+
| "pending"
|
|
51
|
+
| "starting"
|
|
52
|
+
| "probe-required"
|
|
53
|
+
| "failed";
|
|
48
54
|
|
|
49
55
|
export type TelegramTopicSyncStatus = "open" | "closed" | "deleted" | "unknown";
|
|
50
56
|
|
|
@@ -231,7 +237,6 @@ export interface TelegramTopicTargetStore {
|
|
|
231
237
|
target: TelegramTarget & { threadId: number },
|
|
232
238
|
) => Promise<boolean>;
|
|
233
239
|
removePendingProvision: (id: string) => boolean;
|
|
234
|
-
removeReservationByTarget: (target: TelegramTarget) => boolean;
|
|
235
240
|
getBotState: () => TelegramBotStateSnapshot;
|
|
236
241
|
setBotState: (state: Partial<TelegramBotStateSnapshot>) => void;
|
|
237
242
|
setStatusSnapshot: (snapshot: {
|
|
@@ -438,6 +443,60 @@ export function getTelegramTopicTargetsPath(
|
|
|
438
443
|
return getTelegramStatePath(agentDir, profileName);
|
|
439
444
|
}
|
|
440
445
|
|
|
446
|
+
const TELEGRAM_LEADER_SESSION_HANDOFF_KEY =
|
|
447
|
+
"__piTelegramLeaderSessionHandoff";
|
|
448
|
+
export const TELEGRAM_LEADER_SESSION_HANDOFF_TTL_MS = 30_000;
|
|
449
|
+
|
|
450
|
+
export interface TelegramLeaderSessionHandoff {
|
|
451
|
+
pid: number;
|
|
452
|
+
instanceId: string;
|
|
453
|
+
createdAtMs: number;
|
|
454
|
+
profileKey: string;
|
|
455
|
+
target: TelegramTarget & { threadId: number };
|
|
456
|
+
slot?: string;
|
|
457
|
+
threadName?: string;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
export function getTelegramLeaderSessionHandoff():
|
|
461
|
+
| TelegramLeaderSessionHandoff
|
|
462
|
+
| undefined {
|
|
463
|
+
const value = (globalThis as Record<string, unknown>)[
|
|
464
|
+
TELEGRAM_LEADER_SESSION_HANDOFF_KEY
|
|
465
|
+
];
|
|
466
|
+
if (!value || typeof value !== "object") return undefined;
|
|
467
|
+
const handoff = value as Partial<TelegramLeaderSessionHandoff>;
|
|
468
|
+
if (
|
|
469
|
+
typeof handoff.pid !== "number" ||
|
|
470
|
+
typeof handoff.instanceId !== "string" ||
|
|
471
|
+
typeof handoff.createdAtMs !== "number" ||
|
|
472
|
+
typeof handoff.profileKey !== "string" ||
|
|
473
|
+
typeof handoff.target?.chatId !== "number" ||
|
|
474
|
+
typeof handoff.target.threadId !== "number"
|
|
475
|
+
) {
|
|
476
|
+
return undefined;
|
|
477
|
+
}
|
|
478
|
+
return handoff as TelegramLeaderSessionHandoff;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
export function setTelegramLeaderSessionHandoff(
|
|
482
|
+
handoff: TelegramLeaderSessionHandoff | undefined,
|
|
483
|
+
): void {
|
|
484
|
+
const store = globalThis as Record<string, unknown>;
|
|
485
|
+
if (!handoff) delete store[TELEGRAM_LEADER_SESSION_HANDOFF_KEY];
|
|
486
|
+
else store[TELEGRAM_LEADER_SESSION_HANDOFF_KEY] = handoff;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
export function isTelegramLeaderSessionHandoffFresh(
|
|
490
|
+
handoff: TelegramLeaderSessionHandoff | undefined,
|
|
491
|
+
options: { pid?: number; nowMs?: number; ttlMs?: number } = {},
|
|
492
|
+
): handoff is TelegramLeaderSessionHandoff {
|
|
493
|
+
if (!handoff) return false;
|
|
494
|
+
const pid = options.pid ?? process.pid;
|
|
495
|
+
const nowMs = options.nowMs ?? Date.now();
|
|
496
|
+
const ttlMs = options.ttlMs ?? TELEGRAM_LEADER_SESSION_HANDOFF_TTL_MS;
|
|
497
|
+
return handoff.pid === pid && nowMs - handoff.createdAtMs <= ttlMs;
|
|
498
|
+
}
|
|
499
|
+
|
|
441
500
|
export function getTelegramThreadOwnerKey(owner: TelegramThreadOwner): string {
|
|
442
501
|
switch (owner.kind) {
|
|
443
502
|
case "leader": {
|
|
@@ -593,6 +652,7 @@ function normalizeRecord(
|
|
|
593
652
|
status !== "stale" &&
|
|
594
653
|
status !== "pending" &&
|
|
595
654
|
status !== "starting" &&
|
|
655
|
+
status !== "probe-required" &&
|
|
596
656
|
status !== "failed"
|
|
597
657
|
)
|
|
598
658
|
return undefined;
|
|
@@ -655,6 +715,10 @@ function isCurrentThreadRecord(record: TelegramTopicTargetRecord): boolean {
|
|
|
655
715
|
);
|
|
656
716
|
}
|
|
657
717
|
|
|
718
|
+
function isPersistedThreadRecord(record: TelegramTopicTargetRecord): boolean {
|
|
719
|
+
return isCurrentThreadRecord(record) || record.status === "probe-required";
|
|
720
|
+
}
|
|
721
|
+
|
|
658
722
|
function normalizeIdentityRecord(
|
|
659
723
|
value: unknown,
|
|
660
724
|
): TelegramThreadIdentityRecord | undefined {
|
|
@@ -882,7 +946,7 @@ function parseTopicTargetFile(value: unknown): TelegramTopicTargetFile {
|
|
|
882
946
|
.map((record) => normalizeRecord(record))
|
|
883
947
|
.filter(
|
|
884
948
|
(record): record is TelegramTopicTargetRecord =>
|
|
885
|
-
!!record &&
|
|
949
|
+
!!record && isPersistedThreadRecord(record),
|
|
886
950
|
);
|
|
887
951
|
return {
|
|
888
952
|
version: 1,
|
|
@@ -1172,7 +1236,7 @@ export function createTelegramTopicTargetStore(
|
|
|
1172
1236
|
isPendingProvisionLiveOrTargeted(provision, nowMs),
|
|
1173
1237
|
);
|
|
1174
1238
|
const currentRecords = Array.from(records.values())
|
|
1175
|
-
.filter(
|
|
1239
|
+
.filter(isPersistedThreadRecord)
|
|
1176
1240
|
.map(cloneRecord);
|
|
1177
1241
|
records = new Map(
|
|
1178
1242
|
currentRecords.map((record) => [
|
|
@@ -1331,15 +1395,6 @@ export function createTelegramTopicTargetStore(
|
|
|
1331
1395
|
if (changed) markDirty();
|
|
1332
1396
|
return changed;
|
|
1333
1397
|
},
|
|
1334
|
-
removeReservationByTarget(target) {
|
|
1335
|
-
const before = reservations.length;
|
|
1336
|
-
reservations = reservations.filter(
|
|
1337
|
-
(reservation) => !targetMatches(reservation.target, target),
|
|
1338
|
-
);
|
|
1339
|
-
const changed = reservations.length !== before;
|
|
1340
|
-
if (changed) markDirty();
|
|
1341
|
-
return changed;
|
|
1342
|
-
},
|
|
1343
1398
|
getBotState() {
|
|
1344
1399
|
return Object.fromEntries(
|
|
1345
1400
|
Object.entries(botState).filter(([, value]) => value !== undefined),
|
|
@@ -1411,7 +1466,7 @@ export function createTelegramTopicTargetStore(
|
|
|
1411
1466
|
records.delete(getRecordOwnerKey(existing));
|
|
1412
1467
|
}
|
|
1413
1468
|
}
|
|
1414
|
-
if (!
|
|
1469
|
+
if (!isPersistedThreadRecord(next)) {
|
|
1415
1470
|
rememberIdentity(next);
|
|
1416
1471
|
records.delete(nextOwnerKey);
|
|
1417
1472
|
markDirty();
|
|
@@ -1918,9 +1973,6 @@ export async function provisionOwnBusTopic(
|
|
|
1918
1973
|
syncStatus?: "closed" | "deleted",
|
|
1919
1974
|
lastSyncError?: string,
|
|
1920
1975
|
) => deps.store.markStaleByTarget(target, syncStatus, lastSyncError),
|
|
1921
|
-
removeReservationByTarget: (
|
|
1922
|
-
target: TelegramTarget & { threadId: number },
|
|
1923
|
-
) => deps.store.removeReservationByTarget(target),
|
|
1924
1976
|
removePendingProvisionById: (id: string) =>
|
|
1925
1977
|
deps.store.removePendingProvision(id),
|
|
1926
1978
|
persist: () => deps.store.persist(),
|
|
@@ -1959,36 +2011,6 @@ export async function provisionOwnBusTopic(
|
|
|
1959
2011
|
durationMs: Date.now() - reservationCleanupApplyStartedAtMs,
|
|
1960
2012
|
actions: reservationCleanupPlan.actions.length,
|
|
1961
2013
|
});
|
|
1962
|
-
const reservationProbeResults: ThreadReconciler.ThreadReservationProbeResult[] =
|
|
1963
|
-
[];
|
|
1964
|
-
deps.recordEvent("bus", "Bus leader reservation probes skipped", {
|
|
1965
|
-
phase: "leader-topic-reservation-probe-skipped",
|
|
1966
|
-
reservations: reservationsBeforeCleanup.length,
|
|
1967
|
-
});
|
|
1968
|
-
const reservationProbePlan = ThreadReconciler.planThreadReconciliation({
|
|
1969
|
-
nowMs: Date.now(),
|
|
1970
|
-
currentLeaderEpoch: deps.getCurrentLeaderEpoch?.(),
|
|
1971
|
-
previousState: deps.getThreadReconciliationMachineState?.(),
|
|
1972
|
-
records: deps.store.list(),
|
|
1973
|
-
reservations: deps.store.listReservations(),
|
|
1974
|
-
pendingProvisions: deps.store.listPendingProvisions(),
|
|
1975
|
-
reservationProbeResults,
|
|
1976
|
-
});
|
|
1977
|
-
deps.recordThreadReconciliationPlan?.(reservationProbePlan);
|
|
1978
|
-
const reservationProbeApplyStartedAtMs = Date.now();
|
|
1979
|
-
await ThreadReconciler.applyThreadReconciliationPlan(
|
|
1980
|
-
reservationProbePlan,
|
|
1981
|
-
reservationCleanupPorts,
|
|
1982
|
-
);
|
|
1983
|
-
deps.recordEvent(
|
|
1984
|
-
"bus",
|
|
1985
|
-
"Bus leader reservation probe reconciliation applied",
|
|
1986
|
-
{
|
|
1987
|
-
phase: "leader-topic-reservation-probe-apply-duration",
|
|
1988
|
-
durationMs: Date.now() - reservationProbeApplyStartedAtMs,
|
|
1989
|
-
actions: reservationProbePlan.actions.length,
|
|
1990
|
-
},
|
|
1991
|
-
);
|
|
1992
2014
|
const nowMs = Date.now();
|
|
1993
2015
|
const currentLeaderOwner: TelegramThreadOwner = {
|
|
1994
2016
|
kind: "leader",
|
|
@@ -1996,6 +2018,51 @@ export async function provisionOwnBusTopic(
|
|
|
1996
2018
|
instanceId: deps.instanceId,
|
|
1997
2019
|
...(deps.telegramProfile ? { telegramProfile: deps.telegramProfile } : {}),
|
|
1998
2020
|
};
|
|
2021
|
+
const leaderSessionHandoff = getTelegramLeaderSessionHandoff();
|
|
2022
|
+
if (
|
|
2023
|
+
isTelegramLeaderSessionHandoffFresh(leaderSessionHandoff) &&
|
|
2024
|
+
leaderSessionHandoff.profileKey === profileKey
|
|
2025
|
+
) {
|
|
2026
|
+
const existingHandoffRecord = deps.store
|
|
2027
|
+
.list()
|
|
2028
|
+
.find((record) => targetMatches(record.target, leaderSessionHandoff.target));
|
|
2029
|
+
deps.store.upsert({
|
|
2030
|
+
profileKey,
|
|
2031
|
+
owner: currentLeaderOwner,
|
|
2032
|
+
target: { ...leaderSessionHandoff.target },
|
|
2033
|
+
status: "active",
|
|
2034
|
+
createdAtMs: existingHandoffRecord?.createdAtMs ?? leaderSessionHandoff.createdAtMs,
|
|
2035
|
+
updatedAtMs: nowMs,
|
|
2036
|
+
threadName:
|
|
2037
|
+
existingHandoffRecord?.threadName ?? leaderSessionHandoff.threadName,
|
|
2038
|
+
instanceId: deps.instanceId,
|
|
2039
|
+
slot: existingHandoffRecord?.slot ?? leaderSessionHandoff.slot,
|
|
2040
|
+
...(existingHandoffRecord?.syncStatus
|
|
2041
|
+
? { syncStatus: existingHandoffRecord.syncStatus }
|
|
2042
|
+
: {}),
|
|
2043
|
+
...(existingHandoffRecord?.lastSyncObservedAtMs !== undefined
|
|
2044
|
+
? {
|
|
2045
|
+
lastSyncObservedAtMs:
|
|
2046
|
+
existingHandoffRecord.lastSyncObservedAtMs,
|
|
2047
|
+
}
|
|
2048
|
+
: {}),
|
|
2049
|
+
lastReconcileAction: "leader-session-handoff-restored",
|
|
2050
|
+
});
|
|
2051
|
+
await deps.store.persist();
|
|
2052
|
+
setTelegramLeaderSessionHandoff(undefined);
|
|
2053
|
+
deps.recordEvent("bus", "Bus leader session handoff restored", {
|
|
2054
|
+
phase: "leader-session-handoff-restore",
|
|
2055
|
+
chatId: leaderSessionHandoff.target.chatId,
|
|
2056
|
+
threadId: leaderSessionHandoff.target.threadId,
|
|
2057
|
+
slot: existingHandoffRecord?.slot ?? leaderSessionHandoff.slot,
|
|
2058
|
+
threadName:
|
|
2059
|
+
existingHandoffRecord?.threadName ?? leaderSessionHandoff.threadName,
|
|
2060
|
+
previousInstanceId: leaderSessionHandoff.instanceId,
|
|
2061
|
+
instanceId: deps.instanceId,
|
|
2062
|
+
});
|
|
2063
|
+
} else if (leaderSessionHandoff) {
|
|
2064
|
+
setTelegramLeaderSessionHandoff(undefined);
|
|
2065
|
+
}
|
|
1999
2066
|
const recordsBeforePreviousLeaderCleanup = deps.store.list();
|
|
2000
2067
|
const previousLeaderCleanupPlan = ThreadReconciler.planThreadReconciliation({
|
|
2001
2068
|
nowMs,
|
|
@@ -2051,7 +2118,7 @@ export async function provisionOwnBusTopic(
|
|
|
2051
2118
|
continue;
|
|
2052
2119
|
}
|
|
2053
2120
|
const previousLeaderCleanupStartedAtMs = Date.now();
|
|
2054
|
-
await ThreadReconciler.applyThreadReconciliationPlan(
|
|
2121
|
+
const cleanup = await ThreadReconciler.applyThreadReconciliationPlan(
|
|
2055
2122
|
{ actions: [action] },
|
|
2056
2123
|
{
|
|
2057
2124
|
callApi: deps.callApi,
|
|
@@ -2097,6 +2164,11 @@ export async function provisionOwnBusTopic(
|
|
|
2097
2164
|
"Telegram leader ownership changed during topic reconciliation.",
|
|
2098
2165
|
);
|
|
2099
2166
|
}
|
|
2167
|
+
if (cleanup.incompleteActions?.length) {
|
|
2168
|
+
throw new Error(
|
|
2169
|
+
"Previous Telegram leader topic deletion was not confirmed.",
|
|
2170
|
+
);
|
|
2171
|
+
}
|
|
2100
2172
|
deps.store.markStaleByTarget(record.target);
|
|
2101
2173
|
deps.store.reserveThread({
|
|
2102
2174
|
target: record.target,
|
|
@@ -2256,6 +2328,7 @@ export interface TelegramCurrentInstanceThreadRuntime {
|
|
|
2256
2328
|
findRecord(): TelegramTopicTargetRecord | undefined;
|
|
2257
2329
|
getRecord(): TelegramTopicTargetRecord | undefined;
|
|
2258
2330
|
getIdentity(target?: TelegramTarget): TelegramInstanceThreadIdentityCandidate;
|
|
2331
|
+
getRestorationIdentity(): TelegramInstanceThreadIdentityCandidate;
|
|
2259
2332
|
}
|
|
2260
2333
|
|
|
2261
2334
|
export function createTelegramCurrentInstanceThreadRuntime(deps: {
|
|
@@ -2301,6 +2374,14 @@ export function createTelegramCurrentInstanceThreadRuntime(deps: {
|
|
|
2301
2374
|
record,
|
|
2302
2375
|
});
|
|
2303
2376
|
},
|
|
2377
|
+
getRestorationIdentity() {
|
|
2378
|
+
const follower = deps.getFollower();
|
|
2379
|
+
return resolveTelegramInstanceThreadIdentity({
|
|
2380
|
+
follower: follower?.registered ? follower : undefined,
|
|
2381
|
+
leader: deps.getLeader(),
|
|
2382
|
+
record: findRecord(),
|
|
2383
|
+
});
|
|
2384
|
+
},
|
|
2304
2385
|
};
|
|
2305
2386
|
}
|
|
2306
2387
|
|
|
@@ -2657,25 +2738,9 @@ export function createTelegramTopicTargetProvisioner(
|
|
|
2657
2738
|
};
|
|
2658
2739
|
assertLeaderEpoch("start");
|
|
2659
2740
|
normalizeCurrentThreadNameSlots(deps.store);
|
|
2660
|
-
|
|
2741
|
+
const existing = deps.store.getByProfileKey(request.profileKey);
|
|
2661
2742
|
const isManualFollowerRequest = request.owner?.kind === "manual-follower";
|
|
2662
|
-
|
|
2663
|
-
isManualFollowerRequest &&
|
|
2664
|
-
existing &&
|
|
2665
|
-
isCurrentThreadRecord(existing)
|
|
2666
|
-
) {
|
|
2667
|
-
deps.store.markStaleByTarget(
|
|
2668
|
-
existing.target,
|
|
2669
|
-
"unknown",
|
|
2670
|
-
"Manual follower runtime was replaced before reconnect.",
|
|
2671
|
-
);
|
|
2672
|
-
deps.store.forgetIdentityByProfileKey(request.profileKey);
|
|
2673
|
-
existing = undefined;
|
|
2674
|
-
}
|
|
2675
|
-
const identity =
|
|
2676
|
-
isManualFollowerRequest && !existing
|
|
2677
|
-
? undefined
|
|
2678
|
-
: deps.store.getIdentityByProfileKey(request.profileKey);
|
|
2743
|
+
const identity = deps.store.getIdentityByProfileKey(request.profileKey);
|
|
2679
2744
|
const nowMs = getNowMs();
|
|
2680
2745
|
if (existing && isCurrentThreadRecord(existing)) {
|
|
2681
2746
|
const slot = existing.slot ?? deps.store.allocateSlot(request.profileKey);
|
package/lib/turns.ts
CHANGED
|
@@ -9,9 +9,11 @@ import { basename, dirname, join } from "node:path";
|
|
|
9
9
|
|
|
10
10
|
import {
|
|
11
11
|
buildTelegramReplyContextBlock,
|
|
12
|
+
collectTelegramFileInfos,
|
|
12
13
|
collectTelegramMessageIds,
|
|
13
14
|
downloadTelegramMessageFiles,
|
|
14
15
|
extractTelegramForwardContextText,
|
|
16
|
+
extractTelegramMessageText,
|
|
15
17
|
extractTelegramMessagesPromptText,
|
|
16
18
|
extractTelegramMessagesText,
|
|
17
19
|
formatTelegramHistoryText,
|
|
@@ -132,11 +134,36 @@ function appendTelegramAttachmentSection(
|
|
|
132
134
|
return `${prefix}${header}\n${items.map((item) => `- ${item}`).join("\n")}`;
|
|
133
135
|
}
|
|
134
136
|
|
|
135
|
-
function appendTelegramSourceContext(
|
|
137
|
+
function appendTelegramSourceContext(
|
|
138
|
+
text: string,
|
|
139
|
+
sourceContext: string | undefined,
|
|
140
|
+
): string {
|
|
136
141
|
if (!sourceContext) return text;
|
|
137
142
|
return text ? `${text}\n\n${sourceContext}` : sourceContext;
|
|
138
143
|
}
|
|
139
144
|
|
|
145
|
+
|
|
146
|
+
function buildTelegramForwardContextBlock(options: {
|
|
147
|
+
context: string;
|
|
148
|
+
text: string;
|
|
149
|
+
files: DownloadedTelegramTurnFile[];
|
|
150
|
+
}): string {
|
|
151
|
+
const metadata = options.context.replace(/:\s+/g, ":");
|
|
152
|
+
const from = metadata.match(/^from:(.+)$/)?.[1];
|
|
153
|
+
let block = `[forward|${metadata}]${options.text ? ` ${options.text}` : ""}`;
|
|
154
|
+
if (options.files.length === 0) return block;
|
|
155
|
+
const dirs = [...new Set(options.files.map((file) => dirname(file.path)))];
|
|
156
|
+
const sameDir = dirs.length === 1;
|
|
157
|
+
const header = `[attachments${from ? `|from:${from}` : ""}]${
|
|
158
|
+
sameDir ? ` ${dirs[0]}` : ""
|
|
159
|
+
}`;
|
|
160
|
+
const items = sameDir
|
|
161
|
+
? options.files.map((file) => `/${basename(file.path)}`)
|
|
162
|
+
: options.files.map((file) => file.path);
|
|
163
|
+
block += `\n\n${header}\n${items.map((item) => `- ${item}`).join("\n")}`;
|
|
164
|
+
return block;
|
|
165
|
+
}
|
|
166
|
+
|
|
140
167
|
function appendTelegramPromptText(prompt: string, rawText: string): string {
|
|
141
168
|
if (!rawText) return prompt;
|
|
142
169
|
if (rawText.startsWith("\n")) return `${prompt}${rawText}`;
|
|
@@ -165,6 +192,7 @@ export function buildTelegramTurnPrompt(options: {
|
|
|
165
192
|
rawText: string;
|
|
166
193
|
files: DownloadedTelegramTurnFile[];
|
|
167
194
|
promptFiles?: DownloadedTelegramTurnFile[];
|
|
195
|
+
displayFiles?: DownloadedTelegramTurnFile[];
|
|
168
196
|
handlerOutputs?: string[];
|
|
169
197
|
sourceContext?: string;
|
|
170
198
|
historyTurns?: Pick<PendingTelegramTurn, "historyText">[];
|
|
@@ -186,14 +214,15 @@ export function buildTelegramTurnPrompt(options: {
|
|
|
186
214
|
? `${prompt}\n${options.rawText}`
|
|
187
215
|
: appendTelegramPromptText(prompt, options.rawText);
|
|
188
216
|
}
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
prompt =
|
|
217
|
+
const displayFiles =
|
|
218
|
+
options.displayFiles ?? options.promptFiles ?? options.files;
|
|
219
|
+
prompt = appendTelegramAttachmentSection(prompt, displayFiles);
|
|
192
220
|
prompt = appendTelegramListSection(
|
|
193
221
|
prompt,
|
|
194
222
|
"outputs",
|
|
195
223
|
options.handlerOutputs ?? [],
|
|
196
224
|
);
|
|
225
|
+
prompt = appendTelegramSourceContext(prompt, options.sourceContext);
|
|
197
226
|
if (options.voiceContext) {
|
|
198
227
|
prompt = appendTelegramVoiceContext(prompt, options.voiceContext);
|
|
199
228
|
}
|
|
@@ -375,6 +404,7 @@ export interface BuildTelegramPromptTurnOptions {
|
|
|
375
404
|
statusText?: string;
|
|
376
405
|
files: DownloadedTelegramTurnFile[];
|
|
377
406
|
promptFiles?: DownloadedTelegramTurnFile[];
|
|
407
|
+
displayFiles?: DownloadedTelegramTurnFile[];
|
|
378
408
|
handlerOutputs?: string[];
|
|
379
409
|
sourceContext?: string;
|
|
380
410
|
timeLine?: string | null;
|
|
@@ -433,9 +463,26 @@ export function createTelegramPromptTurnRuntimeBuilder<
|
|
|
433
463
|
const replyContext = firstMessage
|
|
434
464
|
? buildTelegramReplyContextBlock(firstMessage, replyFiles)
|
|
435
465
|
: "";
|
|
436
|
-
const
|
|
437
|
-
|
|
438
|
-
|
|
466
|
+
const forwardEntries = messages.flatMap((message) => {
|
|
467
|
+
const context = extractTelegramForwardContextText(
|
|
468
|
+
message,
|
|
469
|
+
deps.getAllowedUserId?.(),
|
|
470
|
+
);
|
|
471
|
+
return context
|
|
472
|
+
? [
|
|
473
|
+
{
|
|
474
|
+
context,
|
|
475
|
+
text: extractTelegramMessageText(message),
|
|
476
|
+
message,
|
|
477
|
+
fileNames: new Set(
|
|
478
|
+
collectTelegramFileInfos(message.rich_message ? [message] : []).map(
|
|
479
|
+
(file) => file.fileName,
|
|
480
|
+
),
|
|
481
|
+
),
|
|
482
|
+
},
|
|
483
|
+
]
|
|
484
|
+
: [];
|
|
485
|
+
});
|
|
439
486
|
const files = await downloadTelegramMessageFiles(messages, {
|
|
440
487
|
downloadFile: deps.downloadFile,
|
|
441
488
|
});
|
|
@@ -444,13 +491,51 @@ export function createTelegramPromptTurnRuntimeBuilder<
|
|
|
444
491
|
: { rawText, promptFiles: files };
|
|
445
492
|
const sourceBlocks: string[] = [];
|
|
446
493
|
let promptRawText = processed.rawText;
|
|
447
|
-
|
|
494
|
+
const forwardedFilePaths = new Set<string>();
|
|
495
|
+
const getForwardFiles = (entry: (typeof forwardEntries)[number]) =>
|
|
496
|
+
(processed.promptFiles ?? files).filter((file) => {
|
|
497
|
+
if (!entry.fileNames.has(file.fileName)) return false;
|
|
498
|
+
forwardedFilePaths.add(file.path);
|
|
499
|
+
return true;
|
|
500
|
+
});
|
|
501
|
+
if (forwardEntries.length === 1 && messages.length === 1) {
|
|
502
|
+
const [forward] = forwardEntries;
|
|
448
503
|
sourceBlocks.push(
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
504
|
+
buildTelegramForwardContextBlock({
|
|
505
|
+
context: forward!.context,
|
|
506
|
+
text: processed.rawText,
|
|
507
|
+
files: getForwardFiles(forward!),
|
|
508
|
+
}),
|
|
452
509
|
);
|
|
453
510
|
promptRawText = "";
|
|
511
|
+
} else if (forwardEntries.length > 0 && processed.rawText === rawText) {
|
|
512
|
+
const forwardedMessages = new Set(
|
|
513
|
+
forwardEntries.map((entry) => entry.message),
|
|
514
|
+
);
|
|
515
|
+
promptRawText = messages
|
|
516
|
+
.filter((message) => !forwardedMessages.has(message))
|
|
517
|
+
.map(extractTelegramMessageText)
|
|
518
|
+
.filter(Boolean)
|
|
519
|
+
.join("\n\n");
|
|
520
|
+
sourceBlocks.push(
|
|
521
|
+
...forwardEntries.map((entry) =>
|
|
522
|
+
buildTelegramForwardContextBlock({
|
|
523
|
+
context: entry.context,
|
|
524
|
+
text: entry.text,
|
|
525
|
+
files: getForwardFiles(entry),
|
|
526
|
+
}),
|
|
527
|
+
),
|
|
528
|
+
);
|
|
529
|
+
} else if (forwardEntries.length > 0) {
|
|
530
|
+
sourceBlocks.push(
|
|
531
|
+
...forwardEntries.map((entry) =>
|
|
532
|
+
buildTelegramForwardContextBlock({
|
|
533
|
+
context: entry.context,
|
|
534
|
+
text: "",
|
|
535
|
+
files: getForwardFiles(entry),
|
|
536
|
+
}),
|
|
537
|
+
),
|
|
538
|
+
);
|
|
454
539
|
}
|
|
455
540
|
if (replyContext) sourceBlocks.push(replyContext);
|
|
456
541
|
const sourceContext = sourceBlocks.join("\n\n");
|
|
@@ -475,6 +560,9 @@ export function createTelegramPromptTurnRuntimeBuilder<
|
|
|
475
560
|
statusText: processed.rawText,
|
|
476
561
|
files,
|
|
477
562
|
promptFiles: processed.promptFiles,
|
|
563
|
+
displayFiles: (processed.promptFiles ?? files).filter(
|
|
564
|
+
(file) => !forwardedFilePaths.has(file.path),
|
|
565
|
+
),
|
|
478
566
|
handlerOutputs: processed.handlerOutputs,
|
|
479
567
|
timeLine,
|
|
480
568
|
inferImageMimeType: guessMediaType,
|
|
@@ -522,6 +610,7 @@ export async function buildTelegramPromptTurn(
|
|
|
522
610
|
rawText: options.rawText,
|
|
523
611
|
files: options.files,
|
|
524
612
|
promptFiles: options.promptFiles,
|
|
613
|
+
displayFiles: options.displayFiles,
|
|
525
614
|
handlerOutputs: options.handlerOutputs,
|
|
526
615
|
sourceContext: options.sourceContext,
|
|
527
616
|
historyTurns: options.historyTurns,
|
|
@@ -566,14 +655,14 @@ export async function buildTelegramPromptTurn(
|
|
|
566
655
|
historyText: appendTelegramSourceContext(
|
|
567
656
|
formatTelegramHistoryText(
|
|
568
657
|
options.rawText,
|
|
569
|
-
options.promptFiles ?? options.files,
|
|
658
|
+
options.displayFiles ?? options.promptFiles ?? options.files,
|
|
570
659
|
options.handlerOutputs,
|
|
571
660
|
),
|
|
572
661
|
options.sourceContext,
|
|
573
662
|
),
|
|
574
663
|
statusSummary: formatTelegramTurnStatusSummary(
|
|
575
664
|
options.statusText ?? options.rawText,
|
|
576
|
-
options.promptFiles ?? options.files,
|
|
665
|
+
options.displayFiles ?? options.promptFiles ?? options.files,
|
|
577
666
|
options.handlerOutputs,
|
|
578
667
|
),
|
|
579
668
|
// Voice tagging (used for preview suppression and prompt guidance)
|
package/lib/updates.ts
CHANGED
|
@@ -249,6 +249,7 @@ export function getAuthorizedTelegramGuestMessage(
|
|
|
249
249
|
|
|
250
250
|
export interface TelegramMessageOwnershipView {
|
|
251
251
|
instanceId: string;
|
|
252
|
+
ownerGeneration?: string;
|
|
252
253
|
}
|
|
253
254
|
|
|
254
255
|
export type TelegramMessageOwnershipLookup = (
|
|
@@ -258,6 +259,7 @@ export type TelegramMessageOwnershipLookup = (
|
|
|
258
259
|
|
|
259
260
|
export interface TelegramTargetOwnershipView {
|
|
260
261
|
instanceId: string;
|
|
262
|
+
ownerGeneration?: string;
|
|
261
263
|
}
|
|
262
264
|
|
|
263
265
|
export type TelegramTargetOwnershipLookup = (
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llblab/pi-telegram",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
"node": ">=22.19.0"
|
|
28
28
|
},
|
|
29
29
|
"scripts": {
|
|
30
|
-
"test": "node --experimental-strip-types --test tests/*.test.ts",
|
|
30
|
+
"test": "node --experimental-strip-types --test --test-reporter=dot tests/*.test.ts",
|
|
31
|
+
"test:verbose": "node --experimental-strip-types --test --test-reporter=spec tests/*.test.ts",
|
|
31
32
|
"typecheck": "tsc --noEmit",
|
|
32
33
|
"audit": "npm audit",
|
|
33
34
|
"pack:check": "npm pack --dry-run",
|