@llblab/pi-kit 0.37.0 → 0.38.0

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 (152) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +3 -3
  3. package/node_modules/@llblab/pi-state-flow/AGENTS.md +7 -3
  4. package/node_modules/@llblab/pi-state-flow/BACKLOG.md +679 -2
  5. package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +17 -0
  6. package/node_modules/@llblab/pi-state-flow/README.md +30 -14
  7. package/node_modules/@llblab/pi-state-flow/api/modules.ts +11 -0
  8. package/node_modules/@llblab/pi-state-flow/dist/api/modules.d.ts +3 -0
  9. package/node_modules/@llblab/pi-state-flow/dist/api/modules.js +3 -0
  10. package/node_modules/@llblab/pi-state-flow/dist/lib/acquisition.d.ts +7 -4
  11. package/node_modules/@llblab/pi-state-flow/dist/lib/acquisition.js +77 -36
  12. package/node_modules/@llblab/pi-state-flow/dist/lib/artifact.d.ts +2 -0
  13. package/node_modules/@llblab/pi-state-flow/dist/lib/artifact.js +26 -27
  14. package/node_modules/@llblab/pi-state-flow/dist/lib/config.d.ts +3 -0
  15. package/node_modules/@llblab/pi-state-flow/dist/lib/config.js +13 -1
  16. package/node_modules/@llblab/pi-state-flow/dist/lib/context.d.ts +27 -21
  17. package/node_modules/@llblab/pi-state-flow/dist/lib/context.js +144 -46
  18. package/node_modules/@llblab/pi-state-flow/dist/lib/continuation.js +14 -18
  19. package/node_modules/@llblab/pi-state-flow/dist/lib/durable.d.ts +6 -0
  20. package/node_modules/@llblab/pi-state-flow/dist/lib/durable.js +60 -50
  21. package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +1049 -310
  22. package/node_modules/@llblab/pi-state-flow/dist/lib/history.d.ts +4 -4
  23. package/node_modules/@llblab/pi-state-flow/dist/lib/history.js +15 -3
  24. package/node_modules/@llblab/pi-state-flow/dist/lib/json.d.ts +2 -0
  25. package/node_modules/@llblab/pi-state-flow/dist/lib/json.js +2 -1
  26. package/node_modules/@llblab/pi-state-flow/dist/lib/memory.d.ts +3 -1
  27. package/node_modules/@llblab/pi-state-flow/dist/lib/memory.js +7 -7
  28. package/node_modules/@llblab/pi-state-flow/dist/lib/migration.d.ts +29 -0
  29. package/node_modules/@llblab/pi-state-flow/dist/lib/migration.js +186 -0
  30. package/node_modules/@llblab/pi-state-flow/dist/lib/model-interface.d.ts +17 -0
  31. package/node_modules/@llblab/pi-state-flow/dist/lib/model-interface.js +158 -0
  32. package/node_modules/@llblab/pi-state-flow/dist/lib/module-registry.d.ts +21 -0
  33. package/node_modules/@llblab/pi-state-flow/dist/lib/module-registry.js +108 -0
  34. package/node_modules/@llblab/pi-state-flow/dist/lib/operation.js +3 -2
  35. package/node_modules/@llblab/pi-state-flow/dist/lib/ownership.d.ts +4 -4
  36. package/node_modules/@llblab/pi-state-flow/dist/lib/ownership.js +5 -5
  37. package/node_modules/@llblab/pi-state-flow/dist/lib/planes.d.ts +29 -0
  38. package/node_modules/@llblab/pi-state-flow/dist/lib/planes.js +84 -0
  39. package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.d.ts +6 -3
  40. package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.js +33 -16
  41. package/node_modules/@llblab/pi-state-flow/dist/lib/query.d.ts +21 -4
  42. package/node_modules/@llblab/pi-state-flow/dist/lib/query.js +53 -13
  43. package/node_modules/@llblab/pi-state-flow/dist/lib/recovery.js +5 -2
  44. package/node_modules/@llblab/pi-state-flow/dist/lib/rehydration.d.ts +3 -0
  45. package/node_modules/@llblab/pi-state-flow/dist/lib/rehydration.js +10 -1
  46. package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.d.ts +84 -5
  47. package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.js +570 -20
  48. package/node_modules/@llblab/pi-state-flow/dist/lib/selection.d.ts +10 -0
  49. package/node_modules/@llblab/pi-state-flow/dist/lib/selection.js +32 -0
  50. package/node_modules/@llblab/pi-state-flow/dist/lib/semantic-module.d.ts +79 -0
  51. package/node_modules/@llblab/pi-state-flow/dist/lib/semantic-module.js +187 -0
  52. package/node_modules/@llblab/pi-state-flow/dist/lib/session.d.ts +98 -1
  53. package/node_modules/@llblab/pi-state-flow/dist/lib/session.js +398 -3
  54. package/node_modules/@llblab/pi-state-flow/dist/lib/skills.d.ts +7 -2
  55. package/node_modules/@llblab/pi-state-flow/dist/lib/skills.js +55 -3
  56. package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.d.ts +67 -14
  57. package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.js +129 -9
  58. package/node_modules/@llblab/pi-state-flow/dist/lib/state.d.ts +26 -14
  59. package/node_modules/@llblab/pi-state-flow/dist/lib/state.js +61 -19
  60. package/node_modules/@llblab/pi-state-flow/dist/lib/status.d.ts +14 -9
  61. package/node_modules/@llblab/pi-state-flow/dist/lib/status.js +59 -22
  62. package/node_modules/@llblab/pi-state-flow/dist/lib/storage.d.ts +111 -10
  63. package/node_modules/@llblab/pi-state-flow/dist/lib/storage.js +728 -11
  64. package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.d.ts +10 -7
  65. package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.js +50 -14
  66. package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.d.ts +40 -4
  67. package/node_modules/@llblab/pi-state-flow/dist/lib/temporal.js +247 -43
  68. package/node_modules/@llblab/pi-state-flow/dist/lib/transition.d.ts +41 -8
  69. package/node_modules/@llblab/pi-state-flow/dist/lib/transition.js +132 -39
  70. package/node_modules/@llblab/pi-state-flow/dist/package.json +6 -1
  71. package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-guide/SKILL.md +28 -16
  72. package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-memory/SKILL.md +12 -8
  73. package/node_modules/@llblab/pi-state-flow/docs/README.md +9 -0
  74. package/node_modules/@llblab/pi-state-flow/docs/architecture.md +76 -43
  75. package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +9 -1
  76. package/node_modules/@llblab/pi-state-flow/docs/fork-contract.md +6 -0
  77. package/node_modules/@llblab/pi-state-flow/docs/lazy-state.md +23 -19
  78. package/node_modules/@llblab/pi-state-flow/docs/protocol-composition-decision.md +66 -0
  79. package/node_modules/@llblab/pi-state-flow/docs/public-module-contract.md +73 -0
  80. package/node_modules/@llblab/pi-state-flow/docs/scope-selection-contract.md +814 -0
  81. package/node_modules/@llblab/pi-state-flow/docs/semantic-compatibility-decision.md +212 -0
  82. package/node_modules/@llblab/pi-state-flow/docs/semantic-extensibility-decision.md +75 -0
  83. package/node_modules/@llblab/pi-state-flow/docs/semantic-handoff-contract.md +174 -0
  84. package/node_modules/@llblab/pi-state-flow/docs/semantic-memory-evaluation.md +166 -0
  85. package/node_modules/@llblab/pi-state-flow/docs/semantic-module-contract.md +84 -0
  86. package/node_modules/@llblab/pi-state-flow/docs/single-scope-interface-contract.md +201 -0
  87. package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +22 -1
  88. package/node_modules/@llblab/pi-state-flow/docs/usage.md +32 -18
  89. package/node_modules/@llblab/pi-state-flow/lib/acquisition.ts +68 -31
  90. package/node_modules/@llblab/pi-state-flow/lib/artifact.ts +21 -22
  91. package/node_modules/@llblab/pi-state-flow/lib/config.ts +14 -1
  92. package/node_modules/@llblab/pi-state-flow/lib/context.ts +142 -46
  93. package/node_modules/@llblab/pi-state-flow/lib/continuation.ts +14 -17
  94. package/node_modules/@llblab/pi-state-flow/lib/durable.ts +53 -44
  95. package/node_modules/@llblab/pi-state-flow/lib/extension.ts +897 -297
  96. package/node_modules/@llblab/pi-state-flow/lib/history.ts +16 -7
  97. package/node_modules/@llblab/pi-state-flow/lib/json.ts +2 -1
  98. package/node_modules/@llblab/pi-state-flow/lib/memory.ts +10 -9
  99. package/node_modules/@llblab/pi-state-flow/lib/migration.ts +189 -0
  100. package/node_modules/@llblab/pi-state-flow/lib/model-interface.ts +136 -0
  101. package/node_modules/@llblab/pi-state-flow/lib/module-registry.ts +119 -0
  102. package/node_modules/@llblab/pi-state-flow/lib/operation.ts +3 -2
  103. package/node_modules/@llblab/pi-state-flow/lib/ownership.ts +6 -6
  104. package/node_modules/@llblab/pi-state-flow/lib/planes.ts +98 -0
  105. package/node_modules/@llblab/pi-state-flow/lib/protocol.ts +37 -16
  106. package/node_modules/@llblab/pi-state-flow/lib/query.ts +59 -18
  107. package/node_modules/@llblab/pi-state-flow/lib/recovery.ts +5 -2
  108. package/node_modules/@llblab/pi-state-flow/lib/rehydration.ts +10 -1
  109. package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +574 -21
  110. package/node_modules/@llblab/pi-state-flow/lib/selection.ts +39 -0
  111. package/node_modules/@llblab/pi-state-flow/lib/semantic-module.ts +239 -0
  112. package/node_modules/@llblab/pi-state-flow/lib/session.ts +387 -3
  113. package/node_modules/@llblab/pi-state-flow/lib/skills.ts +45 -4
  114. package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +156 -16
  115. package/node_modules/@llblab/pi-state-flow/lib/state.ts +69 -28
  116. package/node_modules/@llblab/pi-state-flow/lib/status.ts +65 -31
  117. package/node_modules/@llblab/pi-state-flow/lib/storage.ts +770 -18
  118. package/node_modules/@llblab/pi-state-flow/lib/telegram.ts +58 -20
  119. package/node_modules/@llblab/pi-state-flow/lib/temporal.ts +267 -50
  120. package/node_modules/@llblab/pi-state-flow/lib/transition.ts +184 -52
  121. package/node_modules/@llblab/pi-state-flow/package.json +6 -1
  122. package/node_modules/@llblab/pi-state-flow/skills/state-flow-guide/SKILL.md +28 -16
  123. package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +12 -8
  124. package/node_modules/@llblab/pi-telegram/AGENTS.md +1 -1
  125. package/node_modules/@llblab/pi-telegram/BACKLOG.md +8 -10
  126. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +10 -0
  127. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-buttons.js +20 -18
  128. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-markup.d.ts +6 -6
  129. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-markup.js +23 -65
  130. package/node_modules/@llblab/pi-telegram/dist/lib/outbound.js +3 -3
  131. package/node_modules/@llblab/pi-telegram/dist/lib/prompts.d.ts +1 -1
  132. package/node_modules/@llblab/pi-telegram/dist/lib/prompts.js +1 -1
  133. package/node_modules/@llblab/pi-telegram/dist/lib/runtime.d.ts +1 -0
  134. package/node_modules/@llblab/pi-telegram/dist/lib/runtime.js +1 -0
  135. package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.d.ts +4 -2
  136. package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.js +24 -13
  137. package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
  138. package/node_modules/@llblab/pi-telegram/dist/skills/telegram-bridge/SKILL.md +5 -5
  139. package/node_modules/@llblab/pi-telegram/docs/architecture.md +5 -5
  140. package/node_modules/@llblab/pi-telegram/docs/compact-matrix-literal.md +1 -1
  141. package/node_modules/@llblab/pi-telegram/docs/generative-apps.md +1 -1
  142. package/node_modules/@llblab/pi-telegram/docs/outbound.md +8 -8
  143. package/node_modules/@llblab/pi-telegram/docs/public-api.md +2 -2
  144. package/node_modules/@llblab/pi-telegram/lib/outbound-buttons.ts +17 -15
  145. package/node_modules/@llblab/pi-telegram/lib/outbound-markup.ts +28 -68
  146. package/node_modules/@llblab/pi-telegram/lib/outbound.ts +3 -3
  147. package/node_modules/@llblab/pi-telegram/lib/prompts.ts +1 -1
  148. package/node_modules/@llblab/pi-telegram/lib/runtime.ts +2 -1
  149. package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +25 -12
  150. package/node_modules/@llblab/pi-telegram/package.json +1 -1
  151. package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/SKILL.md +5 -5
  152. package/package.json +3 -3
