@llblab/pi-telegram 0.12.0 → 0.13.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/preview.ts CHANGED
@@ -9,7 +9,7 @@ import type {
9
9
  TelegramReplyParameters,
10
10
  TelegramSendMessageBody,
11
11
  TelegramSentMessage,
12
- } from "./api.ts";
12
+ } from "./telegram-api.ts";
13
13
  import {
14
14
  buildTelegramPreviewSnapshot,
15
15
  MAX_MESSAGE_LENGTH,
@@ -345,6 +345,7 @@ export function createTelegramPreviewControllerRuntime<
345
345
  maxDraftId: deps.maxDraftId,
346
346
  setTimer: deps.setTimer,
347
347
  clearTimer: deps.clearTimer,
348
+ recordRuntimeEvent: deps.recordRuntimeEvent,
348
349
  });
349
350
  }
350
351
 
package/lib/queue.ts CHANGED
@@ -44,6 +44,9 @@ export interface TelegramQueueLaneContract {
44
44
 
45
45
  export const TELEGRAM_QUEUE_LANE_CONTRACTS: readonly TelegramQueueLaneContract[] =
46
46
  [
47
+ // Control lane intentionally accepts both direct controls and resume prompts.
48
+ // Model-switch continuations need prompt semantics but must run before queued user work.
49
+ // Do not admit ordinary user prompts here without an explicit control-flow reason.
47
50
  {
48
51
  lane: "control",
49
52
  admissionMode: "control-queue",
package/lib/rendering.ts CHANGED
@@ -843,7 +843,7 @@ function renderDelimitedInlineStyle(
843
843
  ): string {
844
844
  const escapedDelimiter = delimiter.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
845
845
  const pattern = new RegExp(
846
- `(^|[^\\p{L}\\p{N}\\\\])(${escapedDelimiter})(?=\\S)(.+?)(?<=\\S)\\2(?=[^\\p{L}\\p{N}]|$)`,
846
+ `(^|[^\\p{L}\\p{N}\\\\])(${escapedDelimiter})(?=\\S)([\\s\\S]+?)(?<=\\S)\\2(?=[^\\p{L}\\p{N}]|$)`,
847
847
  "gu",
848
848
  );
849
849
  return text.replace(
@@ -1004,9 +1004,28 @@ function renderMarkdownTextPiece(piece: string): string {
1004
1004
  return renderInlineMarkdown(piece);
1005
1005
  }
1006
1006
 
1007
+ function isPlainInlineMarkdownLine(line: string): boolean {
1008
+ if (line.trim().length === 0) return false;
1009
+ return (
1010
+ matchMarkdownHeadingLine(line) === null &&
1011
+ !/^(\s*)([-*+]|\d+\.)\s+\[([ xX])\]\s+(.+)$/.test(line) &&
1012
+ !/^(\s*)[-*+]\s+(.+)$/.test(line) &&
1013
+ !/^(\s*)(\d+)\.\s+(.+)$/.test(line) &&
1014
+ !/^>\s?(.+)$/.test(line) &&
1015
+ !/^([-*_]\s*){3,}$/.test(line.trim())
1016
+ );
1017
+ }
1018
+
1007
1019
  function renderMarkdownTextLines(block: string): string[] {
1008
1020
  const rendered: string[] = [];
1009
1021
  const lines = block.split("\n");
1022
+ const nonBlankLines = lines.filter((line) => line.trim().length > 0);
1023
+ if (
1024
+ nonBlankLines.length > 1 &&
1025
+ nonBlankLines.every(isPlainInlineMarkdownLine)
1026
+ ) {
1027
+ return renderInlineMarkdown(nonBlankLines.join("\n")).split("\n");
1028
+ }
1010
1029
  for (const line of lines) {
1011
1030
  if (line.trim().length === 0) continue;
1012
1031
  for (const piece of splitPlainMarkdownLine(line)) {
package/lib/replies.ts CHANGED
@@ -4,7 +4,10 @@
4
4
  * Owns rendered-message delivery, reply transport wiring, and plain or markdown final replies
5
5
  */
6
6
 
7
- import type { TelegramReplyParameters, TelegramSentMessage } from "./api.ts";
7
+ import type {
8
+ TelegramReplyParameters,
9
+ TelegramSentMessage,
10
+ } from "./telegram-api.ts";
8
11
  import {
9
12
  renderTelegramMessage,
10
13
  type TelegramRenderedChunk,
package/lib/routing.ts CHANGED
@@ -351,8 +351,8 @@ export function createTelegramInboundRouteRuntime<
351
351
  const turn = await promptTurnBuilder([continueMessage], [], ctx);
352
352
  const continueTurn = {
353
353
  ...turn,
354
- queueLane: "priority" as const,
355
- laneOrder: Number.MIN_SAFE_INTEGER + turn.queueOrder,
354
+ queueLane: "control" as const,
355
+ laneOrder: deps.bridgeRuntime.queue.allocateControlOrder(),
356
356
  statusSummary: "continue",
357
357
  };
358
358
  deps.queueMutationRuntime.append(continueTurn, ctx);
package/lib/status.ts CHANGED
@@ -289,12 +289,25 @@ function formatTelegramRuntimeEvent(event: TelegramRuntimeEvent): string {
289
289
  return `${new Date(event.at).toISOString()} ${formatTelegramRuntimeEventSummary(event)}`;
290
290
  }
291
291
 
292
+ function buildTelegramRuntimeEventSummary(events: TelegramRuntimeEvent[]): string {
293
+ const counts = new Map<string, number>();
294
+ for (const event of events) {
295
+ const category = formatTelegramRuntimeEventCategory(event);
296
+ counts.set(category, (counts.get(category) ?? 0) + 1);
297
+ }
298
+ return Array.from(counts.entries())
299
+ .sort(([left], [right]) => left.localeCompare(right))
300
+ .map(([category, count]) => `${category}=${count}`)
301
+ .join(", ");
302
+ }
303
+
292
304
  export function buildTelegramRuntimeEventLines(
293
305
  events: TelegramRuntimeEvent[],
294
306
  ): string[] {
295
307
  if (events.length === 0) return ["recent runtime events: none"];
296
308
  return [
297
309
  "recent runtime events:",
310
+ `- summary: ${buildTelegramRuntimeEventSummary(events)}`,
298
311
  ...events
299
312
  .slice()
300
313
  .reverse()
@@ -416,13 +416,13 @@ async function writeTelegramDownloadResponse(
416
416
  if (!response.body) {
417
417
  const buffer = Buffer.from(await response.arrayBuffer());
418
418
  assertTelegramFileSizeWithinLimit(buffer.byteLength, maxFileSizeBytes);
419
- await writeFile(targetPath, buffer);
419
+ await writeFile(targetPath, buffer, { mode: 0o600 });
420
420
  return;
421
421
  }
422
422
  await pipeline(
423
423
  Readable.from(response.body, { objectMode: false }),
424
424
  createTelegramDownloadLimitTransform(maxFileSizeBytes),
425
- createWriteStream(targetPath),
425
+ createWriteStream(targetPath, { mode: 0o600 }),
426
426
  );
427
427
  }
428
428
 
@@ -536,7 +536,7 @@ export async function prepareTelegramTempDir(
536
536
  tempDir: string,
537
537
  maxAgeMs: number,
538
538
  ): Promise<number> {
539
- await mkdir(tempDir, { recursive: true });
539
+ await mkdir(tempDir, { recursive: true, mode: 0o700 });
540
540
  return cleanupTelegramTempFiles(tempDir, maxAgeMs);
541
541
  }
542
542
 
@@ -630,7 +630,7 @@ export async function downloadTelegramFile(
630
630
  { signal: options?.signal },
631
631
  );
632
632
  assertTelegramFileSizeWithinLimit(file.file_size, options?.maxFileSizeBytes);
633
- await mkdir(tempDir, { recursive: true });
633
+ await mkdir(tempDir, { recursive: true, mode: 0o700 });
634
634
  const targetPath = join(
635
635
  tempDir,
636
636
  `${randomUUID()}-${sanitizeFileName(suggestedName)}`,
@@ -6,7 +6,7 @@
6
6
 
7
7
  const TELEGRAM_TEXT_GROUP_DEBOUNCE_MS = 1000;
8
8
  const TELEGRAM_TEXT_GROUP_MIN_SPLIT_LENGTH = 3600;
9
- const TELEGRAM_TEXT_GROUP_MAX_MESSAGE_ID_GAP = 10;
9
+ const TELEGRAM_TEXT_GROUP_MAX_MESSAGE_ID_GAP = 12;
10
10
 
11
11
  export interface TelegramTextGroupMessage {
12
12
  message_id: number;
@@ -92,7 +92,8 @@ function canAppendTelegramTextGroupMessage<
92
92
  return (
93
93
  !!previous &&
94
94
  message.message_id > previous.message_id &&
95
- message.message_id <= previous.message_id + TELEGRAM_TEXT_GROUP_MAX_MESSAGE_ID_GAP &&
95
+ message.message_id <=
96
+ previous.message_id + TELEGRAM_TEXT_GROUP_MAX_MESSAGE_ID_GAP &&
96
97
  text.length > 0 &&
97
98
  !isTelegramTextGroupCommand(text)
98
99
  );
package/lib/voice.ts CHANGED
@@ -23,6 +23,21 @@ const VOICE_SYNTHESIS_PROVIDER_REGISTRY_KEY =
23
23
  const VOICE_TRANSCRIPTION_PROVIDER_REGISTRY_KEY =
24
24
  "__piTelegramVoiceTranscriptionProviders__";
25
25
 
26
+ let nextGeneratedVoiceSynthesisProviderId = 0;
27
+ let nextGeneratedVoiceTranscriptionProviderId = 0;
28
+
29
+ function getNextAvailableProviderId<T>(
30
+ registry: Map<string, T>,
31
+ prefix: string,
32
+ nextId: () => number,
33
+ ): string {
34
+ let id: string;
35
+ do {
36
+ id = `${prefix}-${nextId()}`;
37
+ } while (registry.has(id));
38
+ return id;
39
+ }
40
+
26
41
  export type TelegramVoiceReplyMode = "mirror" | "always" | "manual";
27
42
 
28
43
  export type TelegramVoiceSynthesisProviderResult =
@@ -106,7 +121,13 @@ export function registerTelegramVoiceSynthesisProvider(
106
121
  options?: { id?: string },
107
122
  ): () => void {
108
123
  const registry = getOrCreateVoiceSynthesisProviderRegistry();
109
- const id = options?.id ?? `voice-synthesis-provider-${registry.size}`;
124
+ const id =
125
+ options?.id ??
126
+ getNextAvailableProviderId(
127
+ registry,
128
+ "voice-synthesis-provider",
129
+ () => nextGeneratedVoiceSynthesisProviderId++,
130
+ );
110
131
  const normalized =
111
132
  typeof provider === "function"
112
133
  ? (Object.assign(
@@ -123,7 +144,7 @@ export function registerTelegramVoiceSynthesisProvider(
123
144
  : provider;
124
145
  registry.set(id, normalized);
125
146
  return () => {
126
- registry.delete(id);
147
+ if (registry.get(id) === normalized) registry.delete(id);
127
148
  };
128
149
  }
129
150
 
@@ -168,10 +189,16 @@ export function registerTelegramVoiceTranscriptionProvider(
168
189
  options?: { id?: string },
169
190
  ): () => void {
170
191
  const registry = getOrCreateVoiceTranscriptionProviderRegistry();
171
- const id = options?.id ?? `voice-transcription-provider-${registry.size}`;
192
+ const id =
193
+ options?.id ??
194
+ getNextAvailableProviderId(
195
+ registry,
196
+ "voice-transcription-provider",
197
+ () => nextGeneratedVoiceTranscriptionProviderId++,
198
+ );
172
199
  registry.set(id, provider);
173
200
  return () => {
174
- registry.delete(id);
201
+ if (registry.get(id) === provider) registry.delete(id);
175
202
  };
176
203
  }
177
204
 
@@ -303,12 +330,12 @@ export function shouldSuppressPreviewForVoice(
303
330
  return !!(turn?.voiceReplyPreferred || turn?.voiceReplyRequired);
304
331
  }
305
332
 
306
- // --- Outbound Handler Re-Exports ---
333
+ // --- Outbound Markup Re-Exports ---
307
334
 
308
335
  export {
336
+ normalizeMarkdownAfterVoiceExtraction,
309
337
  planTelegramVoiceReply,
310
- stripTelegramCommentMarkupForPreview,
311
338
  stripTelegramCommentMarkupForDelivery,
339
+ stripTelegramCommentMarkupForPreview,
312
340
  stripTelegramVoiceMarkupForPreview,
313
- normalizeMarkdownAfterVoiceExtraction,
314
- } from "./outbound.ts";
341
+ } from "./outbound-markup.ts";
package/package.json CHANGED
@@ -1,21 +1,11 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.12.0",
3
+ "version": "0.13.1",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
8
8
  "description": "Telegram runtime adapter for π",
9
- "type": "module",
10
- "exports": {
11
- ".": "./index.ts",
12
- "./inbound": "./api/inbound.ts",
13
- "./outbound": "./api/outbound.ts",
14
- "./updates": "./api/updates.ts",
15
- "./sections": "./api/sections.ts",
16
- "./voice": "./api/voice.ts",
17
- "./keyboard": "./api/keyboard.ts"
18
- },
19
9
  "keywords": [
20
10
  "pi-package",
21
11
  "pi",
@@ -23,6 +13,7 @@
23
13
  "bot",
24
14
  "extension"
25
15
  ],
16
+ "type": "module",
26
17
  "license": "MIT",
27
18
  "repository": {
28
19
  "type": "git",
@@ -33,7 +24,7 @@
33
24
  "url": "https://github.com/llblab/pi-telegram/issues"
34
25
  },
35
26
  "engines": {
36
- "node": ">=22.0.0"
27
+ "node": ">=22.19.0"
37
28
  },
38
29
  "scripts": {
39
30
  "test": "node --experimental-strip-types --test tests/*.test.ts",
@@ -53,6 +44,15 @@
53
44
  "docs/",
54
45
  "screenshot.png"
55
46
  ],
47
+ "exports": {
48
+ ".": "./index.ts",
49
+ "./inbound": "./api/inbound.ts",
50
+ "./outbound": "./api/outbound.ts",
51
+ "./updates": "./api/updates.ts",
52
+ "./sections": "./api/sections.ts",
53
+ "./voice": "./api/voice.ts",
54
+ "./keyboard": "./api/keyboard.ts"
55
+ },
56
56
  "pi": {
57
57
  "extensions": [
58
58
  "./index.ts"