@llblab/pi-kit 0.37.0 → 0.37.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.
Files changed (31) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/README.md +1 -1
  3. package/node_modules/@llblab/pi-telegram/AGENTS.md +1 -1
  4. package/node_modules/@llblab/pi-telegram/BACKLOG.md +8 -10
  5. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +10 -0
  6. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-buttons.js +20 -18
  7. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-markup.d.ts +6 -6
  8. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-markup.js +23 -65
  9. package/node_modules/@llblab/pi-telegram/dist/lib/outbound.js +3 -3
  10. package/node_modules/@llblab/pi-telegram/dist/lib/prompts.d.ts +1 -1
  11. package/node_modules/@llblab/pi-telegram/dist/lib/prompts.js +1 -1
  12. package/node_modules/@llblab/pi-telegram/dist/lib/runtime.d.ts +1 -0
  13. package/node_modules/@llblab/pi-telegram/dist/lib/runtime.js +1 -0
  14. package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.d.ts +4 -2
  15. package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.js +24 -13
  16. package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
  17. package/node_modules/@llblab/pi-telegram/dist/skills/telegram-bridge/SKILL.md +5 -5
  18. package/node_modules/@llblab/pi-telegram/docs/architecture.md +5 -5
  19. package/node_modules/@llblab/pi-telegram/docs/compact-matrix-literal.md +1 -1
  20. package/node_modules/@llblab/pi-telegram/docs/generative-apps.md +1 -1
  21. package/node_modules/@llblab/pi-telegram/docs/outbound.md +8 -8
  22. package/node_modules/@llblab/pi-telegram/docs/public-api.md +2 -2
  23. package/node_modules/@llblab/pi-telegram/lib/outbound-buttons.ts +17 -15
  24. package/node_modules/@llblab/pi-telegram/lib/outbound-markup.ts +28 -68
  25. package/node_modules/@llblab/pi-telegram/lib/outbound.ts +3 -3
  26. package/node_modules/@llblab/pi-telegram/lib/prompts.ts +1 -1
  27. package/node_modules/@llblab/pi-telegram/lib/runtime.ts +2 -1
  28. package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +25 -12
  29. package/node_modules/@llblab/pi-telegram/package.json +1 -1
  30. package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/SKILL.md +5 -5
  31. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  All notable changes to `@llblab/pi-kit` are documented here.
4
4
 
5
+ ## 0.37.1: Bundled Package Updates (2026-10-26)
6
+
7
+ - `Exact pins`: `@llblab/pi-telegram` 0.58.1 → 0.58.3.
8
+ - `Packaging`: Synchronizes the lockfile, bundle aliases and README inventory. Package membership and resource order remain unchanged; packed validation does not certify extension safety or installed-client behavior.
9
+
5
10
  ## 0.37.0: Bundled Package Updates (2026-10-25)
6
11
 
7
12
  - `Exact pins`: `@llblab/pi-telegram` 0.57.11 → 0.58.1.