@@ -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.
@@ -106,7 +106,7 @@ Only the live cleanup origin uses the approved historical-protection reduction.
106
106
  - `preview` / `replies` / `rendering`: throttled native Rich Markdown draft delivery, native final reply delivery, reply parameters, transport-limit chunking, and remaining Telegram HTML rendering (bold, italic, strikethrough, spoilers, code, links) for bridge-owned UI/compatibility surfaces.
107
107
  - `delivery`: public extension operational-view delivery, active-turn/instance/aggregate/authorized target policy, logical chunk handles, per-target ordering, runtime generation fencing, and the process-local runtime membrane. Its bridge adapter composes the established UI/compat reply renderer with narrow bus-aware Telegram API and ownership ports; it never exposes bot clients or Pi contexts.
108
108
  - `activity`: public normalized Pi lifecycle registration, activity/source identity, assistant segment and reasoning normalization, executed-tool events, non-blocking per-handler queues, delivery contexts, compatibility adapters, and shutdown fencing. Internal publication/command-work and handler drain notifications share the lifecycle-owned captured-session/transport observer with companion queues and API target counters; bindings/root wire those neutral ports into routing's existing leader cleanup scheduler, never into deletion authority. Routing retains busy/unproven-idle delivery hints without early admission, preserves ordinary timers, and lets semantic settlement or post-settlement output drain request fresh checks. The same domain extends assistant-output observation for connected companion projection: eligible completed local/autonomous public segments retain source order and deduplicate event identity. `bindings` assembles observation, authority, sender, and failure-projection ports; routing owns exact delivery authority, outbound composes established transformations and reply delivery, and Bot API domains implement transport. No separate proactive state-machine domain exists.
