@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/lib/threads.ts CHANGED
@@ -44,7 +44,13 @@ export interface TelegramThreadNameInput {
44
44
  }
45
45
 
46
46
  export type TelegramTopicTargetStatus =
47
- "active" | "offline" | "stale" | "pending" | "starting" | "failed";
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 && isCurrentThreadRecord(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(isCurrentThreadRecord)
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 (!isCurrentThreadRecord(next)) {
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
- let existing = deps.store.getByProfileKey(request.profileKey);
2741
+ const existing = deps.store.getByProfileKey(request.profileKey);
2661
2742
  const isManualFollowerRequest = request.owner?.kind === "manual-follower";
2662
- if (
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(text: string, sourceContext: string | undefined): string {
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 promptFiles = options.promptFiles ?? options.files;
190
- prompt = appendTelegramAttachmentSection(prompt, promptFiles);
191
- prompt = appendTelegramSourceContext(prompt, options.sourceContext);
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 forwardContext = firstMessage
437
- ? extractTelegramForwardContextText(firstMessage, deps.getAllowedUserId?.())
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
- if (forwardContext) {
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
- `[forward|${forwardContext.replace(/:\s+/g, ":")}]${
450
- processed.rawText ? ` ${processed.rawText}` : ""
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.22.1",
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",