@llblab/pi-telegram 0.13.1 → 0.13.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/BACKLOG.md CHANGED
@@ -1,28 +1,3 @@
1
1
  # Project Backlog
2
2
 
3
- ## Bound inbound handler output
4
-
5
- `Task`: Bound inbound handler stdout, stderr, and recorded failure text before they enter prompts or runtime status.
6
-
7
- `Why`: Large OCR, PDF, STT, or failing command output can inflate prompt context, memory use, and `/telegram-status`.
8
-
9
- `Exit criteria`:
10
-
11
- - Handler stdout added to `[outputs]` is truncated or externalized behind a bounded artifact reference.
12
- - Handler stderr and stdout included in failure messages are bounded.
13
- - Runtime event messages and details are bounded before storage and rendering.
14
- - Regression tests cover large handler stdout and large failure output.
15
-
16
- ## Recover from invalid config JSON
17
-
18
- `Task`: Make `telegram.json` load failures recoverable without bricking pi-telegram session startup.
19
-
20
- `Why`: A hand-edited or partially written invalid config currently bubbles `JSON.parse` failure through session start, which can block the normal repair path.
21
-
22
- `Exit criteria`:
23
-
24
- - Invalid config JSON is reported through a runtime event or clear status diagnostic.
25
- - Session startup continues with safe empty config defaults.
26
- - The invalid file is preserved or renamed for operator recovery.
27
- - `/telegram-setup` remains usable after an invalid config is detected.
28
- - Regression tests cover invalid config startup behavior.
3
+ No open work.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.13.2: Config Recovery And Inbound Output Bounds Hotfix
6
+
7
+ - `[Config]` Invalid `telegram.json` now recovers on session startup by renaming the broken file to an `.invalid-*` recovery path, loading safe empty defaults, and recording a runtime diagnostic. Impact: a hand-edited or partially written config no longer bricks `/telegram-setup` or session startup.
8
+ - `[Inbound]` Inbound handler, programmatic handler, voice transcription, and built-in text attachment outputs are now bounded before entering Telegram prompt context. Impact: large OCR, PDF, STT, or text-file outputs cannot silently explode prompt size.
9
+ - `[Diagnostics]` Runtime event messages/details and inbound handler failure stdout/stderr are truncated before storage/rendering. Impact: `/telegram-status` remains useful after noisy provider or handler failures without hiding that truncation happened.
10
+
5
11
  ## 0.13.1: Rendering, Typing, And Continue Queue Hotfix
6
12
 
7
13
  - `[Rendering]` Fixed Telegram HTML rendering for Markdown bold/italic spans that cross soft line breaks, so assistant replies like `**first line\nsecond line**` render as bold text instead of showing raw asterisks. Added a regression for the guest-mode-style multiline bold reply shape.
package/index.ts CHANGED
@@ -48,7 +48,15 @@ export default function (pi: Pi.ExtensionAPI) {
48
48
  } = piRuntime;
49
49
  const bridgeRuntime = Runtime.createTelegramBridgeRuntime();
50
50
  const { abort, lifecycle, queue, setup, typing } = bridgeRuntime;
51
- const configStore = Config.createTelegramConfigStore();
51
+ let configStoreForRedaction: Config.TelegramConfigStore | undefined;
52
+ const runtimeEvents = Status.createTelegramRuntimeEventRecorder({
53
+ getBotToken() {
54
+ return configStoreForRedaction?.getBotToken();
55
+ },
56
+ });
57
+ const recordRuntimeEvent = runtimeEvents.record;
58
+ const configStore = Config.createTelegramConfigStore({ recordRuntimeEvent });
59
+ configStoreForRedaction = configStore;
52
60
  Config.bindGlobalTelegramConfigRuntime(configStore);
53
61
  const configControls = Config.createTelegramConfigControls(configStore);
54
62
  const lockRuntime = Locks.createTelegramLockRuntime<Pi.ExtensionContext>();