109
- - `outbound-markup`: top-level assistant action comment/fence parsing, shared JSON/CML grammar, attribute parsing, voice reply planning, and preview/delivery stripping.
109
+ - `outbound-markup`: delimiter-based assistant action comments, top-level button fence parsing, shared JSON/CML grammar, attribute parsing, voice reply planning, and preview/delivery stripping.
110
110
  - `outbound`: outbound text transformations, voice/button artifact delivery, and generated callback actions.
111
111
  - `generative-apps`: managed deterministic application identity, canonical installation and explicit replacement, content-addressed module loading, state timelines, cross-process transition serialization, bounded executable-plus-argv adaptation, `telegram_bind`, and pre-model-queue `app::method` invocation. It does not own Telegram transport, arbitrary shell execution, or the external application adapted by one Generative App.
112
112
  - `outbound-attachments`: `telegram_attach`, queued outbound files, stat/limit checks, ordinary photo/document delivery, and narrow single-artifact Rich Message planning/sending for probe-confirmed photo/video/audio formats. It owns known-failure fallback eligibility and ambiguous-send no-replay classification through structural error contracts without importing Bot API helpers.
@@ -685,7 +685,7 @@ Native typing during compaction follows connected-instance activity rather than
685
685
  - Thread-targeted typing is sent only to the concrete thread. Aggregate `All` mirroring is intentionally omitted because duplicating every keepalive multiplies shared-chat flood pressure. Compaction completion, failure, or timeout stops only a loop actually started by the compaction observer; a pre-existing agent-owned loop remains active. Ownership is sticky across a repeated compaction start, whose shared-loop start reports `false` for the observer's own running loop. Authority loss and shutdown still stop the keyed loop.