package/README.md CHANGED
@@ -18,7 +18,7 @@ Package links lead to the owning repositories for usage, documentation, issues,
18
18
  | [`@llblab/pi-codex-usage`](https://github.com/llblab/pi-codex-usage) | `0.13.2` | OpenAI/legacy Codex subscription quota and Business credit status, with persistent priority Fast toggle mirrored in Telegram |
19
19
  | [`@llblab/pi-grow-loop`](https://github.com/llblab/pi-grow-loop) | `0.9.0` | Visible continuation scheduling and bounded worker Skills through compiled, manifest-owned resources |
20
20
  | [`@llblab/pi-state-flow`](https://github.com/llblab/pi-state-flow) | `0.26.5` | Scoped context/memory compiler with step-independent current memory, stable Passive/Active modes and visible blocked-state status |
21
- | [`@llblab/pi-telegram`](https://github.com/llblab/pi-telegram) | `0.58.1` | Telegram companion with per-prompt Steer, queued controls, Reroute/Restore, recoverable connections, previews, follower Threads, files, and voice |
21
+ | [`@llblab/pi-telegram`](https://github.com/llblab/pi-telegram) | `0.58.3` | Telegram companion with per-prompt Steer, queued controls, Reroute/Restore, recoverable connections, previews, follower Threads, files, and voice |
22
22
  | [`@llblab/skills`](https://github.com/llblab/skills) | `1.15.0` | Portable workflows for engineering, review, design, context maintenance, and other focused tasks |
23
23
 
24
24
  Versions are exact by design. An upstream release does not change an installed kit until this repository explicitly advances the dependency and publishes a new kit version. Runtime defects and package-specific feature requests belong in the linked repository; package selection and kit installation issues belong here.
@@ -107,7 +107,7 @@ Use the relevant local skill before non-trivial work in its domain. Keep skill o
107
107
  - A model selection from an authorized Telegram target may stop and continue any interruptible active agent run in that same Pi session, including local/TUI work. Queue the synthetic control-lane continuation before aborting; when no Telegram prompt owns the run, bind continuation and reply ownership to the exact model-menu chat/Thread/message. Delay abort until every active tool execution settles, and clear both selection and fallback target on cancellation, agent start, settlement, or session replacement.
108
108
  - Telegram extension side effects must not hold Pi's core lifecycle hostage after semantic completion. Preserve ordering in extension-owned background work, record failures, and fence target/profile/transport/session authority.
109
109
  - In Threaded Mode, All is service/input-only. New output, previews, typing and routing UI require a concrete Thread; neither implicit fallback nor explicit aggregate delivery may write to All. Unavailable routing authority retains input without an All notice or silent settlement. Classic DM and explicit channel/business destinations remain separate; historical All output is not auto-cleaned.
110
- - Complete assistant/guest model answers use Telegram-native Rich Markdown. Harness-owned menus, status, diagnostics, thinking, and tool evidence remain explicit HTML/plain or their documented native surface. Before Telegram preview or final delivery, strip every assistant-authored HTML comment regardless of Markdown position while keeping action activation top-level-only; a comment-only result sends no text message. Preserve literal code outside comments and structurally safe chunking; never split invalid markup.
110
+ - Complete assistant/guest model answers use Telegram-native Rich Markdown. Harness-owned menus, status, diagnostics, thinking, and tool evidence remain explicit HTML/plain or their documented native surface. Before Telegram preview or final delivery, strip every assistant-authored HTML comment regardless of Markdown position and recognize complete action comments by their delimiters and payload, independent of surrounding Markdown; a comment-only result sends no text message. Preserve literal code outside comments and structurally safe chunking; never split invalid markup.
111
111
  - `preview` owns streaming lifecycle only, not assistant rendering. Finalization waits for the captured generation's preceding preview work, never later drafts blocked by that publication, and must not issue pre/post-final draft-clear calls that create transient Telegram draft UI. Reset/invalidation detaches predecessor dependencies; late settlements cannot mutate replacement state. Turns that already answer as one atomic reply (voice replies, Guest Mode queries) never stream previews.
112
112
  - Native `sendChatAction(typing)` is the automatic activity signal for unsettled agent and compaction work while Telegram transport is authorized. Extension-owned blocking UI prompts pause it and completion resumes it while either work owner remains active. Compaction may stop only a typing loop it actually started; it must preserve a pre-existing agent-owned loop. Refresh only the assigned Thread on a conservative three-second cadence; do not mirror typing to aggregate `All` and multiply shared-chat flood pressure. One Thread's pending action must not suppress another Thread's activity. Typing is best-effort presence only, never a Thread-existence or message-delivery probe: a failed exact-target action remains a structured diagnostic and must not replace a healthy connected/leader/follower status with `error`. Do not invent extra in-chat work indicators or emit activity for startup/connect/reload/recovery alone.
113
113
  - Public activity handlers and connected companion delivery are asynchronous, target-bound, generation-fenced surfaces. Connected companion projection has no independent opt-out: disconnect or authority loss is its boundary. Token deltas, hidden reasoning, unknown sources, and stale authority never enter public projection.
@@ -2,21 +2,19 @@
2
2
 
3
3
  _This file owns unresolved project work only. Completed behavior belongs in `CHANGELOG.md`; durable contracts belong in `AGENTS.md` and `/docs`._
4
4
 
5
- ## 0.58.1 publication checkpoint
6
-
7
- - [ ] Publish the independent Thread chat-action fix through the protected dev-to-main path, requiring fresh Ubuntu, Windows and both macOS shard checks at the exact candidate head and candidate/merged-tree equality. Keep the immutable Connect-only minor unchanged and verify its publication before issuing the hotfix tag.
8
- - [ ] Verify the hotfix GitHub Release and public npm latest, exact tag/gitHead identity, canonical notes, packed runtime/skills and provenance before closing this checkpoint. No live session reload or Telegram probes; controlled transport tests do not prove live client presence.
9
-
10
5
  ## Disposable Thread live acceptance
11
6
 
12
7
  - [ ] After separate operator authorization, use disposable Telegram tabs to check leader Settings review/Back, one confirmed cleanup of an inactive slotless binding, preservation of its replacement letter holder, and visible outcome/recovery copy. Native filesystem and injected-transport tests do not prove live Telegram/client or native Windows acceptance. Never use production history as the disposable fixture.
13
8
  - [ ] On separately authorized disposable routing tabs, verify pending-choice protection, Cancel after the view becomes unavailable, expiry notice in the owned leader Thread and no duplicate deletion after an unknown outcome. Local native journal/API-decoder fixtures are not live client acceptance.
14
9
 
15
- ## Prompt-cache live confirmation
10
+ ## Typing authority during Telegram turns
11
+
12
+ Live report on package 0.58.2 and Pi 1.4.0: every Telegram turn logged `sendChatAction: Telegram API call authority is unavailable` in a connected, long-lived session. The reporter's leader/follower role and refusal stage are not yet known. Prompt-cache acceptance is complete; this is a separate typing-indicator investigation.
16
13
 
17
- The prompt-cache report remains open for the reporter's offered live Anthropic capture after the published fix. Native request/transcript checks do not replace cache-hit or signed-thinking measurements.
14
+ The native expired-loop diagnostic case is corrected: real typing loop/starter, direct API runtime and API client regressions cover mocked HTTP 200 after stop/same-target replacement, with unchanged session generation and transport epoch. A process-local ended-loop observation suppresses only the corresponding typed authority diagnostic; the request still rejects, active-owner refusals remain visible, and no callback crosses IPC. Real-client regressions retain the original HTTP/network failure when authority expires during error handling; a native follower caller rejects a delayed ACK after its local owner ends, without replay. Direct/follower and Classic/Threaded paths remain distinct from live acceptance. This does not establish the cause of the reporter's live failure or prove client typing.
18
15
 
19
- - [ ] Review the reporter's resume and mid-session tool-change results (versions, cache read/write counts and new thinking-drop events). Close the report if the live evidence confirms the fix, or narrow any residual failure into actionable follow-up. Do not reconstruct or claim recovery of previously lost thinking blocks.
16
+ - [ ] Correlate the live report with the corrected native case using only redacted leader/follower role, timing around loop end/replacement and refusal-stage evidence if available. Do not infer transport loss or widen authority from the error string alone; isolate any distinct current-owner refusal before another runtime change.
17
+ - [ ] After separately approved live validation, confirm client typing and absence of expired-loop API noise across Telegram turns and loop replacement. Preserve exact Thread targeting, independent Thread requests, best-effort presence and no unhealthy-status escalation; local native fixtures are not live acceptance.
20
18
 
21
19
  ## Stuck forwarded command after Thread re-creation
22
20
 
@@ -40,8 +38,8 @@ A target-scoped reply refusal cannot prove that a command made no prior effects.
40
38
 
41
39
  ## Passive-child cold-import follow-up
42
40
 
43
- The 0.57.8 tag validation failed `Child process sharing the agent dir does not poll while parent owns Telegram lock` even when that file ran alone: `Child process did not exit. stdout= stderr=` at its existing child deadline. The other seven process tests and the identical pre-merge tree passed. That historical failure has no child-stage trace, so cold import, startup and a retained runtime handle remain competing explanations. Bounded diagnostic sampling is exhausted; passing traces showed imports up to 4.37 seconds against the five-second non-Windows child budget and natural exit within 8–14 ms after the event-loop checkpoint. These traces do not identify the original cause. A later main-validation recurrence stopped after `eval` and `import-start`, with parent-clock `elapsedMs=5009` and no import-complete checkpoint. That trace locates this recurrence before import completion, not in natural exit or a retained post-checkpoint handle; the slow dependency/startup cause remains unknown. The independent tag-owned Release validation passed the same committed tree, which does not erase the failed main check. The initial hotfix candidate then showed the same pre-import-completion state on Ubuntu at `elapsedMs=5026`; the recurrence is not macOS-only. Slow dependency/startup cost remains unexplained, separate from the fixture's corrected startup/exit observation contract.
44
- - [ ] Validate the corrected phase-owned passive-child fixture in fresh complete platform CI before publication: bounded total startup, a strict post-readiness exit guard, unchanged no-poll/natural-exit assertions and no forced shutdown or retry. Controlled delayed-start/retained-work regressions and local native passes are not platform proof. For any genuine recurrence, preserve deadline and stage evidence before correcting runtime imports or budgets. Do not spend unchanged-head diagnostic samples or claim runner contention or the historical cause is proven.
41
+ The 0.57.8 tag validation failed `Child process sharing the agent dir does not poll while parent owns Telegram lock` even when that file ran alone: `Child process did not exit. stdout= stderr=` at its existing child deadline. The other seven process tests and the identical pre-merge tree passed. That historical failure has no child-stage trace, so cold import, startup and a retained runtime handle remain competing explanations. Bounded diagnostic sampling is exhausted; passing traces showed imports up to 4.37 seconds against the five-second non-Windows child budget and natural exit within 8–14 ms after the event-loop checkpoint. These traces do not identify the original cause. A later main-validation recurrence stopped after `eval` and `import-start`, with parent-clock `elapsedMs=5009` and no import-complete checkpoint. That trace locates this recurrence before import completion, not in natural exit or a retained post-checkpoint handle; the slow dependency/startup cause remains unknown. The minor release's independent tag-owned validation passed the same committed tree, which does not erase that historical failed main check. The initial hotfix candidate then showed the same pre-import-completion state on Ubuntu at `elapsedMs=5026`; the recurrence is not macOS-only. Slow dependency/startup cost remains unexplained, separate from the fixture's corrected startup/exit observation contract.
42
+ - [ ] On a genuinely failing cold-import trace under the phase-owned fixture, isolate dependency/startup cost using its actual overall/exit deadline and stage evidence before changing runtime imports or budgets. Preserve natural-exit/no-poll assertions and job budgets. Do not spend unchanged-head diagnostic samples or infer runner contention or the historical cause from passing runs.
45
43
 
46
44
  ## Windows Workspace-gate fixture timeout
47
45
 
@@ -4,6 +4,16 @@
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.58.3: Typing Cancellation Diagnostics
8
+
9
+ - `Typing diagnostics`: Ended typing loops no longer log cancellation-only authority refusals as API failures. Real HTTP/network failures retain their provenance and diagnostics even if authority expires before settlement. Requests remain rejected and single-attempt; callbacks stay local. Native late-success/failure and follower-ACK regressions cover this case, not the cause of the live report.
10
+ - `Prompt-cache acceptance`: Reporter-provided live Anthropic captures and connected-session measurements confirm the released fix across tool changes, resume, local/Telegram switches and reconnect, with no new thinking drops in the tested runs. The remaining live-confirmation gate is closed; connected wire capture and broader model/cache-expiry coverage remain outside this evidence. Already-lost thinking is not reconstructed.
11
+
12
+ ## 0.58.2: Flexible Assistant Action Markup
13
+
14
+ - `HTML actions`: Complete button/voice comments activate by their delimiters and accepted payload regardless of whitespace or surrounding Markdown, including code fences. Adjacent same-line comments are independent; unfinished comments do not activate. Escape the opening delimiter as `<!--` for literal examples.
15
+ - `In-body buttons`: Fenced `telegram_button` blocks tolerate horizontal indentation, whitespace before the exact name, and three or more backticks or tildes. Closures require the same marker and at least the opening length on a separate line. Enclosing fenced examples remain literal; incomplete blocks, malformed JSON/CML and invalid rows still register no callbacks.
16
+
7
17
  ## 0.58.1: Independent Thread Typing
8
18
 
9
19
  - `Typing`: A pending activity request in one Thread no longer silently suppresses another Thread's typing. Exact chat/Thread/action calls still coalesce with their own minimum interval, while real Telegram 429 cooldowns remain shared across the chat, including late failures after a faster peer completes. Authority checks, one-attempt typing and bounded gate storage remain intact.
@@ -4,7 +4,7 @@
4
4
  * Owns assistant-authored telegram_button extraction, button action storage, callback handling, and prompt-turn construction
5
5
  */
6
6
  import { randomUUID } from "node:crypto";
7
- import { getTelegramActionString as getTelegramButtonString, parseTelegramActionPayloadRows, parseTelegramButtonPayloadRows, replaceTelegramButtonFences, replaceTopLevelHtmlComments, } from "./outbound-markup.js";
7
+ import { getTelegramActionString as getTelegramButtonString, parseTelegramActionPayloadRows, parseTelegramButtonPayloadRows, replaceTelegramButtonFences, replaceTelegramHtmlComments, } from "./outbound-markup.js";
8
8
  import { truncateTelegramQueueSummary, } from "./queue.js";
9
9
  const TELEGRAM_BUTTON_CALLBACK_PREFIX = "tgbtn";
10
10
  const TELEGRAM_BUTTON_ACTION_TTL_MS = 24 * 60 * 60 * 1000;
@@ -117,7 +117,23 @@ export function planTelegramButtonReply(markdown, deps) {
117
117
  }),
118
118
  }));
119
119
  };
120
- const withRichButtons = replaceTelegramButtonFences(markdown, (payload, closed) => {
120
+ const footerKeyboard = [];
121
+ const stripped = replaceTelegramHtmlComments(markdown, (comment) => {
122
+ const command = "telegram_button";
123
+ const normalizedContent = comment.content
124
+ .replace(/^\s+/, "")
125
+ .replace(/^!/, "");
126
+ if (!normalizedContent.startsWith(command))
127
+ return comment.raw;
128
+ const payloadRows = parseTelegramActionPayloadRows(comment, command);
129
+ if (!payloadRows)
130
+ return "";
131
+ const rows = buildRows(payloadRows, false);
132
+ if (rows)
133
+ footerKeyboard.push(...rows);
134
+ return "";
135
+ });
136
+ const withRichButtons = replaceTelegramButtonFences(stripped, (payload, closed) => {
121
137
  if (!closed)
122
138
  return "";
123
139
  const payloadRows = parseTelegramButtonPayloadRows(payload);
@@ -133,22 +149,8 @@ export function planTelegramButtonReply(markdown, deps) {
133
149
  }
134
150
  return `\n${rows.map(renderTelegramRichButtonRow).join("\n\n")}\n`;
135
151
  });
136
- const stripped = replaceTopLevelHtmlComments(withRichButtons, (comment) => {
137
- const command = "telegram_button";
138
- const normalizedContent = comment.content
139
- .replace(/^\s+/, "")
140
- .replace(/^!/, "");
141
- if (!normalizedContent.startsWith(command))
142
- return comment.raw;
143
- const payloadRows = parseTelegramActionPayloadRows(comment, command);
144
- if (!payloadRows)
145
- return "";
146
- const rows = buildRows(payloadRows, false);
147
- if (rows)
148
- keyboard.push(...rows);
149
- return "";
150
- });
151
- const visibleMarkdown = normalizeMarkdownAfterButtonExtraction(stripped);
152
+ keyboard.push(...footerKeyboard);
153
+ const visibleMarkdown = normalizeMarkdownAfterButtonExtraction(withRichButtons);
152
154
  return {
153
155
  markdown: keyboard.length > 0 && !visibleMarkdown
154
156
  ? DEFAULT_TELEGRAM_BUTTON_REPLY_MARKDOWN
@@ -1,22 +1,22 @@
1
1
  /**
2
2
  * Telegram outbound markup parsing helpers
3
3
  * Zones: telegram outbound, assistant markup
4
- * Owns top-level assistant action comment extraction, attribute parsing, and markup stripping shared by voice and outbound delivery
4
+ * Owns delimiter-based assistant action comment extraction, attribute parsing, and markup stripping shared by voice and outbound delivery
5
5
  */
6
- export interface TelegramTopLevelHtmlComment {
6
+ export interface TelegramHtmlComment {
7
7
  raw: string;
8
8
  content: string;
9
9
  start: number;
10
10
  end: number;
11
11
  }
12
- export declare function collectTopLevelHtmlComments(markdown: string): {
13
- comments: TelegramTopLevelHtmlComment[];
12
+ export declare function collectTelegramHtmlComments(markdown: string): {
13
+ comments: TelegramHtmlComment[];
14
14
  openCommentStart?: number;
15
15
  };
16
16
  export declare function replaceTelegramButtonFences(markdown: string, replace: (payload: string, closed: boolean) => string): string;
17
- export declare function replaceTopLevelHtmlComments(markdown: string, replacer: (comment: TelegramTopLevelHtmlComment) => string): string;
17
+ export declare function replaceTelegramHtmlComments(markdown: string, replacer: (comment: TelegramHtmlComment) => string): string;
18
18
  export declare function parseTelegramButtonPayloadRows(source: string): Record<string, unknown>[][] | undefined;
19
- export declare function parseTelegramActionPayloadRows(comment: TelegramTopLevelHtmlComment, command: string): Record<string, unknown>[][] | undefined;
19
+ export declare function parseTelegramActionPayloadRows(comment: TelegramHtmlComment, command: string): Record<string, unknown>[][] | undefined;
20
20
  export declare function stripTelegramCommentMarkupForPreview(markdown: string): string;
21
21
  export declare function stripTelegramCommentMarkupForDelivery(markdown: string): string;
22
22
  export interface TelegramVoiceReplyItem {
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Telegram outbound markup parsing helpers
3
3
  * Zones: telegram outbound, assistant markup
4
- * Owns top-level assistant action comment extraction, attribute parsing, and markup stripping shared by voice and outbound delivery
4
+ * Owns delimiter-based assistant action comment extraction, attribute parsing, and markup stripping shared by voice and outbound delivery
5
5
  */
6
6
  import { isWireRecord as isTelegramActionPayload } from "./wire.js";
7
7
  function getMarkdownLineEnd(markdown, offset) {
@@ -12,7 +12,7 @@ function getMarkdownLineText(markdown, offset, end) {
12
12
  return markdown.slice(offset, end).replace(/\r?\n$/, "");
13
13
  }
14
14
  function getTopLevelOpeningFence(line) {
15
- const match = line.match(/^(?: {0,3})(`{3,}|~{3,})/);
15
+ const match = line.match(/^[ \t]*(`{3,}|~{3,})/);
16
16
  const sequence = match?.[1];
17
17
  if (!sequence)
18
18
  return undefined;
@@ -22,43 +22,26 @@ function getTopLevelOpeningFence(line) {
22
22
  };
23
23
  }
24
24
  function isTopLevelClosingFence(line, fence) {
25
- const match = line.match(/^(?: {0,3})(`{3,}|~{3,})([ \t]*)$/);
25
+ const match = line.match(/^[ \t]*(`{3,}|~{3,})[ \t]*$/);
26
26
  const sequence = match?.[1];
27
27
  return (!!sequence &&
28
28
  sequence[0] === fence.marker &&
29
29
  sequence.length >= fence.length);
30
30
  }
31
- export function collectTopLevelHtmlComments(markdown) {
31
+ export function collectTelegramHtmlComments(markdown) {
32
32
  const comments = [];
33
33
  let offset = 0;
34
- let fence;
35
34
  while (offset < markdown.length) {
36
- const lineEnd = getMarkdownLineEnd(markdown, offset);
37
- const line = getMarkdownLineText(markdown, offset, lineEnd);
38
- if (fence) {
39
- if (isTopLevelClosingFence(line, fence))
40
- fence = undefined;
41
- offset = lineEnd;
42
- continue;
43
- }
44
- const nextFence = getTopLevelOpeningFence(line);
45
- if (nextFence) {
46
- fence = nextFence;
47
- offset = lineEnd;
48
- continue;
49
- }
50
- if (line.startsWith("<!--")) {
51
- const closeIndex = markdown.indexOf("-->", offset + 4);
52
- if (closeIndex === -1)
53
- return { comments, openCommentStart: offset };
54
- const end = closeIndex + 3;
55
- const raw = markdown.slice(offset, end);
56
- const content = raw.slice(4, -3);
57
- comments.push({ raw, content, start: offset, end });
58
- offset = getMarkdownLineEnd(markdown, end);
59
- continue;
60
- }
61
- offset = lineEnd;
35
+ const start = markdown.indexOf("<!--", offset);
36
+ if (start === -1)
37
+ break;
38
+ const closeIndex = markdown.indexOf("-->", start + 4);
39
+ if (closeIndex === -1)
40
+ return { comments, openCommentStart: start };
41
+ const end = closeIndex + 3;
42
+ const raw = markdown.slice(start, end);
43
+ comments.push({ raw, content: raw.slice(4, -3), start, end });
44
+ offset = end;
62
45
  }
63
46
  return { comments };
64
47
  }
@@ -92,7 +75,7 @@ export function replaceTelegramButtonFences(markdown, replace) {
92
75
  }
93
76
  else {
94
77
  fence = getTopLevelOpeningFence(line);
95
- if (fence && /^```telegram_button[ \t]*$/.test(line)) {
78
+ if (fence && /^[ \t]*(?:`{3,}|~{3,})[ \t]*telegram_button[ \t]*$/.test(line)) {
96
79
  actionStart = offset;
97
80
  contentStart = end;
98
81
  }
@@ -105,8 +88,8 @@ export function replaceTelegramButtonFences(markdown, replace) {
105
88
  }
106
89
  return result + markdown.slice(copied);
107
90
  }
108
- export function replaceTopLevelHtmlComments(markdown, replacer) {
109
- const { comments } = collectTopLevelHtmlComments(markdown);
91
+ export function replaceTelegramHtmlComments(markdown, replacer) {
92
+ const { comments } = collectTelegramHtmlComments(markdown);
110
93
  if (comments.length === 0)
111
94
  return markdown;
112
95
  let result = "";
@@ -118,34 +101,9 @@ export function replaceTopLevelHtmlComments(markdown, replacer) {
118
101
  }
119
102
  return result + markdown.slice(offset);
120
103
  }
121
- function findTopLevelOpenOrPartialHtmlCommentIndex(markdown) {
122
- const { openCommentStart } = collectTopLevelHtmlComments(markdown);
123
- if (openCommentStart !== undefined)
124
- return openCommentStart;
125
- let offset = 0;
126
- let fence;
127
- while (offset < markdown.length) {
128
- const lineEnd = getMarkdownLineEnd(markdown, offset);
129
- const line = getMarkdownLineText(markdown, offset, lineEnd);
130
- const isLastLine = lineEnd >= markdown.length;
131
- if (fence) {
132
- if (isTopLevelClosingFence(line, fence))
133
- fence = undefined;
134
- offset = lineEnd;
135
- continue;
136
- }
137
- const nextFence = getTopLevelOpeningFence(line);
138
- if (nextFence) {
139
- fence = nextFence;
140
- offset = lineEnd;
141
- continue;
142
- }
143
- if (isLastLine && (line === "<" || line === "<!" || line === "<!-")) {
144
- return offset;
145
- }
146
- offset = lineEnd;
147
- }
148
- return -1;
104
+ function findOpenOrPartialHtmlCommentIndex(markdown) {
105
+ const start = markdown.indexOf("<!--");
106
+ return start >= 0 ? start : (markdown.match(/<(?:!-?)?$/u)?.index ?? -1);
149
107
  }
150
108
  function parseTolerantTelegramAttributes(source, names) {
151
109
  const attributes = {};
@@ -592,7 +550,7 @@ function stripTelegramHtmlCommentBlocks(markdown) {
592
550
  }
593
551
  export function stripTelegramCommentMarkupForPreview(markdown) {
594
552
  const withoutClosedBlocks = stripTelegramHtmlCommentBlocks(replaceTelegramButtonFences(markdown, () => ""));
595
- const openBlockIndex = findTopLevelOpenOrPartialHtmlCommentIndex(withoutClosedBlocks);
553
+ const openBlockIndex = findOpenOrPartialHtmlCommentIndex(withoutClosedBlocks);
596
554
  const previewMarkdown = openBlockIndex >= 0
597
555
  ? withoutClosedBlocks.slice(0, openBlockIndex)
598
556
  : withoutClosedBlocks;
@@ -600,7 +558,7 @@ export function stripTelegramCommentMarkupForPreview(markdown) {
600
558
  }
601
559
  export function stripTelegramCommentMarkupForDelivery(markdown) {
602
560
  const withoutClosedBlocks = stripTelegramHtmlCommentBlocks(markdown);
603
- const openBlockIndex = findTopLevelOpenOrPartialHtmlCommentIndex(withoutClosedBlocks);
561
+ const openBlockIndex = findOpenOrPartialHtmlCommentIndex(withoutClosedBlocks);
604
562
  const deliveryMarkdown = openBlockIndex >= 0
605
563
  ? withoutClosedBlocks.slice(0, openBlockIndex)
606
564
  : withoutClosedBlocks;
@@ -617,7 +575,7 @@ export function planTelegramVoiceReply(markdown) {
617
575
  const voiceReplies = [];
618
576
  let lang;
619
577
  let rate;
620
- const stripped = replaceTopLevelHtmlComments(markdown, (comment) => {
578
+ const stripped = replaceTelegramHtmlComments(markdown, (comment) => {
621
579
  const command = "telegram_voice";
622
580
  const normalizedContent = comment.content
623
581
  .replace(/^\s+/, "")
@@ -412,10 +412,10 @@ export function createTelegramOutboundReplyPlanner(store, getRenderingMode = ()
412
412
  rendering: getRenderingMode(),
413
413
  ...(options?.binding ? { binding: options.binding } : {}),
414
414
  });
415
- // Button replies can also contain <!-- telegram_voice --> markup
416
- const voiceReply = planTelegramVoiceReply(buttonReply.markdown);
415
+ // Extract voice from the original text before button-fence rendering consumes it.
416
+ const voiceReply = planTelegramVoiceReply(markdown);
417
417
  return {
418
- markdown: voiceReply.markdown,
418
+ markdown: stripTelegramCommentMarkupForDelivery(buttonReply.markdown),
419
419
  ...(buttonReply.replyMarkup
420
420
  ? { replyMarkup: buttonReply.replyMarkup }
421
421
  : {}),
@@ -9,7 +9,7 @@ export declare const TELEGRAM_DISCONNECTED_CONTEXT_MESSAGE = "Telegram session d
9
9
  export declare const TELEGRAM_ATTACH_PROMPT_SNIPPET = "Queue files for the active Telegram reply; outside Telegram turns, send files directly to Telegram.";
10
10
  export declare const TELEGRAM_ATTACH_PROMPT_GUIDELINES: readonly ["When handling a [telegram] message and the user asked for a file or generated artifact, call telegram_attach with the local path instead of only mentioning the path in text.", "When a local/TUI user explicitly asks to send a generated file to Telegram, telegram_attach can deliver it to the paired/default Telegram chat even without an active Telegram turn.", "For an explicit thread target, provide chat_id plus thread_id; registered multi-instance followers default to their assigned thread target."];
11
11
  export declare const TELEGRAM_MESSAGE_PROMPT_SNIPPET = "Send direct Telegram Markdown text when the user explicitly asks for Telegram delivery to a concrete chat, channel, or live Pi Thread outside the normal reply flow.";
12
- export declare const TELEGRAM_MESSAGE_PROMPT_GUIDELINES: readonly ["Use telegram_message only when the user explicitly asks to send a message to Telegram from the local/TUI side, or names a concrete Telegram delivery target.", "For an explicitly requested channel post, pass its exact numeric id or public @username as chat_id; no local channel registry is required, and Telegram remains the authority on the bot's posting permission.", "For an explicitly requested channel media post, pass one local .jpg/.jpeg/.png/.webp photo or .mp4 video as media; the text becomes its caption (max 1024 characters), and albums or other media types are rejected.", "For a live Pi thread target, provide thread as its case-insensitive name or numeric id; the bridge sends visibly and admits one attributed turn to that live instance. Unknown, ambiguous, same, or offline targets fail before sending.", "Add buttons by embedding the same top-level telegram_button HTML comments used in normal Telegram replies; Telegram does not support standalone buttons.", "During an active Telegram turn, omit telegram_message for the current target and answer normally; use thread only when the user requests delivery to a different live Pi thread."];
12
+ export declare const TELEGRAM_MESSAGE_PROMPT_GUIDELINES: readonly ["Use telegram_message only when the user explicitly asks to send a message to Telegram from the local/TUI side, or names a concrete Telegram delivery target.", "For an explicitly requested channel post, pass its exact numeric id or public @username as chat_id; no local channel registry is required, and Telegram remains the authority on the bot's posting permission.", "For an explicitly requested channel media post, pass one local .jpg/.jpeg/.png/.webp photo or .mp4 video as media; the text becomes its caption (max 1024 characters), and albums or other media types are rejected.", "For a live Pi thread target, provide thread as its case-insensitive name or numeric id; the bridge sends visibly and admits one attributed turn to that live instance. Unknown, ambiguous, same, or offline targets fail before sending.", "Add buttons by embedding the same telegram_button HTML comments used in normal Telegram replies; Telegram does not support standalone buttons.", "During an active Telegram turn, omit telegram_message for the current target and answer normally; use thread only when the user requests delivery to a different live Pi thread."];
13
13
  export interface TelegramModelContextAvailabilityMemory {
14
14
  suspended: boolean;
15
15
  toolNames: Set<string>;
@@ -24,7 +24,7 @@ export const TELEGRAM_MESSAGE_PROMPT_GUIDELINES = [
24
24
  "For an explicitly requested channel post, pass its exact numeric id or public @username as chat_id; no local channel registry is required, and Telegram remains the authority on the bot's posting permission.",
25
25
  "For an explicitly requested channel media post, pass one local .jpg/.jpeg/.png/.webp photo or .mp4 video as media; the text becomes its caption (max 1024 characters), and albums or other media types are rejected.",
26
26
  "For a live Pi thread target, provide thread as its case-insensitive name or numeric id; the bridge sends visibly and admits one attributed turn to that live instance. Unknown, ambiguous, same, or offline targets fail before sending.",
27
- "Add buttons by embedding the same top-level telegram_button HTML comments used in normal Telegram replies; Telegram does not support standalone buttons.",
27
+ "Add buttons by embedding the same telegram_button HTML comments used in normal Telegram replies; Telegram does not support standalone buttons.",
28
28
  "During an active Telegram turn, omit telegram_message for the current target and answer normally; use thread only when the user requests delivery to a different live Pi thread.",
29
29
  ];
30
30
  const TELEGRAM_TOOL_METADATA_LINES = Object.fromEntries([
@@ -119,6 +119,7 @@ export interface TelegramTypingLoopStarterDeps<TContext> extends TelegramRuntime
119
119
  sendTypingAction: (chatId: number, options?: {
120
120
  message_thread_id?: number;
121
121
  assertAuthority?: () => void;
122
+ isCancelled?: () => boolean;
122
123
  }) => Promise<unknown>;
123
124
  updateStatus: (ctx: TContext, error?: string) => void;
124
125
  isContextActive?: (ctx: TContext) => boolean;
@@ -236,6 +236,7 @@ export function createTelegramTypingLoopStarter(deps) {
236
236
  try {
237
237
  await deps.sendTypingAction(targetChatId, {
238
238
  ...actionOptions,
239
+ isCancelled: () => !active,
239
240
  // This stays process-local; adapters recheck it before HTTP/IPC issuance, not just loop ticks.
240
241
  assertAuthority() {
241
242
  if (!active || !canContinue())
@@ -304,10 +304,10 @@ export interface TelegramApiCallOptions {
304
304
  }
305
305
  export declare class TelegramApiAuthorityError extends Error {
306
306
  readonly requestIssued: boolean;
307
- constructor(requestIssued: boolean);
307
+ constructor(requestIssued: boolean, options?: ErrorOptions);
308
308
  }
309
309
  /** Recheck caller authority, classifying refusal by whether a Bot API request was already issued. */
310
- export declare function assertTelegramApiCallAuthority(assertAuthority: (() => void) | undefined, requestIssued: boolean): void;
310
+ export declare function assertTelegramApiCallAuthority(assertAuthority: (() => void) | undefined, requestIssued: boolean, errorOptions?: ErrorOptions): void;
311
311
  export interface TelegramFileDownloadOptions {
312
312
  signal?: AbortSignal;
313
313
  maxFileSizeBytes?: number;
@@ -444,6 +444,8 @@ export interface TelegramBridgeApiRuntimeDeps {
444
444
  /** Typing issuance guard is local authority, never Bot API body or IPC data. */
445
445
  export interface TelegramTypingActionOptions extends Pick<TelegramApiCallOptions, "assertAuthority"> {
446
446
  message_thread_id?: number;
447
+ /** Diagnostic observation only: an ended loop does not grant issuance, success or retry. */
448
+ isCancelled?: () => boolean;
447
449
  }
448
450
  export interface TelegramBridgeApiRuntime extends Pick<TelegramApiClient, "call" | "callMultipart"> {
449
451
  downloadFile: (fileId: string, suggestedName: string, source?: TelegramAttachmentSource) => Promise<string>;
@@ -43,19 +43,19 @@ const TELEGRAM_NETWORK_FAMILY_VALUES = new Set([
43
43
  ]);
44
44
  export class TelegramApiAuthorityError extends Error {
45
45
  requestIssued;
46
- constructor(requestIssued) {
47
- super("Telegram API call authority is unavailable.");
46
+ constructor(requestIssued, options) {
47
+ super("Telegram API call authority is unavailable.", options);
48
48
  this.name = "TelegramApiAuthorityError";
49
49
  this.requestIssued = requestIssued;
50
50
  }
51
51
  }
52
52
  /** Recheck caller authority, classifying refusal by whether a Bot API request was already issued. */
53
- export function assertTelegramApiCallAuthority(assertAuthority, requestIssued) {
53
+ export function assertTelegramApiCallAuthority(assertAuthority, requestIssued, errorOptions) {
54
54
  try {
55
55
  assertAuthority?.();
56
56
  }
57
57
  catch {
58
- throw new TelegramApiAuthorityError(requestIssued);
58
+ throw new TelegramApiAuthorityError(requestIssued, errorOptions);
59
59
  }
60
60
  }
61
61
  function parseTelegramApiTargetInteger(value) {
@@ -765,7 +765,7 @@ async function callTelegramWithRetry(method, request, options) {
765
765
  // Authority refusal is never a transport failure, retry grant or acknowledged success.
766
766
  if (error instanceof TelegramApiAuthorityError)
767
767
  throw error;
768
- assertTelegramApiCallAuthority(assertAuthority, requestIssued);
768
+ assertTelegramApiCallAuthority(assertAuthority, requestIssued, { cause: error });
769
769
  const retryable = isRetryableTelegramApiError(error) &&
770
770
  !(options?.retryRateLimit === false &&
771
771
  error instanceof TelegramApiHttpError &&
@@ -1120,7 +1120,10 @@ export function createTelegramBridgeApiRuntime(deps) {
1120
1120
  }));
1121
1121
  return true;
1122
1122
  }
1123
- deps.recordRuntimeEvent("api", error, withTelegramTransportDiagnostics(error, { method }));
1123
+ const diagnosticError = error instanceof TelegramApiAuthorityError && Object.hasOwn(error, "cause")
1124
+ ? error.cause : error;
1125
+ if (!options?.isExpectedError?.(error))
1126
+ deps.recordRuntimeEvent("api", diagnosticError, withTelegramTransportDiagnostics(diagnosticError, { method }));
1124
1127
  throw error;
1125
1128
  })
1126
1129
  .finally(() => {
@@ -1194,13 +1197,21 @@ export function createTelegramBridgeApiRuntime(deps) {
1194
1197
  ? { message_thread_id: options.message_thread_id }
1195
1198
  : {}),
1196
1199
  }),
1197
- sendTypingAction: createTelegramChatActionSender((chatId, action, options) => callRecorded("sendChatAction", {
1198
- chat_id: chatId,
1199
- action,
1200
- ...(options?.message_thread_id !== undefined
1201
- ? { message_thread_id: options.message_thread_id }
1202
- : {}),
1203
- }, { assertAuthority: options?.assertAuthority }), "typing"),
1200
+ sendTypingAction: createTelegramChatActionSender((chatId, action, options) => {
1201
+ const isCancelled = options?.isCancelled;
1202
+ return callRecorded("sendChatAction", {
1203
+ chat_id: chatId,
1204
+ action,
1205
+ ...(options?.message_thread_id !== undefined
1206
+ ? { message_thread_id: options.message_thread_id }
1207
+ : {}),
1208
+ }, {
1209
+ assertAuthority: options?.assertAuthority,
1210
+ ...(isCancelled ? {
1211
+ isExpectedError: (error) => error instanceof TelegramApiAuthorityError && !Object.hasOwn(error, "cause") && isCancelled() === true,
1212
+ } : {}),
1213
+ });
1214
+ }, "typing"),
1204
1215
  sendRecordVoiceAction: createTelegramChatActionSender((chatId, action, options) => callRecorded("sendChatAction", {
1205
1216
  chat_id: chatId,
1206
1217
  action,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.58.1",
3
+ "version": "0.58.3",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -16,12 +16,12 @@ Use Telegram as a mobile companion to the current Pi session. Preserve the exact
16
16
  | Explicitly send from local/TUI to Telegram | `telegram_message` or `telegram_attach` |
17
17
  | Explicitly send to a different live Thread | `telegram_message(thread=...)` |
18
18
  | Add buttons | Hidden comment for footer; `telegram_button` fence for in-body rows |
19
- | Add explicit voice | Top-level hidden action comment |
19
+ | Add explicit voice | Hidden action comment |
20
20
  | Build a repeated deterministic interaction | Follow `generative-apps` |
21
21
 
22
22
  A connected Telegram session proves capability, not user intent. Use Telegram features on Telegram-originated turns or explicit Telegram delivery requests only. Never call `telegram_message` for the current active target.
23
23
 
24
- For direct delivery, Thread routing, configuration, or diagnosis, read only the applicable reference listed under [Conditional References](#conditional-references).
24
+ For delivery, Thread routing, configuration or diagnosis, read the applicable reference under [Conditional References](#conditional-references).
25
25
 
26
26
  ## Turn Context
27
27
 
@@ -40,7 +40,7 @@ Reply in concise, phone-width Telegram Rich Markdown. Use `$...$` and `$$...$$`
40
40
 
41
41
  ## Assistant Actions
42
42
 
43
- `telegram_button` and `telegram_voice` are markup, not tools. Emit action comments at column zero outside lists, quotes, code, and indentation. Comments create footer buttons or voice artifacts. For buttons between paragraphs, use a column-zero triple-backtick `telegram_button` block. Both button wrappers accept the same singleton JSON/CML cell or mixed matrix; the wrapper determines placement. Telegram removes every assistant-authored HTML comment from previews and final replies regardless of owner or Markdown position; only recognized top-level wrappers activate actions; comment-only output sends no text message.
43
+ `telegram_button` and `telegram_voice` are markup, not tools. Complete comments activate regardless of surrounding Markdown. Escape `<!--` as `&lt;!--` for literal examples; backticks and backslashes do not disable actions. Comments create footer buttons or voice artifacts. In-body fences: ≥3 backticks/tildes, optional indentation/whitespace, exact `telegram_button`; separate same-marker closure, no shorter. Both wrappers accept a singleton JSON/CML cell or mixed matrix; the wrapper determines placement. Telegram removes every assistant-authored HTML comment from previews and final replies regardless of owner or Markdown position; only complete recognized comments or valid button fences activate actions; comment-only output sends no text message.
44
44
 
45
45
  ### Shared Encoding Rule
46
46
 
@@ -61,7 +61,7 @@ Every enabled button has a self-contained prompt and an optional selection style
61
61
  - `{label|prompt|selected_style}` and `{|prompt|selected_style}` accept `primary`, `success`, or `danger`.
62
62
  - Fourth-position `1`/`true` disables, `0`/`false` enables; omission means enabled. JSON uses boolean `disabled`. `{|Next||1}` omits label/style; `{Next|||1}` omits prompt/style; `{|||1}` is blank (Telegram receives a non-breaking space). Prefer meaningful labels and retain a useful enabled action. Enabled CML requires a prompt. Disabled controls stay visible but have no callback, queued prompt, or bound-method invocation.
63
63
  - Both wrappers concatenate rows from adjacent JSON/CML cells or matrices, with optional commas. Cells are vertical; horizontal peers require a nested row: `[[{A|a}{B|b}]]`.
64
- - Prefer one matrix per related group. Fenced blocks stay in place in Rich mode; HTML compatibility moves them to the footer. Native rows allow eight buttons. Malformed/oversized/unclosed blocks activate nothing; drafts hide them. Outer code fences and quoted/indented examples remain literal.
64
+ - Prefer one matrix per related group. Fenced blocks stay in place in Rich mode; HTML compatibility moves them to the footer. Native rows allow eight buttons. Malformed/oversized/unclosed blocks activate nothing; drafts hide them. Enclosing code fences and quoted/list-prefixed button fences remain literal.
65
65
  - Both placements share prompt/app routing. In-body clicks acknowledge without recoloring; selected-style highlighting remains footer-only.
66
66
 
67
67
  A single in-body button (the four-backtick wrapper below makes this a literal example):
@@ -124,6 +124,6 @@ Before replying:
124
124
 
125
125
  - Use the ordinary path for the current target and direct tools only for explicit other delivery.
126
126
  - Attach requested files rather than only mentioning them.
127
- - Keep action comments and button fences top-level, complete, and canonical: CML first, JSON when necessary; either syntax may coexist within one matrix.
127
+ - Keep comments complete and button fences top-level and canonical: CML first, JSON when necessary; either syntax may coexist within one matrix.
128
128
  - Give every enabled button a self-contained prompt; preserve confirmation for dangerous actions.
129
129
  - Expose no secret or hidden reasoning.