@llblab/pi-telegram 0.19.3 → 0.20.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/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 { homedir } from "node:os";
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
- | "active"
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
- | { kind: "leader"; cwd?: string; instanceId?: string }
36
- | { kind: "manual-follower"; instanceId: string }
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(agentDir = getAgentDir()): string {
343
- return join(agentDir, "tmp", "telegram", "state.json");
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(agentDir = getAgentDir()): string {
347
- return getTelegramStatePath(agentDir);
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
- return owner.cwd
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 `manual:${owner.instanceId}`;
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(record: Record<string, unknown>): string | undefined {
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 = normalizeTelegramTopicTargetThreadName(
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
- if (!existsSync(options.path)) {
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(options.path, "utf8");
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(options.path), { recursive: true });
925
- const tempPath = `${options.path}.${process.pid}.${Date.now()}.${randomUUID()}.tmp`;
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, options.path);
971
- await chmod(options.path, 0o600);
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", "Ibis"],
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(names.length - 1, Math.floor(input.getRandom() * names.length)),
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 ? { slot: 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
- | ThreadReconciler.ThreadReconciliationMachineState
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 = deps.cwd ? `cwd:${deps.cwd}` : `leader:${deps.instanceId}`;
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 = profileKey.startsWith(
1682
- "leader:",
1683
- )
1684
- ? { kind: "leader", instanceId: deps.instanceId }
1685
- : { kind: "leader", cwd: deps.cwd, instanceId: deps.instanceId };
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
- const existing = deps.store.getByProfileKey(request.profileKey);
2112
- const identity = deps.store.getIdentityByProfileKey(request.profileKey);
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
- request.preferredSlot ?? preferredNameSlot,
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 replyContext = messages[0]
417
- ? extractTelegramReplyContextText(messages[0])
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 promptText = appendTelegramReplyContext(
426
- processed.rawText,
427
- replyContext,
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: promptText,
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: formatTelegramHistoryText(
538
- options.rawText,
539
- options.promptFiles ?? options.files,
540
- options.handlerOutputs,
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.19.3",
3
+ "version": "0.20.1",
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
- "banner.png"
45
+ "screenshot.png"
46
46
  ],
47
47
  "exports": {
48
48
  ".": "./index.ts",
@@ -59,7 +59,7 @@
59
59
  "extensions": [
60
60
  "./index.ts"
61
61
  ],
62
- "image": "https://github.com/llblab/pi-telegram/raw/main/banner.png"
62
+ "image": "https://github.com/llblab/pi-telegram/raw/main/screenshot.png"
63
63
  },
64
64
  "peerDependencies": {
65
65
  "@earendil-works/pi-agent-core": "*",
File without changes