686
686
  - Pi `ui_prompt_start` pauses typing while an extension-owned local prompt waits for the operator; `ui_prompt_end` emits the matching Activity boundary and resumes typing whenever agent or compaction work remains unsettled.
687
687
 
688
- At every connected instance `agent_start`, the lifecycle binding starts Telegram's native `…typing` indicator in that instance's assigned target, whether the run came from Telegram, the local TUI, or an autonomous continuation such as Grow Loop. Terminal `Active` remains Telegram-turn-specific; the native indicator answers the separate question of whether the instance is doing agent work. Streaming keepalives reuse a valid typing owner with the same session generation, transport and target rather than revoking its pending HTTP/IPC action. Fresh context wrappers may reuse that owner within a proven generation; without generation evidence, context identity must match. Explicit queue dispatch and Telegram prompt handoff still replace the owner. Queue dispatch carries the queued turn's exact target into typing before `agent_start`, rather than reducing it to a chat ID. In Threaded Mode, missing or invalid Thread targets are refused before starting a loop or invoking the API; mode changes also invalidate a captured Classic chat-only loop. A rejected start never replaces an existing valid Thread loop. The starter also requires the captured Pi context and session generation to remain current at start, queued send, tick and late failure boundaries, even if the transport epoch is unchanged; replacing a context object in place cannot revive old activity. Message activity hooks capture generation across awaited preview work, and agent-start bindings recheck it before post-await activity/typing, so an obsolete finally or continuation cannot acquire replacement ownership. The typing sender carries its captured loop/context/generation/transport assertion as process-local call authority: direct API issuance rechecks it after queued work, and the follower caller rechecks it after registration waits and immediately before socket issuance without putting callbacks in IPC data. Automatic typing uses one HTTP attempt with no transport-family fallback or delayed replay, including forwarded calls; a future keepalive is a fresh owner-checked attempt. An already issued HTTP/RPC cannot be retroactively unsent. Each loop refreshes its exact target every three seconds and keeps one action in flight. The leader API runtime coalesces identical chat/thread/action calls for two seconds, admits different Threads independently, and shares a real Telegram 429 `retry_after` fence across every Thread key in that chat. A pending-request count retains that shared state until every concurrent action settles, so completion of a faster peer cannot orphan a late 429; expired gates prune opportunistically and each gate family retains at most 256 active keys. Suppression never schedules a delayed retry. A failed typing action remains a structured diagnostic but cannot project `error` onto an otherwise healthy connected/leader/follower status. Assistant message start/update hooks still re-arm typing during Telegram-owned turns so transient provider/model errors do not leave a continuing run without activity feedback, and agent/session completion stops it.
688
+ At every connected instance `agent_start`, the lifecycle binding starts Telegram's native `…typing` indicator in that instance's assigned target, whether the run came from Telegram, the local TUI, or an autonomous continuation such as Grow Loop. Terminal `Active` remains Telegram-turn-specific; the native indicator answers the separate question of whether the instance is doing agent work. Streaming keepalives reuse a valid typing owner with the same session generation, transport and target rather than revoking its pending HTTP/IPC action. Fresh context wrappers may reuse that owner within a proven generation; without generation evidence, context identity must match. Explicit queue dispatch and Telegram prompt handoff still replace the owner. Queue dispatch carries the queued turn's exact target into typing before `agent_start`, rather than reducing it to a chat ID. In Threaded Mode, missing or invalid Thread targets are refused before starting a loop or invoking the API; mode changes also invalidate a captured Classic chat-only loop. A rejected start never replaces an existing valid Thread loop. The starter also requires the captured Pi context and session generation to remain current at start, queued send, tick and late failure boundaries, even if the transport epoch is unchanged; replacing a context object in place cannot revive old activity. Message activity hooks capture generation across awaited preview work, and agent-start bindings recheck it before post-await activity/typing, so an obsolete finally or continuation cannot acquire replacement ownership. The typing sender carries its captured loop/context/generation/transport assertion as process-local call authority: direct API issuance rechecks it after queued work, and the follower caller rechecks it after registration waits and immediately before socket issuance without putting callbacks in IPC data. Automatic typing uses one HTTP attempt with no transport-family fallback or delayed replay, including forwarded calls; a future keepalive is a fresh owner-checked attempt. An already issued HTTP/RPC cannot be retroactively unsent. An ended loop supplies a process-local cancellation observation: only a typed API authority refusal with no underlying failure and paired with that proof is omitted from API failure logs, while the call still rejects without success or retry. If authority expires while handling a real HTTP/network failure, the authority error retains that failure as its cause and the API recorder logs the original failure. Active-owner refusals and HTTP/network failures keep their diagnostics; the cancellation observation never crosses IPC. Each loop refreshes its exact target every three seconds and keeps one action in flight. The leader API runtime coalesces identical chat/thread/action calls for two seconds, admits different Threads independently, and shares a real Telegram 429 `retry_after` fence across every Thread key in that chat. A pending-request count retains that shared state until every concurrent action settles, so completion of a faster peer cannot orphan a late 429; expired gates prune opportunistically and each gate family retains at most 256 active keys. Suppression never schedules a delayed retry. A failed typing action remains a structured diagnostic but cannot project `error` onto an otherwise healthy connected/leader/follower status. Assistant message start/update hooks still re-arm typing during Telegram-owned turns so transient provider/model errors do not leave a continuing run without activity feedback, and agent/session completion stops it.
689
689
 
