@llblab/pi-telegram 0.20.0 → 0.20.2

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/bus.ts CHANGED
@@ -34,6 +34,40 @@ import { resolveAgentDir } from "./paths.ts";
34
34
 
35
35
  export type TelegramBusRole = "leader" | "follower";
36
36
 
37
+ export interface TelegramBusProcessRuntime {
38
+ instanceId: string;
39
+ manualFollowerOwnerId: string;
40
+ getLeaderSocketPath: () => string;
41
+ getFollowerSocketPath: () => string;
42
+ }
43
+
44
+ export function createTelegramBusProcessRuntime(input: {
45
+ getActiveProfileName: () => string | undefined;
46
+ pid: number;
47
+ parentPid: number;
48
+ createdAtMs: number;
49
+ }): TelegramBusProcessRuntime {
50
+ const instanceId = `${input.pid}:${input.createdAtMs}`;
51
+ const manualFollowerOwnerId = String(input.parentPid || input.pid);
52
+ return {
53
+ instanceId,
54
+ manualFollowerOwnerId,
55
+ getLeaderSocketPath: () =>
56
+ getTelegramBusSocketPath(
57
+ undefined,
58
+ undefined,
59
+ input.getActiveProfileName(),
60
+ ),
61
+ getFollowerSocketPath: () =>
62
+ getTelegramBusFollowerSocketPath(
63
+ instanceId,
64
+ undefined,
65
+ undefined,
66
+ input.getActiveProfileName(),
67
+ ),
68
+ };
69
+ }
70
+
37
71
  export function createTelegramBusAuthSecret(): string {
38
72
  return randomBytes(32).toString("base64url");
39
73
  }