@@ -68,10 +76,6 @@ export default function (pi: Pi.ExtensionAPI) {
68
76
  const modelMenuRuntime = Menu.createTelegramModelMenuRuntime<ActivePiModel>();
69
77
  const sectionRegistry = Sections.createAndBindTelegramSectionRegistry();
70
78
 
71
- const runtimeEvents = Status.createTelegramRuntimeEventRecorder({
72
- getBotToken: configStore.getBotToken,
73
- });
74
- const recordRuntimeEvent = runtimeEvents.record;
75
79
  const timeInjectionRuntime = TimeInjection.createTimeInjectionRuntime({
76
80
  getConfig: Config.createTelegramTimeConfigGetter(configStore),
77
81
  recordRuntimeEvent,
package/lib/config.ts CHANGED
@@ -84,6 +84,17 @@ export interface TelegramConfigStoreOptions {
84
84
  initialConfig?: TelegramConfig;
85
85
  agentDir?: string;
86
86
  configPath?: string;
87
+ recordRuntimeEvent?: (
88
+ category: string,
89
+ error: unknown,
90
+ details?: Record<string, unknown>,
91
+ ) => void;
92
+ }
93
+
94
+ export interface TelegramInvalidConfigRecovery {
95
+ configPath: string;
96
+ recoveryPath: string;
97
+ error: unknown;
87
98
  }
88
99
 
89
100
  export interface TelegramConfigRuntime {
@@ -125,12 +136,26 @@ export function bindGlobalTelegramConfigRuntime(
125
136
  });
126
137
  }
127
138
 
139
+ function getInvalidTelegramConfigRecoveryPath(configPath: string): string {
140
+ return `${configPath}.invalid-${process.pid}-${Date.now()}`;
141
+ }
142
+
128
143
  export async function readTelegramConfig(
129
144
  configPath: string,
145
+ options: {
146
+ onInvalidConfig?: (recovery: TelegramInvalidConfigRecovery) => void;
147
+ } = {},
130
148
  ): Promise<TelegramConfig> {
131
149
  if (!existsSync(configPath)) return {};
132
150
  const content = await readFile(configPath, "utf8");
133
- return JSON.parse(content) as TelegramConfig;
151
+ try {
152
+ return JSON.parse(content) as TelegramConfig;
153
+ } catch (error) {
154
+ const recoveryPath = getInvalidTelegramConfigRecoveryPath(configPath);
155
+ await rename(configPath, recoveryPath);
156
+ options.onInvalidConfig?.({ configPath, recoveryPath, error });
157
+ return {};
158
+ }
134
159
  }
135
160
 
136
161
  export async function writeTelegramConfig(
@@ -176,7 +201,15 @@ export function createTelegramConfigStore(
176
201
  config.allowedUserId = userId;
177
202
  },
178
203
  load: async () => {
179
- config = await readTelegramConfig(configPath);
204
+ config = await readTelegramConfig(configPath, {
205
+ onInvalidConfig: (recovery) => {
206
+ options.recordRuntimeEvent?.("config", recovery.error, {
207
+ phase: "load",
208
+ configPath: recovery.configPath,
209
+ recoveryPath: recovery.recoveryPath,
210
+ });
211
+ },
212
+ });
180
213
  },
181
214
  persist: async (nextConfig = config) => {
182
215
  await writeTelegramConfig(agentDir, configPath, nextConfig);
package/lib/inbound.ts CHANGED
@@ -19,6 +19,8 @@ import { getTelegramVoiceTranscriptionProviders } from "./voice.ts";
19
19
 
20
20
  const DEFAULT_INBOUND_HANDLER_TIMEOUT_MS = 120_000;
21
21
  const INBOUND_HANDLER_REGISTRY_KEY = "__piTelegramInboundHandlers__";
22
+ const MAX_INBOUND_HANDLER_OUTPUT_LENGTH = 12_000;
23
+ const MAX_INBOUND_HANDLER_FAILURE_STREAM_LENGTH = 4_000;
22
24
 
23
25
  type TelegramInboundCommandTemplateConfig =
24
26
  | string
@@ -187,12 +189,28 @@ export function clearTelegramInboundHandlers(): void {
187
189
  getOrCreateInboundHandlerRegistry().handlers.clear();
188
190
  }
189
191
 
192
+ function truncateTelegramInboundText(text: string, maxLength: number): string {
193
+ if (text.length <= maxLength) return text;
194
+ return `${text.slice(0, maxLength).trimEnd()}… [truncated ${text.length - maxLength} chars]`;
195
+ }
196
+
197
+ function truncateTelegramInboundOutput(text: string): string {
198
+ return truncateTelegramInboundText(text, MAX_INBOUND_HANDLER_OUTPUT_LENGTH);
199
+ }
200
+
201
+ function truncateTelegramInboundFailureStream(text: string): string {
202
+ return truncateTelegramInboundText(
203
+ text.trimEnd(),
204
+ MAX_INBOUND_HANDLER_FAILURE_STREAM_LENGTH,
205
+ );
206
+ }
207
+
190
208
  function normalizeInboundProgrammaticHandlerText(
191
209
  result: TelegramInboundProgrammaticHandlerResult,
192
210
  ): string | undefined {
193
211
  const text = typeof result === "string" ? result : result?.text;
194
212
  const normalized = text?.trim();
195
- return normalized || undefined;
213
+ return normalized ? truncateTelegramInboundOutput(normalized) : undefined;
196
214
  }
197
215
 
198
216
  function normalizeStringList(value: string | string[] | undefined): string[] {
@@ -401,8 +419,10 @@ function formatTelegramInboundHandlerFailure(
401
419
  const parts = [
402
420
  `Inbound handler exited with code ${result.code}${result.killed ? " (killed)" : ""}`,
403
421
  ];
404
- if (result.stderr.trim()) parts.push(`stderr:\n${result.stderr.trimEnd()}`);
405
- if (result.stdout.trim()) parts.push(`stdout:\n${result.stdout.trimEnd()}`);
422
+ if (result.stderr.trim())
423
+ parts.push(`stderr:\n${truncateTelegramInboundFailureStream(result.stderr)}`);
424
+ if (result.stdout.trim())
425
+ parts.push(`stdout:\n${truncateTelegramInboundFailureStream(result.stdout)}`);
406
426
  return parts.join("\n\n");
407
427
  }
408
428
 
@@ -431,7 +451,7 @@ async function executeTelegramInboundHandlerInvocation(
431
451
  });
432
452
  if (result.code !== 0)
433
453
  throw new Error(formatTelegramInboundHandlerFailure(result));
434
- return result.stdout;
454
+ return truncateTelegramInboundOutput(result.stdout);
435
455
  }
436
456
 
437
457
  function getTelegramInboundHandlerCompositionSteps(
@@ -504,7 +524,7 @@ async function executeTelegramTextHandlerInvocation(
504
524
  });
505
525
  if (result.code !== 0)
506
526
  throw new Error(formatTelegramInboundHandlerFailure(result));
507
- return result.stdout;
527
+ return truncateTelegramInboundOutput(result.stdout);
508
528
  }
509
529
 
510
530
  async function executeTelegramTextHandler(
@@ -633,7 +653,7 @@ async function transcribeTelegramVoiceFileWithProviders(
633
653
  try {
634
654
  const result = await provider(file, {});
635
655
  const text = typeof result === "string" ? result : result?.text;
636
- if (text?.trim()) return text.trim();
656
+ if (text?.trim()) return truncateTelegramInboundOutput(text.trim());
637
657
  } catch (error) {
638
658
  options.recordRuntimeEvent?.("voice-transcription-provider", error, {
639
659
  fileName: file.fileName || basename(file.path),
@@ -656,7 +676,7 @@ async function readBuiltInTelegramTextAttachment(
656
676
  return undefined;
657
677
  }
658
678
  const name = file.fileName || basename(file.path);
659
- return `[${name}]\n${normalized}`;
679
+ return truncateTelegramInboundOutput(`[${name}]\n${normalized}`);
660
680
  }
661
681
 
662
682
  async function executeTelegramInboundHandler(
package/lib/status.ts CHANGED
@@ -53,6 +53,8 @@ export interface TelegramStatusContext {
53
53
  export type TelegramRuntimeEventDetailValue = string | number | boolean | null;
54
54
 
55
55
  const MAX_RECENT_TELEGRAM_RUNTIME_EVENTS = 10;
56
+ const MAX_TELEGRAM_RUNTIME_EVENT_MESSAGE_LENGTH = 1000;
57
+ const MAX_TELEGRAM_RUNTIME_EVENT_DETAIL_LENGTH = 1000;
56
58
 
57
59
  export interface TelegramRuntimeEvent {
58
60
  at: number;
@@ -164,12 +166,35 @@ export interface TelegramStatusRuntime<
164
166
  getStatusLines: () => string[];
165
167
  }
166
168
 
169
+ function truncateTelegramRuntimeEventText(text: string, maxLength: number): string {
170
+ if (text.length <= maxLength) return text;
171
+ return `${text.slice(0, maxLength).trimEnd()}… [truncated ${text.length - maxLength} chars]`;
172
+ }
173
+
167
174
  export function redactTelegramRuntimeMessage(
168
175
  message: string,
169
176
  botToken: string | undefined,
170
177
  ): string {
171
- if (!botToken) return message;
172
- return message.split(botToken).join("<redacted-token>");
178
+ const redacted = botToken
179
+ ? message.split(botToken).join("<redacted-token>")
180
+ : message;
181
+ return truncateTelegramRuntimeEventText(
182
+ redacted,
183
+ MAX_TELEGRAM_RUNTIME_EVENT_MESSAGE_LENGTH,
184
+ );
185
+ }
186
+
187
+ function redactTelegramRuntimeDetail(
188
+ message: string,
189
+ botToken: string | undefined,
190
+ ): string {
191
+ const redacted = botToken
192
+ ? message.split(botToken).join("<redacted-token>")
193
+ : message;
194
+ return truncateTelegramRuntimeEventText(
195
+ redacted,
196
+ MAX_TELEGRAM_RUNTIME_EVENT_DETAIL_LENGTH,
197
+ );
173
198
  }
174
199
 
175
200
  function normalizeTelegramRuntimeEventDetails(
@@ -181,7 +206,7 @@ function normalizeTelegramRuntimeEventDetails(
181
206
  for (const [key, value] of Object.entries(details)) {
182
207
  if (value === undefined) continue;
183
208
  if (typeof value === "string") {
184
- normalized[key] = redactTelegramRuntimeMessage(value, botToken);
209
+ normalized[key] = redactTelegramRuntimeDetail(value, botToken);
185
210
  continue;
186
211
  }
187
212
  if (typeof value === "number" || typeof value === "boolean") {
@@ -192,7 +217,7 @@ function normalizeTelegramRuntimeEventDetails(
192
217
  normalized[key] = null;
193
218
  continue;
194
219
  }
195
- normalized[key] = redactTelegramRuntimeMessage(String(value), botToken);
220
+ normalized[key] = redactTelegramRuntimeDetail(String(value), botToken);
196
221
  }
197
222
  return Object.keys(normalized).length > 0 ? normalized : undefined;
198
223
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.13.1",
3
+ "version": "0.13.2",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"