690
690
  ### Rendering And Delivery
691
691
 
@@ -714,14 +714,14 @@ Outbound files staged during an active Telegram turn are delivered after that tu
714
714
 
715
715
  The channel-post journal records only this agent path's own publication intent. `prepare` is exact and idempotent; `beginPublication` durably changes one prepared operation to outcome-unknown before any future non-idempotent send; only `confirmPublished` records the returned numeric channel/message identity. A retry never regains issuance from outcome-unknown or published state. The store validates profile and token fingerprint, refuses malformed/duplicate/over-capacity state, and lists newest retained records without claiming Telegram history. The public-`@username` direct-leader sender now uses the tool-call ID to prepare and begin this journal transition before `sendRichMessage`, then records the returned numeric channel/message identity plus bounded username/title observed by `getChat`. Retained outcome-unknown state refuses automatic replay. Bounded agent-facing listing is available locally. Exact edit/delete journal transitions fence each tool-call mutation as outcome-unknown before direct-leader `editMessageText` or `deleteMessage`, and require the same mutation ID to confirm edited or deleted state. Numeric-channel publication requires explicit `channel: true`; direct-leader `getChat` must prove the exact negative ID and channel type before issuance, and the send response must preserve that identity. Cross-process regressions prove one winner for concurrent publication/edit/delete issuance and no restart regrant. Reads fail closed before parsing links, foreign/loose files, unsupported no-follow platforms, identity races, or oversized input. No legacy post schema is migrated: absent state starts empty and unknown versions/fields fail closed. The 0.44 downgrade checker leaves this inert journal untouched because old code cannot issue its effects. Only an explicit successful list returns retained authored Markdown. Channel publication/list/mutation failures and runtime events use fixed messages without retained content, token, path, or arbitrary transport detail. Production-helper regressions model a lost successful caller ACK and an ambiguous send response: the same operation returns retained success or refuses outcome-unknown without a second transport call. The retry uses a freshly opened replacement store, proving durable behavior across helper replacement/restart. A native entrypoint regression disconnects/reconnects direct ownership and proves both retained success and ambiguous outcome avoid a second `sendRichMessage`. Sender-rights and live rollout evidence remain separate 0.45.0 work. Channel media posts upload one local `.jpg`/`.jpeg`/`.png`/`.webp` photo or `.mp4` video through the multipart transport with `text` as the HTML caption; kind, byte size (photo ≤ 10 MiB, video ≤ 50 MiB), and 1024 visible caption characters are validated before issuance, and unsupported types or albums are rejected rather than downgraded to links. The journal binds the inspected kind/file name/byte size/SHA-256 media identity and caption, so duplicate requests and lost acknowledgements never re-upload. A media-post edit replaces the caption through `editMessageCaption` with the same Markdown-to-HTML rendering, where `||spoiler||` now renders as `<tg-spoiler>`.
