@llblab/pi-telegram 0.20.1 → 0.20.3

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/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.1",
3
+ "version": "0.20.3",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"