@@ -41,16 +75,23 @@ export function createTelegramBusAuthSecret(): string {
41
75
  export function getTelegramBusSocketPath(
42
76
  agentDir = resolveAgentDir(),
43
77
  platform = getPlatform(),
78
+ profileName?: string,
44
79
  ): string {
45
- return getTelegramBusLeaderEndpoint({ agentDir, platform });
80
+ return getTelegramBusLeaderEndpoint({ agentDir, platform, profileName });
46
81
  }
47
82
 
48
83
  export function getTelegramBusFollowerSocketPath(
49
84
  instanceId: string,
50
85
  agentDir = resolveAgentDir(),
51
86
  platform = getPlatform(),
87
+ profileName?: string,
52
88
  ): string {
53
- return getTelegramBusFollowerEndpoint({ agentDir, platform, instanceId });
89
+ return getTelegramBusFollowerEndpoint({
90
+ agentDir,
91
+ platform,
92
+ instanceId,
93
+ profileName,
94
+ });
54
95
  }
55
96
 
56
97
  export interface TelegramBusInstanceRegistration {
@@ -344,8 +385,16 @@ export interface TelegramBusLocalServer {
344
385
  stop: () => Promise<void>;
345
386
  }
346
387
 
388
+ export type TelegramBusSocketPathSource = string | (() => string);
389
+
390
+ export function resolveTelegramBusSocketPath(
391
+ source: TelegramBusSocketPathSource,
392
+ ): string {
393
+ return typeof source === "function" ? source() : source;
394
+ }
395
+
347
396
  export interface TelegramBusLocalServerDeps {
348
- socketPath: string;
397
+ socketPath: TelegramBusSocketPathSource;
349
398
  handleEnvelope: (
350
399
  envelope: TelegramBusEnvelope,
351
400
  ) =>
@@ -362,7 +411,7 @@ export interface TelegramBusLocalClientOptions {
362
411
  }
363
412
 
364
413
  export interface TelegramBusForeignOwnedForwarderDeps {
365
- socketPath: string;
414
+ socketPath: TelegramBusSocketPathSource;
366
415
  createRequestId: () => string;
367
416
  getNowMs?: () => number;
368
417
  timeoutMs?: number;
@@ -401,12 +450,13 @@ export function createTelegramBusForeignOwnedUpdateForwarder<
401
450
  const getNowMs = deps.getNowMs ?? Date.now;
402
451
  const send = async (envelope: TelegramBusEnvelope): Promise<boolean> => {
403
452
  if (deps.getAuthSecret) envelope.auth = deps.getAuthSecret();
453
+ const socketPath = resolveTelegramBusSocketPath(deps.socketPath);
404
454
  const response = await sendTelegramBusLocalEnvelope({
405
- socketPath: deps.socketPath,
455
+ socketPath,
406
456
  envelope,
407
457
  timeoutMs: deps.timeoutMs,
408
458
  retry: getTelegramBusTransportRetryPolicy({
409
- endpoint: deps.socketPath,
459
+ endpoint: socketPath,
410
460
  operation: "operation",
411
461
  }),
412
462
  });
@@ -558,6 +608,7 @@ export function createTelegramBusLocalServer(
558
608
  deps: TelegramBusLocalServerDeps,
559
609
  ): TelegramBusLocalServer {
560
610
  let server: Server | undefined;
611
+ let activeSocketPath: string | undefined;
561
612
  const sockets = new Set<Socket>();
562
613
  const closeSocket = (socket: Socket) => {
563
614
  sockets.delete(socket);
@@ -566,16 +617,18 @@ export function createTelegramBusLocalServer(
566
617
  return {
567
618
  start: async () => {
568
619
  if (server) return;
569
- const usesWindowsPipe = isTelegramBusPipePath(deps.socketPath);
620
+ const socketPath = resolveTelegramBusSocketPath(deps.socketPath);
621
+ activeSocketPath = socketPath;
622
+ const usesWindowsPipe = isTelegramBusPipePath(socketPath);
570
623
  deps.recordTransportEvent?.(
571
624
  "server-start",
572
- getTelegramBusEndpointDiagnostics(deps.socketPath),
625
+ getTelegramBusEndpointDiagnostics(socketPath),
573
626
  );
574
627
  if (!usesWindowsPipe) {
575
- const socketDir = dirname(deps.socketPath);
628
+ const socketDir = dirname(socketPath);
576
629
  mkdirSync(socketDir, { recursive: true, mode: 0o700 });
577
630
  chmodSync(socketDir, 0o700);
578
- if (existsSync(deps.socketPath)) unlinkSync(deps.socketPath);
631
+ if (existsSync(socketPath)) unlinkSync(socketPath);
579
632
  }
580
633
  server = createServer((socket) => {
581
634
  sockets.add(socket);
@@ -591,14 +644,14 @@ export function createTelegramBusLocalServer(
591
644
  socket,
592
645
  deps.handleEnvelope,
593
646
  deps.recordTransportEvent,
594
- deps.socketPath,
647
+ socketPath,
595
648
  );
596
649
  }
597
650
  });
598
651
  socket.on("close", () => sockets.delete(socket));
599
652
  socket.on("error", (error) => {
600
653
  deps.recordTransportEvent?.("server-socket-error", {
601
- ...getTelegramBusEndpointDiagnostics(deps.socketPath),
654
+ ...getTelegramBusEndpointDiagnostics(socketPath),
602
655
  ...classifyTelegramBusTransportError(error),
603
656
  });
604
657
  closeSocket(socket);
@@ -607,24 +660,28 @@ export function createTelegramBusLocalServer(
607
660
  try {
608
661
  await new Promise<void>((resolve, reject) => {
609
662
  server?.once("error", reject);
610
- server?.listen(deps.socketPath, resolve);
663
+ server?.listen(socketPath, resolve);
611
664
  });
612
665
  deps.recordTransportEvent?.(
613
666
  "server-started",
614
- getTelegramBusEndpointDiagnostics(deps.socketPath),
667
+ getTelegramBusEndpointDiagnostics(socketPath),
615
668
  );
616
669
  } catch (error) {
670
+ server = undefined;
671
+ activeSocketPath = undefined;
617
672
  deps.recordTransportEvent?.("server-start-failed", {
618
- ...getTelegramBusEndpointDiagnostics(deps.socketPath),
673
+ ...getTelegramBusEndpointDiagnostics(socketPath),
619
674
  ...classifyTelegramBusTransportError(error),
620
675
  });
621
676
  throw error;
622
677
  }
623
- if (!usesWindowsPipe) chmodSync(deps.socketPath, 0o600);
678
+ if (!usesWindowsPipe) chmodSync(socketPath, 0o600);
624
679
  },
625
680
  stop: async () => {
626
681
  const activeServer = server;
682
+ const socketPath = activeSocketPath;
627
683
  server = undefined;
684
+ activeSocketPath = undefined;
628
685
  for (const socket of sockets) closeSocket(socket);
629
686
  if (activeServer) {
630
687
  await new Promise<void>((resolve) =>
@@ -632,15 +689,18 @@ export function createTelegramBusLocalServer(
632
689
  );
633
690
  }
634
691
  if (
635
- !isTelegramBusPipePath(deps.socketPath) &&
636
- existsSync(deps.socketPath)
692
+ socketPath &&
693
+ !isTelegramBusPipePath(socketPath) &&
694
+ existsSync(socketPath)
637
695
  ) {
638
- unlinkSync(deps.socketPath);
696
+ unlinkSync(socketPath);
697
+ }
698
+ if (socketPath) {
699
+ deps.recordTransportEvent?.(
700
+ "server-stopped",
701
+ getTelegramBusEndpointDiagnostics(socketPath),
702
+ );
639
703
  }
640
- deps.recordTransportEvent?.(
641
- "server-stopped",
642
- getTelegramBusEndpointDiagnostics(deps.socketPath),
643
- );
644
704
  },
645
705
  };
646
706
  }
package/lib/paths.ts CHANGED
@@ -71,6 +71,17 @@ export function resolveTelegramProfileTempFilePath(
71
71
  );
72
72
  }
73
73
 
74
+ export function getTelegramDiagnosticsDisplayPaths(profileName?: string): {
75
+ state: string;
76
+ logs: string;
77
+ } {
78
+ const suffix = getTelegramProfilePathSuffix(profileName);
79
+ return {
80
+ state: `~/.pi/agent/tmp/telegram/state${suffix}.json`,
81
+ logs: `~/.pi/agent/tmp/telegram/logs${suffix}.jsonl`,
82
+ };
83
+ }
84
+
74
85
  /** Runtime event log (<agentDir>/tmp/telegram/logs.jsonl). */
75
86
  export function resolveTelegramRuntimeLogPath(): string {
76
87
  return resolveTelegramProfileTempFilePath("logs", "jsonl");
package/lib/prompts.ts CHANGED
@@ -6,6 +6,7 @@
6
6
 
7
7
  import { Type } from "@sinclair/typebox";
8
8
 
9
+ import { getTelegramDiagnosticsDisplayPaths } from "./paths.ts";
9
10
  import type { BeforeAgentStartEvent, ExtensionAPI } from "./pi.ts";
10
11
  import { TELEGRAM_PREFIX } from "./turns.ts";
11
12
 
@@ -17,7 +18,9 @@ const TELEGRAM_TURN_SYSTEM_PROMPT_SUFFIX = `
17
18
 
18
19
  Telegram turn note: If context was compacted or you need the pi-telegram bridge contract, call tool \`telegram_help\`; hidden comments are valid only for explicit \`telegram_voice\` or \`telegram_button\` actions with payload.`;
19
20
 
20
- const TELEGRAM_HELP_TEXT = `--- TELEGRAM BRIDGE HELP ---
21
+ function buildTelegramHelpText(profileName?: string): string {
22
+ const diagnosticsPaths = getTelegramDiagnosticsDisplayPaths(profileName);
23
+ return `--- TELEGRAM BRIDGE HELP ---
21
24
 
22
25
  How to understand Telegram turns:
23
26
  - \`[telegram|thread:name|from:user|guest:group]\` marks Telegram origin and attributes.
@@ -60,15 +63,19 @@ Configurable handlers:
60
63
  - If command-template config is not enough, build a companion extension through the public pi-telegram APIs; do not import package-private \`lib/*\` paths.
61
64
 
62
65
  Debugging pi-telegram:
63
- - Inspect \`~/.pi/agent/tmp/telegram/state.json\` for runtime state, roster, bindings, slots, reservations, and diagnostics.
64
- - Inspect \`~/.pi/agent/tmp/telegram/logs.jsonl\` for redacted runtime event evidence.
66
+ - Inspect \`${diagnosticsPaths.state}\` for runtime state, roster, bindings, slots, reservations, and diagnostics.
67
+ - Inspect \`${diagnosticsPaths.logs}\` for redacted runtime event evidence.
65
68
  - Use terminal \`telegram-status\` for compact human health; use \`telegram-status --debug\` for the full human-readable diagnostic dump.`;
69
+ }
66
70
 
67
- export function getTelegramHelpText(): string {
68
- return TELEGRAM_HELP_TEXT;
71
+ export function getTelegramHelpText(profileName?: string): string {
72
+ return buildTelegramHelpText(profileName);
69
73
  }
70
74
 
71
- export function registerTelegramHelpTool(pi: ExtensionAPI): void {
75
+ export function registerTelegramHelpTool(
76
+ pi: ExtensionAPI,
77
+ options: { getActiveProfileName?: () => string | undefined } = {},
78
+ ): void {
72
79
  pi.registerTool({
73
80
  name: "telegram_help",
74
81
  label: "Telegram Help",
@@ -77,7 +84,12 @@ export function registerTelegramHelpTool(pi: ExtensionAPI): void {
77
84
  parameters: Type.Object({}),
78
85
  async execute() {
79
86
  return {
80
- content: [{ type: "text", text: getTelegramHelpText() }],
87
+ content: [
88
+ {
89
+ type: "text",
90
+ text: getTelegramHelpText(options.getActiveProfileName?.()),
91
+ },
92
+ ],
81
93
  details: {},
82
94
  };
83
95
  },
package/lib/setup.ts CHANGED
@@ -27,6 +27,11 @@ export interface TelegramPollingStartResult {
27
27
  message?: string;
28
28
  }
29
29
 
30
+ export type TelegramSetupCompletion =
31
+ | { status: "success"; config: TelegramSetupConfig }
32
+ | { status: "cancelled" | "unavailable" | "busy" | "validation-failed" }
33
+ | { status: "polling-failed"; config: TelegramSetupConfig };
34
+
30
35
  export interface TelegramSetupDeps {
31
36
  hasUI: boolean;
32
37
  env: NodeJS.ProcessEnv;
@@ -120,8 +125,8 @@ export function getTelegramBotTokenPromptSpec(
120
125
 
121
126
  export async function runTelegramSetup(
122
127
  deps: TelegramSetupDeps,
123
- ): Promise<TelegramSetupConfig | undefined> {
124
- if (!deps.hasUI) return undefined;
128
+ ): Promise<TelegramSetupCompletion> {
129
+ if (!deps.hasUI) return { status: "unavailable" };
125
130
  const tokenPrompt = getTelegramBotTokenPromptSpec(
126
131
  deps.env,
127
132
  deps.config.botToken,
@@ -130,7 +135,7 @@ export async function runTelegramSetup(
130
135
  tokenPrompt.method === "editor"
131
136
  ? await deps.promptEditor("Telegram bot token", tokenPrompt.value)
132
137
  : await deps.promptInput("Telegram bot token", tokenPrompt.value);
133
- if (!token) return undefined;
138
+ if (!token) return { status: "cancelled" };
134
139
  const nextConfig: TelegramSetupConfig = {
135
140
  ...deps.config,
136
141
  botToken: token.trim(),
@@ -141,11 +146,11 @@ export async function runTelegramSetup(
141
146
  } catch (error) {
142
147
  const message = error instanceof Error ? error.message : String(error);
143
148
  deps.notify(`Telegram API check failed: ${message}`, "error");
144
- return undefined;
149
+ return { status: "validation-failed" };
145
150
  }
146
151
  if (!data.ok || !data.result) {
147
152
  deps.notify(data.description || "Invalid Telegram bot token", "error");
148
- return undefined;
153
+ return { status: "validation-failed" };
149
154
  }
150
155
  nextConfig.botId = data.result.id;
151
156
  nextConfig.botUsername = data.result.username;
@@ -158,21 +163,33 @@ export async function runTelegramSetup(
158
163
  "Send /start to your bot in Telegram to pair this extension with your account.",
159
164
  "info",
160
165
  );
161
- const startResult = await deps.startPolling();
166
+ let startResult: unknown;
167
+ try {
168
+ startResult = await deps.startPolling();
169
+ } catch (error) {
170
+ const message = error instanceof Error ? error.message : String(error);
171
+ deps.notify(`Telegram polling failed: ${message}`, "error");
172
+ deps.updateStatus();
173
+ return { status: "polling-failed", config: nextConfig };
174
+ }
162
175
  if (isTelegramPollingStartResult(startResult) && startResult.message) {
163
176
  deps.notify(startResult.message, startResult.ok ? "info" : "error");
164
177
  }
165
178
  deps.updateStatus();
166
- return nextConfig;
179
+ if (isTelegramPollingStartResult(startResult) && !startResult.ok) {
180
+ return { status: "polling-failed", config: nextConfig };
181
+ }
182
+ return { status: "success", config: nextConfig };
167
183
  }
168
184
 
169
185
  export function createTelegramSetupPromptRuntime<
170
186
  TContext extends TelegramSetupPromptContext,
171
187
  >(deps: TelegramSetupPromptRuntimeDeps<TContext>) {
172
- return async (ctx: TContext): Promise<void> => {
173
- if (!ctx.hasUI || !deps.setupGuard.start()) return;
188
+ return async (ctx: TContext): Promise<TelegramSetupCompletion> => {
189
+ if (!ctx.hasUI) return { status: "unavailable" };
190
+ if (!deps.setupGuard.start()) return { status: "busy" };
174
191
  try {
175
- await runTelegramSetup({
192
+ return await runTelegramSetup({
176
193
  hasUI: ctx.hasUI,
177
194
  env: deps.env ?? process.env,
178
195
  config: deps.getConfig(),
package/lib/status.ts CHANGED
@@ -1065,6 +1065,13 @@ function buildTelegramBridgeCompactStatusLines(
1065
1065
  : state.activeSourceMessageIds?.length
1066
1066
  ? "active"
1067
1067
  : "idle";
1068
+ const profileSuffix = state.activeProfileName
1069
+ ? `.${state.activeProfileName.replace(/[^a-zA-Z0-9._-]+/g, "_")}`
1070
+ : "";
1071
+ const diagnosticsPaths = {
1072
+ state: `~/.pi/agent/tmp/telegram/state${profileSuffix}.json`,
1073
+ logs: `~/.pi/agent/tmp/telegram/logs${profileSuffix}.jsonl`,
1074
+ };
1068
1075
  return [
1069
1076
  "connection:",
1070
1077
  `- bot: ${formatTelegramBridgeBotStatus(state)}`,
@@ -1094,8 +1101,8 @@ function buildTelegramBridgeCompactStatusLines(
1094
1101
  ...buildTelegramThreadReconciliationLines(state),
1095
1102
  "",
1096
1103
  "diagnostics:",
1097
- "- state: ~/.pi/agent/tmp/telegram/state.json",
1098
- "- logs: ~/.pi/agent/tmp/telegram/logs.jsonl",
1104
+ `- state: ${diagnosticsPaths.state}`,
1105
+ `- logs: ${diagnosticsPaths.logs}`,
1099
1106
  "- full dump: /telegram-status --debug",
1100
1107
  ];
1101
1108
  }
package/lib/threads.ts CHANGED
@@ -133,30 +133,32 @@ function getNextMonotonicSlot(
133
133
  nowMs: number,
134
134
  lastSlot?: string,
135
135
  ): string | undefined {
136
- let maxCode = "A".charCodeAt(0) - 1;
137
- for (const record of records.values()) {
138
- if (!record.slot || !isCurrentThreadRecord(record)) continue;
139
- maxCode = Math.max(maxCode, record.slot.charCodeAt(0));
140
- }
141
- for (const reservation of reservations) {
142
- if (
143
- reservation.expiresAtMs !== undefined &&
144
- reservation.expiresAtMs <= nowMs
145
- )
146
- continue;
147
- if (!reservation.slot) continue;
148
- maxCode = Math.max(maxCode, reservation.slot.charCodeAt(0));
149
- }
150
- for (const provision of pendingProvisions) {
151
- if (provision.expiresAtMs !== undefined && provision.expiresAtMs <= nowMs)
152
- continue;
153
- if (!provision.slot) continue;
154
- maxCode = Math.max(maxCode, provision.slot.charCodeAt(0));
155
- }
136
+ let cursorCode: number | undefined;
156
137
  if (lastSlot && /^[A-Z]$/.test(lastSlot)) {
157
- maxCode = Math.max(maxCode, lastSlot.charCodeAt(0));
138
+ cursorCode = lastSlot.charCodeAt(0);
139
+ } else {
140
+ cursorCode = "A".charCodeAt(0) - 1;
141
+ for (const record of records.values()) {
142
+ if (!record.slot || !isCurrentThreadRecord(record)) continue;
143
+ cursorCode = Math.max(cursorCode, record.slot.charCodeAt(0));
144
+ }
145
+ for (const reservation of reservations) {
146
+ if (
147
+ reservation.expiresAtMs !== undefined &&
148
+ reservation.expiresAtMs <= nowMs
149
+ )
150
+ continue;
151
+ if (!reservation.slot) continue;
152
+ cursorCode = Math.max(cursorCode, reservation.slot.charCodeAt(0));
153
+ }
154
+ for (const provision of pendingProvisions) {
155
+ if (provision.expiresAtMs !== undefined && provision.expiresAtMs <= nowMs)
156
+ continue;
157
+ if (!provision.slot) continue;
158
+ cursorCode = Math.max(cursorCode, provision.slot.charCodeAt(0));
159
+ }
158
160
  }
159
- let code = maxCode + 1;
161
+ let code = cursorCode + 1;
160
162
  if (code > "Z".charCodeAt(0)) code = "A".charCodeAt(0);
161
163
  for (let attempt = 0; attempt < 26; attempt++) {
162
164
  const candidate = String.fromCharCode(code);
@@ -228,6 +230,39 @@ export interface TelegramTopicTargetStore {
228
230
  ) => TelegramTopicTargetRecord | undefined;
229
231
  }
230
232
 
233
+ export function reconcileTelegramFreshAllocationCursor(
234
+ store: Pick<
235
+ TelegramTopicTargetStore,
236
+ "getBotState" | "list" | "setBotState"
237
+ >,
238
+ nowMs = Date.now(),
239
+ ): boolean {
240
+ const currentCursor = store.getBotState().lastSlot;
241
+ const slottedRecords = store
242
+ .list()
243
+ .filter((record) => !!record.slot && /^[A-Z]$/.test(record.slot));
244
+ if (slottedRecords.some((record) => record.slot === currentCursor)) {
245
+ return false;
246
+ }
247
+ const latestLiveRecord = slottedRecords.reduce<
248
+ TelegramTopicTargetRecord | undefined
249
+ >((latest, record) => {
250
+ if (!latest) return record;
251
+ if (record.createdAtMs !== latest.createdAtMs) {
252
+ return record.createdAtMs > latest.createdAtMs ? record : latest;
253
+ }
254
+ return record.updatedAtMs > latest.updatedAtMs ? record : latest;
255
+ }, undefined);
256
+ const nextCursor = latestLiveRecord?.slot;
257
+ if (nextCursor === currentCursor) return false;
258
+ store.setBotState({
259
+ lastSlot: nextCursor,
260
+ updatedAtMs: nowMs,
261
+ lastReconcileAction: "live-cursor-realignment",
262
+ });
263
+ return true;
264
+ }
265
+
231
266
  export interface TelegramTopicTargetStoreOptions {
232
267
  path: string | (() => string);
233
268
  getNowMs?: () => number;
@@ -881,9 +916,6 @@ export function createTelegramTopicTargetStore(
881
916
 
882
917
  const rememberSlot = (slot: string | undefined, nowMs = getNowMs()) => {
883
918
  if (!slot || !/^[A-Z]$/.test(slot)) return;
884
- const currentCode =
885
- botState.lastSlot?.charCodeAt(0) ?? "A".charCodeAt(0) - 1;
886
- if (slot.charCodeAt(0) < currentCode) return;
887
919
  botState = { ...botState, lastSlot: slot, updatedAtMs: nowMs };
888
920
  };
889
921
  const rememberIdentity = (record: TelegramTopicTargetRecord) => {
@@ -1151,6 +1183,7 @@ export function createTelegramTopicTargetStore(
1151
1183
  upsert(record) {
1152
1184
  const next = cloneRecord(record);
1153
1185
  const nextOwnerKey = getRecordOwnerKey(next);
1186
+ const previousRecord = records.get(nextOwnerKey);
1154
1187
  if (isCurrentThreadRecord(next)) {
1155
1188
  for (const existing of Array.from(records.values())) {
1156
1189
  const existingOwnerKey = getRecordOwnerKey(existing);
@@ -1180,7 +1213,12 @@ export function createTelegramTopicTargetStore(
1180
1213
  return cloneRecord(next);
1181
1214
  }
1182
1215
  records.set(nextOwnerKey, next);
1183
- rememberSlot(next.slot, next.updatedAtMs);
1216
+ if (
1217
+ !previousRecord ||
1218
+ !targetMatches(previousRecord.target, next.target)
1219
+ ) {
1220
+ rememberSlot(next.slot, next.updatedAtMs);
1221
+ }
1184
1222
  rememberIdentity(next);
1185
1223
  loaded = true;
1186
1224
  dirty = true;
package/lib/turns.ts CHANGED
@@ -8,8 +8,6 @@ import { readFile } from "node:fs/promises";
8
8
  import { basename, dirname, join } from "node:path";
9
9
 
10
10
  import {
11
- appendTelegramForwardContext,
12
- appendTelegramReplyContext,
13
11
  buildTelegramReplyContextBlock,
14
12
  collectTelegramMessageIds,
15
13
  downloadTelegramMessageFiles,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.20.0",
3
+ "version": "0.20.2",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"