716
716
 
717
- Assistant-authored final-message actions use hidden top-level comments, with an additional fenced wrapper for in-body buttons:
717
+ Assistant-authored final-message actions use hidden HTML comments, with an additional fenced wrapper for in-body buttons:
718
718
 
719
719
  - `telegram_voice` accepts one positional compact action cell or JSON object and creates one voice artifact through configured outbound handlers, programmatic voice handlers, or registered synthesis providers.
720
720
  - `telegram_button` accepts a JSON object, adaptive JSON/CML matrix, or positional Compact Matrix Literal. Named JSON objects and positional cells may coexist, with commas optional only between completed matrix or row elements. Each top-level cell creates one full-width row, while a nested row groups buttons horizontally without an artificial parser-width cap; every callback enqueues its configured prompt text as a normal Telegram prompt turn. The JSON-first grammar, positional trim/escape rules, atomic rejection, and renderer-owned width policy are specified in [Adaptive Button Literal](./compact-matrix-literal.md).
721
721
 
722
- Standalone column-zero triple-backtick `telegram_button` blocks reuse the same cell/matrix grammar and callback store, including adjacent complete cells and matrices whose normalized rows concatenate in source order, and compile to native Rich Markdown button rows between paragraphs. Rendering validates each complete block before registering any callbacks, escapes label text, preserves disabled cells, and leaves larger enclosing fences literal. Previews hide complete and unfinished action fences. HTML mode projects those controls into the footer. In-body callbacks acknowledge without rewriting the Rich message; selected-style highlighting remains footer-only.
722
+ Standalone `telegram_button` fenced blocks reuse the same cell/matrix grammar and callback store, including adjacent complete cells and matrices whose normalized rows concatenate in source order, and compile to native Rich Markdown button rows between paragraphs. Rendering validates each complete block before registering any callbacks, escapes label text, preserves disabled cells, and leaves enclosing fenced examples literal. Opening and closing lines allow spaces/tabs; openings accept three or more backticks or tildes, optional whitespace before the exact `telegram_button` name, and trailing whitespace. Closures must use the same marker and at least the opening length, with no other text. Previews hide complete and unfinished action fences. HTML mode projects those controls into the footer. In-body callbacks acknowledge without rewriting the Rich message; selected-style highlighting remains footer-only.
723
723
 
724
- Action recognition remains restricted to top-level column-zero comments and exact-name button fences so nested examples cannot trigger voice, buttons, or callbacks. The Telegram surface independently strips every complete assistant-authored HTML comment from previews and final delivery regardless of Markdown position or comment owner; an unclosed comment is withheld through the remaining tail, and a comment-only result sends no text message. Pi's terminal transcript and model context remain unchanged.
724
+ Complete HTML action comments activate solely by their delimiters and accepted payload, independent of whitespace, adjacent text, lists, quotes or code fences. Same-line comments are independent. Escape the opening delimiter as `&lt;!--` to show a literal example; backticks and backslashes do not disable an action comment. Exact-name button fences retain their separate top-level recognition rules. The Telegram surface independently strips every complete assistant-authored HTML comment from previews and final delivery regardless of Markdown position or comment owner; an unclosed comment is withheld through the remaining tail, and a comment-only result sends no text message. Pi's terminal transcript and model context remain unchanged.
725
725
 
726
726
  Unknown callback data outside owned prefixes is forwarded as `[callback] <data>` only after built-in and extension handlers decline it.
727
727
 
@@ -154,7 +154,7 @@ For `telegram_button` hidden comments and fenced blocks:
154
154
  - Nested rows become horizontal keyboard rows; horizontal intent still requires the explicit nested array.
155
155
  - Invalid payloads are stripped with their recognized action comment and register no callbacks.
156
156
 
157
- The wrapper selects placement without changing cell semantics. A hidden `telegram_button` HTML comment builds the footer keyboard. A standalone column-zero fenced block opened by exactly three backticks plus `telegram_button` renders rows between paragraphs in Native Rich Markdown. A singleton JSON/CML object needs no array in either wrapper. Fenced content must be one complete group sequence without trailing envelope text; any invalid group rejects the entire block before callback registration. Larger outer fences and ordinary code blocks remain literal examples; unclosed action fences are withheld. Native Rich rows support at most eight buttons and must fit one message chunk; these are renderer constraints, not grammar limits. HTML compatibility places fenced controls in the footer. See [Outbound](./outbound.md) for delivery and callback behavior.
157
+ The wrapper selects placement without changing cell semantics. A hidden `telegram_button` HTML comment builds the footer keyboard. A standalone fenced block opened by at least three backticks or tildes plus the exact name `telegram_button` renders rows between paragraphs in Native Rich Markdown. Delimiter lines accept horizontal indentation, optional whitespace before the name, and trailing spaces/tabs. Close on a separate line using the same marker and at least the opening length, with no other text. Enclosing code fences keep button-block examples literal. A singleton JSON/CML object needs no array in either wrapper. Fenced content must be one complete group sequence without trailing envelope text; any invalid group rejects the entire block before callback registration. Larger outer fences and ordinary code blocks remain literal examples; unclosed action fences are withheld. Native Rich rows support at most eight buttons and must fit one message chunk; these are renderer constraints, not grammar limits. HTML compatibility places fenced controls in the footer. See [Outbound](./outbound.md) for delivery and callback behavior.
158
158
 
159
159
  ````markdown
160
160
  Description.