@llblab/pi-telegram 0.52.2 → 0.53.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 (204) hide show
  1. package/AGENTS.md +9 -7
  2. package/BACKLOG.md +1 -18
  3. package/CHANGELOG.md +15 -0
  4. package/README.md +7 -11
  5. package/api/commands.ts +3 -0
  6. package/dist/api/commands.d.ts +1 -1
  7. package/dist/lib/activity-verbosity.d.ts +1 -5
  8. package/dist/lib/activity-verbosity.js +15 -42
  9. package/dist/lib/activity.d.ts +9 -0
  10. package/dist/lib/activity.js +39 -10
  11. package/dist/lib/agent-messages.js +7 -3
  12. package/dist/lib/bindings.d.ts +43 -5
  13. package/dist/lib/bindings.js +163 -20
  14. package/dist/lib/bus-api.d.ts +12 -0
  15. package/dist/lib/bus-api.js +182 -79
  16. package/dist/lib/bus-follower.d.ts +111 -31
  17. package/dist/lib/bus-follower.js +1324 -359
  18. package/dist/lib/bus-leader.d.ts +41 -91
  19. package/dist/lib/bus-leader.js +969 -637
  20. package/dist/lib/bus-transport.js +16 -9
  21. package/dist/lib/bus.d.ts +215 -18
  22. package/dist/lib/bus.js +1048 -327
  23. package/dist/lib/channel-posts.d.ts +5 -0
  24. package/dist/lib/channel-posts.js +434 -261
  25. package/dist/lib/command-templates.d.ts +26 -5
  26. package/dist/lib/command-templates.js +92 -176
  27. package/dist/lib/commands.d.ts +168 -82
  28. package/dist/lib/commands.js +1409 -291
  29. package/dist/lib/config.d.ts +1 -8
  30. package/dist/lib/config.js +34 -76
  31. package/dist/lib/delivery.d.ts +2 -2
  32. package/dist/lib/delivery.js +16 -16
  33. package/dist/lib/extension.js +639 -171
  34. package/dist/lib/generative-app-worker.mjs +19 -7
  35. package/dist/lib/generative-apps.d.ts +0 -5
  36. package/dist/lib/generative-apps.js +61 -49
  37. package/dist/lib/inbound.d.ts +5 -0
  38. package/dist/lib/inbound.js +35 -59
  39. package/dist/lib/journal.d.ts +33 -3
  40. package/dist/lib/journal.js +2063 -782
  41. package/dist/lib/lifecycle.d.ts +2 -6
  42. package/dist/lib/lifecycle.js +75 -58
  43. package/dist/lib/locks.d.ts +29 -3
  44. package/dist/lib/locks.js +289 -108
  45. package/dist/lib/logging.js +50 -14
  46. package/dist/lib/media.d.ts +22 -1
  47. package/dist/lib/media.js +46 -28
  48. package/dist/lib/menu-model.d.ts +19 -5
  49. package/dist/lib/menu-model.js +53 -11
  50. package/dist/lib/menu-queue.d.ts +5 -4
  51. package/dist/lib/menu-queue.js +45 -63
  52. package/dist/lib/menu-settings.d.ts +5 -4
  53. package/dist/lib/menu-settings.js +114 -78
  54. package/dist/lib/menu-status.d.ts +1 -1
  55. package/dist/lib/menu-status.js +8 -25
  56. package/dist/lib/menu-thinking.d.ts +6 -1
  57. package/dist/lib/menu-thinking.js +14 -21
  58. package/dist/lib/menu.d.ts +8 -61
  59. package/dist/lib/menu.js +92 -164
  60. package/dist/lib/model.d.ts +1 -1
  61. package/dist/lib/model.js +2 -16
  62. package/dist/lib/outbound-attachments.d.ts +6 -27
  63. package/dist/lib/outbound-attachments.js +45 -38
  64. package/dist/lib/outbound-buttons.js +22 -16
  65. package/dist/lib/outbound-markup.d.ts +0 -2
  66. package/dist/lib/outbound-markup.js +51 -64
  67. package/dist/lib/outbound-voice.js +6 -3
  68. package/dist/lib/outbound.d.ts +6 -6
  69. package/dist/lib/outbound.js +65 -51
  70. package/dist/lib/ownership.js +6 -2
  71. package/dist/lib/paths.d.ts +5 -0
  72. package/dist/lib/paths.js +37 -21
  73. package/dist/lib/pi.d.ts +2 -0
  74. package/dist/lib/pi.js +8 -1
  75. package/dist/lib/polling.d.ts +21 -37
  76. package/dist/lib/polling.js +59 -50
  77. package/dist/lib/preview.js +39 -19
  78. package/dist/lib/process-identity.d.ts +24 -0
  79. package/dist/lib/process-identity.js +133 -0
  80. package/dist/lib/prompts.js +2 -7
  81. package/dist/lib/queue.d.ts +14 -16
  82. package/dist/lib/queue.js +252 -127
  83. package/dist/lib/recovery.d.ts +2 -83
  84. package/dist/lib/recovery.js +20 -274
  85. package/dist/lib/replies.d.ts +16 -24
  86. package/dist/lib/replies.js +99 -116
  87. package/dist/lib/routing.d.ts +120 -7
  88. package/dist/lib/routing.js +4667 -1152
  89. package/dist/lib/runtime.d.ts +1 -11
  90. package/dist/lib/sections.d.ts +4 -29
  91. package/dist/lib/sections.js +7 -20
  92. package/dist/lib/skills.js +4 -3
  93. package/dist/lib/status.js +86 -57
  94. package/dist/lib/sync.d.ts +1 -3
  95. package/dist/lib/sync.js +67 -43
  96. package/dist/lib/target.d.ts +4 -0
  97. package/dist/lib/target.js +18 -0
  98. package/dist/lib/telegram-api.d.ts +18 -13
  99. package/dist/lib/telegram-api.js +148 -53
  100. package/dist/lib/text-groups.js +4 -39
  101. package/dist/lib/thread-cleanup-manager.d.ts +6 -27
  102. package/dist/lib/thread-cleanup-manager.js +369 -186
  103. package/dist/lib/thread-display.d.ts +1 -2
  104. package/dist/lib/thread-display.js +58 -38
  105. package/dist/lib/thread-naming.d.ts +28 -8
  106. package/dist/lib/thread-naming.js +175 -66
  107. package/dist/lib/thread-reconciler.d.ts +42 -16
  108. package/dist/lib/thread-reconciler.js +90 -50
  109. package/dist/lib/threads.d.ts +82 -20
  110. package/dist/lib/threads.js +2247 -855
  111. package/dist/lib/time-injection.js +1 -2
  112. package/dist/lib/turns.js +1 -19
  113. package/dist/lib/updates.d.ts +169 -80
  114. package/dist/lib/updates.js +2418 -641
  115. package/dist/lib/voice.d.ts +7 -22
  116. package/dist/lib/voice.js +7 -22
  117. package/dist/lib/wire.js +1 -1
  118. package/dist/lib/workspace-admission.d.ts +3 -1
  119. package/dist/lib/workspace-admission.js +425 -211
  120. package/dist/lib/workspace-identity.js +8 -13
  121. package/dist/lib/workspace-retirement.d.ts +1 -1
  122. package/dist/lib/workspace-retirement.js +328 -140
  123. package/dist/lib/workspace-slots.js +8 -4
  124. package/dist/package.json +1 -1
  125. package/docs/architecture.md +31 -29
  126. package/docs/inbound.md +1 -1
  127. package/docs/multi-instance-bus.md +15 -3
  128. package/docs/outbound.md +1 -1
  129. package/docs/public-api.md +29 -1
  130. package/docs/ui-style.md +25 -8
  131. package/lib/activity-verbosity.ts +59 -82
  132. package/lib/activity.ts +146 -102
  133. package/lib/agent-messages.ts +12 -5
  134. package/lib/bindings.ts +389 -82
  135. package/lib/bus-api.ts +263 -102
  136. package/lib/bus-follower.ts +2440 -601
  137. package/lib/bus-leader.ts +2209 -1290
  138. package/lib/bus-transport.ts +46 -12
  139. package/lib/bus.ts +1673 -444
  140. package/lib/channel-posts.ts +848 -327
  141. package/lib/command-templates.ts +152 -265
  142. package/lib/commands.ts +2398 -555
  143. package/lib/config.ts +177 -170
  144. package/lib/delivery.ts +41 -29
  145. package/lib/extension.ts +1206 -433
  146. package/lib/generative-app-worker.mjs +19 -7
  147. package/lib/generative-apps.ts +324 -211
  148. package/lib/inbound.ts +95 -97
  149. package/lib/journal.ts +5311 -1763
  150. package/lib/keyboard.ts +1 -3
  151. package/lib/lifecycle.ts +128 -78
  152. package/lib/locks.ts +677 -203
  153. package/lib/logging.ts +147 -58
  154. package/lib/media.ts +82 -43
  155. package/lib/menu-model.ts +85 -18
  156. package/lib/menu-queue.ts +88 -129
  157. package/lib/menu-settings.ts +260 -155
  158. package/lib/menu-status.ts +20 -60
  159. package/lib/menu-thinking.ts +41 -56
  160. package/lib/menu.ts +112 -285
  161. package/lib/model.ts +8 -22
  162. package/lib/outbound-attachments.ts +156 -125
  163. package/lib/outbound-buttons.ts +76 -49
  164. package/lib/outbound-markup.ts +96 -94
  165. package/lib/outbound-voice.ts +33 -27
  166. package/lib/outbound.ts +128 -125
  167. package/lib/ownership.ts +19 -10
  168. package/lib/paths.ts +148 -49
  169. package/lib/pi.ts +35 -15
  170. package/lib/polling.ts +144 -147
  171. package/lib/preview.ts +93 -40
  172. package/lib/process-identity.ts +164 -0
  173. package/lib/prompts.ts +4 -11
  174. package/lib/queue.ts +516 -301
  175. package/lib/recovery.ts +50 -454
  176. package/lib/rendering.ts +1 -3
  177. package/lib/replies.ts +248 -209
  178. package/lib/routing.ts +8509 -2186
  179. package/lib/runtime.ts +1 -12
  180. package/lib/sections.ts +24 -89
  181. package/lib/setup.ts +1 -3
  182. package/lib/skills.ts +9 -4
  183. package/lib/status.ts +215 -110
  184. package/lib/sync.ts +448 -361
  185. package/lib/target.ts +22 -3
  186. package/lib/telegram-api.ts +302 -127
  187. package/lib/text-groups.ts +5 -39
  188. package/lib/thread-cleanup-manager.ts +833 -331
  189. package/lib/thread-display.ts +213 -97
  190. package/lib/thread-naming.ts +232 -87
  191. package/lib/thread-reconciler.ts +155 -72
  192. package/lib/threads.ts +5318 -1845
  193. package/lib/time-injection.ts +1 -4
  194. package/lib/turns.ts +15 -28
  195. package/lib/updates.ts +5184 -1612
  196. package/lib/voice.ts +7 -35
  197. package/lib/wire.ts +1 -1
  198. package/lib/workspace-admission.ts +996 -416
  199. package/lib/workspace-identity.ts +14 -17
  200. package/lib/workspace-retirement.ts +878 -376
  201. package/lib/workspace-slots.ts +22 -13
  202. package/package.json +1 -1
  203. package/scripts/check-downgrade.mjs +0 -472
  204. package/scripts/measure-workspace.mjs +0 -108
package/AGENTS.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## 0. Meta-Protocol Principles
4
4
 
5
- - `Mobile companion boundary`: Telegram extends a running Pi session; it is not a remote terminal, PTY supervisor, process launcher, session browser, or replacement TUI. Never emulate Pi navigation through private internals, ANSI/TTY injection, or a shadow `pi` process.
5
+ - `Mobile companion boundary`: Telegram extends a running Pi session; it is not a remote terminal, PTY supervisor, process launcher, session browser, or replacement TUI. Never emulate Pi navigation through private internals, ANSI/TTY injection, or a shadow `pi` process. Built-in Telegram controls manage work inside a running session; Pi process termination stays a local Pi/TUI operation, not a terminal command to mirror.
6
6
  - `Runtime safety`: Prefer explicit, fenced, recoverable behavior over shortcuts that can desynchronize Telegram transport, durable admission, local queue state, or Pi lifecycle state.
7
7
  - `Pi baseline`: Require Pi ≥1.0.0 across coding-agent, agent-core and AI peers; keep locks and public API compatibility tests aligned without weakening live acceptance gates.
8
8
  - `Pi-native extensibility`: Add capabilities through stable Pi and pi-telegram contracts. Do not fork polling, transport, menu ownership, or package-private runtime internals.
@@ -27,7 +27,7 @@ Canonical terms:
27
27
 
28
28
  Keep each fact in one authoritative layer:
29
29
 
30
- - [`README.md`](./README.md): Public product entrypoint. Preserve the flow identity → install/connect → examples → product model → compact capabilities → controls/safety → docs. Balance strong positioning with a practical catalogue; neither hide capabilities nor duplicate implementation docs.
30
+ - [`README.md`](./README.md): Public product entrypoint. Preserve the flow identity → install/connect → examples → product model → compact capabilities → controls/safety → docs. Balance strong positioning with a practical catalogue of capabilities, operator gestures and meaningful consequences; keep detailed routing proofs, recovery algorithms and action-markup grammar in indexed `/docs` instead of accumulating release-by-release paragraphs.
31
31
  - `AGENTS.md`: Stable engineering boundaries, recurring runtime invariants, and work protocol. Link to evolving subsystem contracts instead of copying them here.
32
32
  - [`BACKLOG.md`](./BACKLOG.md): Canonical unresolved work. Keep only open top-level outcomes with nested decomposition and done criteria. Remove completed outcomes rather than retaining checked history.
33
33
  - [`CHANGELOG.md`](./CHANGELOG.md): Completed user/operator/developer impact. A release has at most eight outcome bullets of at most 512 characters, each beginning with an inline-code domain label and colon. Exclude personal names and real user/chat/message/thread identifiers. Consolidate the current pre-release section before release; do not rewrite historical sections without an explicit retrospective request and evidence pass.
@@ -77,7 +77,7 @@ Use the relevant local skill before non-trivial work in its domain. Keep skill o
77
77
  - Threaded Mode has exactly one live leader per bot profile. Followers are real operator-started Pi processes and must authenticate/register over local IPC; Telegram never spawns hidden Pi processes. A live but unreachable owner does not authorize split-brain polling.
78
78
  - Local IPC is a trust boundary, not merely a private socket. Unknown, stale, mismatched-generation, or unauthorized requests must not inject prompts, callbacks, API sends, artifacts, liveness, or bindings. Follower registration identity may prepare a binding, but inbound generation authority is published only after successful preparation for the same generation and current context; pending or failed startup cannot append into a retained old journal. Readiness also binds the active Pi context and supplied session generation. Same-session refresh awaits binding preparation without re-registering; a changed session ID cannot publish readiness until its exact leader registration is acknowledged. Reusing a context object cannot carry readiness across a session-generation or ID change. Registration requests capture session authority before asynchronous startup and fence every publication/finalization against the current attempt. Stop or supersession invalidates that attempt; obsolete cleanup cannot erase a newer registration or a refreshed context.
79
79
  - Protocol compatibility is independent from package version. Registration negotiates protocol version, runtime build, and canonical capabilities before target provisioning or live publication. `durable-follower-admission-v1` gates source forwarding; `queue-handoff-v1` independently gates semantic queue transfer for every participant and is advertised only with exact source/recipient journal-binding composition. `follower.register` and capability-gated restore-only `follower.restoreWorkspace` are bootstrap requests; other requests require exact live-registry generation authority, and `bus.ack` is response-only. `thread-display-mode-v1` gates follower display-setting requests; the leader owns their serialized profile preference and title application.
80
- - Long-lived timers, pollers, watchers, receivers, heartbeats, background delivery, and deferred dispatch are session-bound. Shutdown cancels/drains diagnostics publications and fences deferred status reads before they can acquire guards or repair files. Replacement stops stale activity and makes late work inert; teardown must recheck the captured session generation even when context identity is reused. Same-process handoff may preserve exact profile/target identity but never stale Pi context or cross-profile authority. Follower heartbeat response deadlines align with the eight-second leader stale-liveness window rather than the generic one-second local-RPC default; an ordinary multi-second Pi/TUI event-loop stall is not registration loss. Participating source observations must hold a reference for the actual read, including pending-mutation/count queries against a stopped worker's retained source; a scoped observation never restores receipt readiness or execution authority. A donor cancellation resumed after remote handoff awaits must also hold a source reference; only the existing exact journal CAS may cancel, never undo accepted recipient custody. Stable source keys do not certify captured callable lifetimes: snapshot prepared worker capabilities at construction and replace them on source-handle renewal without replaying unsettled input. Aborting a durable update generation does not release that `update_id`: replacement replay waits for its actual handler settlement, and effectful handlers use the shared execution fence immediately before commit and after awaited delegation. Admission also rechecks that fence after the default handler returns, before its outcome can settle custody; a stopped handler's ordinary return is not completion authority. Internal clones explicitly carry the hidden fence; reroute forwarding, thread-store mutation, cleanup, and Bot API boundaries retain the originating generation.
80
+ - Long-lived timers, pollers, watchers, receivers, heartbeats, background delivery, and deferred dispatch are session-bound. Shutdown cancels/drains diagnostics publications and fences deferred status reads before they can acquire guards or repair files. Replacement stops stale activity and makes late work inert; teardown must recheck the captured session generation even when context identity is reused. Same-process handoff may preserve exact profile/target identity but never stale Pi context or cross-profile authority. Follower heartbeat and leader-to-follower Restore/live-rebind control reply deadlines align with the eight-second leader stale-liveness window rather than the generic one-second local-RPC default; an ordinary multi-second Pi/TUI event-loop stall or a slower Windows named pipe is not registration or recipient loss. Participating source observations must hold a reference for the actual read, including pending-mutation/count queries against a stopped worker's retained source; a scoped observation never restores receipt readiness or execution authority. A donor cancellation resumed after remote handoff awaits must also hold a source reference; only the existing exact journal CAS may cancel, never undo accepted recipient custody. Stable source keys do not certify captured callable lifetimes: snapshot prepared worker capabilities at construction and replace them on source-handle renewal without replaying unsettled input. Aborting a durable update generation does not release that `update_id`: replacement replay waits for its actual handler settlement, and effectful handlers use the shared execution fence immediately before commit and after awaited delegation. Admission also rechecks that fence after the default handler returns, before its outcome can settle custody; a stopped handler's ordinary return is not completion authority. Internal clones explicitly carry the hidden fence; reroute forwarding, thread-store mutation, cleanup, and Bot API boundaries retain the originating generation.
81
81
  - Runtime state is event-driven reconciliation of local assumptions against Telegram signals, not a complete bot read-model and not permission to query Telegram on every action. Destructive thread cleanup goes through `thread-reconciler` with current proof and leader fencing. Fresh Workspace Thread creation derives its initial Bot API title from the active display mode before issuance; the stable generated `threadName` remains separate from the acknowledged `displayTitle`.
82
82
 
83
83
  ### 4.3 Durable Admission And Settlement
@@ -89,10 +89,10 @@ Use the relevant local skill before non-trivial work in its domain. Keep skill o
89
89
  - Foreign forwarding settles as `accepted`, `retryable`, or `terminal-rejected`. Only an authenticated acknowledgement carrying the expected `deliveryId` and `sourceUpdateId` releases leader journal authority. Negative, missing, stale, mismatched, or capacity-failed settlement remains durable; callback error answers are side effects only. Exception: the operator-approved fixed chooser deadline may discard its exact unclaimed donor pending source without a body archive or delivery-success claim. Accepted recipient queue/work and bound Threads are untouched; old donor controls and late reports cannot replay it. The lifetime and temporary cleanup contract lives in `docs/architecture.md`.
90
90
  - A forwarding delivery id is stable across registration replacement and derives from envelope kind, source `update_id`, and stable recipient binding. Runtime instance and registration generation remain separate attempt fences. Persisted message ownership carries the stable binding so replay can rebind only to its current authenticated registration.
91
91
  - A queued receipt persists its acquiring runtime instance, OS pid/process-birth identity, session generation, acquisition id, and acquisition time. Only exact authority may settle or discard it. Cached presence is not current execution proof: prepared custody must revalidate the exact queued owner/group without recovery and refuse offers or uncertain reads. Completion requires an exact removal acknowledgement, never merely `!ready`; a retained acknowledgement permits local cleanup only, not replay. Same-process session replacement may reconstruct the claim and the original process may settle after transport ownership moves; a foreign process may neither replay nor settle it through generic removal or a copied acquisition id.
92
- - Startup and elapsed time are not owner-death proof; queued authority has no time lease. Dead-owner cleanup groups the complete receipt and transactionally rechecks pid liveness plus process-birth identity: only an absent PID or mismatched stable Linux/macOS birth proof discards all session-owned sources without replay; a matching proof is `alive`, while Windows or inaccessible birth metadata is `unverifiable`, and both non-dead outcomes keep authority queued. Under actual `A`–`Z` allocation pressure, retirement may invoke that journal-owned CAS only for a current inactive binding after complete strict source inspection, no local work/live owner/delivery authority, whole unoffered receipt groups, and a preflight proving every grouped owner dead. It must then recapture all protection before preparing deletion; partial progress never grants deletion authority. The live-transfer contract is authenticated offer → exact-generation bounded payload staging → recipient CAS acceptance → exact receipt-and-owner ACK → donor removal → recipient readiness. The offer freezes donor settlement/recovery; controls rebuild local closures; negative/mismatched pre-acceptance ACK cancels only an unaccepted offer and retains donor work; a lost post-acceptance ACK cannot cancel recipient authority and leaves donor memory frozen for explicit reconciliation.
92
+ - Startup and elapsed time are not owner-death proof; queued authority has no time lease. Dead-owner cleanup groups the complete receipt and transactionally rechecks pid liveness plus process-birth identity: only an absent PID or mismatched stable Linux/macOS/Windows birth proof discards all session-owned sources without replay; a matching proof is `alive`, while inaccessible birth metadata is `unverifiable`, and both non-dead outcomes keep authority queued. Under actual `A`–`Z` allocation pressure, retirement may invoke that journal-owned CAS only for a current inactive binding after complete strict source inspection, no local work/live owner/delivery authority, whole unoffered receipt groups, and a preflight proving every grouped owner dead. It must then recapture all protection before preparing deletion; partial progress never grants deletion authority. The live-transfer contract is authenticated offer → exact-generation bounded payload staging → recipient CAS acceptance → exact receipt-and-owner ACK → donor removal → recipient readiness. The offer freezes donor settlement/recovery; controls rebuild local closures; negative/mismatched pre-acceptance ACK cancels only an unaccepted offer and retains donor work; a lost post-acceptance ACK cannot cancel recipient authority and leaves donor memory frozen for explicit reconciliation.
93
93
  - Execution failures persist bounded diagnostics and attempt state as `retry-wait`, except that an exact Telegram HTTP 400 stale/deleted-thread API failure with a proven `{chatId, threadId}` terminally settles the currently executing source after best-effort shared binding invalidation. Automatic retry continues indefinitely with exponential `1s → 2s → 4s → 8s → 16s → 32s → 60s` delay capped at 60 seconds; later independent updates continue draining, durable authority is never silently discarded, and legacy `failed` entries resume automatically at startup. Snapshot-plus-segment journals compact only after 256 unapplied revisions or 4 MiB; snapshot-first cleanup tolerates redundant segments, and empty authority may atomically rebind bot/profile identity. Missing snapshots left by the retired broad temp cleanup rebuild only from a complete provably empty segment chain, while revisionless snapshots may recover from a validated later segment predecessor; otherwise the transaction-locked reset deletes damaged segments before publishing a fresh journal, with accepted input loss and informational evidence, not a quarantine copy. Unsupported versions remain untouched; strict protection reads never reset. Startup best-effort removes obsolete current-runtime root/session `recovery/` folders without touching pre-0.52.0 storage.
94
94
  - Business connection chats are a separate namespace even when their chat/message IDs match bot-chat IDs. Default DM routing must never infer private-queue deletion intent from `deleted_business_messages`; raw companion handlers remain separate owners.
95
- - Clock-bearing chooser sources survive cold spending until their fixed deadline; new-world forgetting preserves their positively unbound temporary frame without restoring routing intent. Expiry is a donor discard, not recipient cancellation or source execution completion. Confirmed expiry releases worker custody before UI/cleanup awaits; metadata retries retain no prompt bodies and cannot repeat an issued delete.
95
+ - Clock-bearing chooser sources survive cold spending until their fixed deadline. A restarted owner revives one in place only while its saved phase is still `waiting`, its recorded chooser location is known, its tab stays unbound and live threads exist; that waiting phase is the proof no selection or effect ran. Selected, expired or bound sources keep the protective hold, and new-world forgetting preserves their positively unbound temporary frame. Expiry is a donor discard, not recipient cancellation or source execution completion. Confirmed expiry releases worker custody before UI/cleanup awaits; metadata retries retain no prompt bodies and cannot repeat an issued delete.
96
96
  - An unresolved reaction delays only the exact governed queue item identified by chat/message sources, not unrelated queue work. An explicit immutable `preApprovalExcluded: true` cannot govern accepted work, even after re-pairing; missing or false exclusion evidence must not bypass the dependency guard. Prepared v3 drain may dispose of excluded pending input only through journal-owned `removeExcluded`, never generic raw completion; mixed requests containing non-excluded input must fail atomically. Queue receipt publication follows in-memory append and precedes dispatch request; receipt-bearing turns remain queued until every exact source commits.
97
97
  - The detailed implementation and release gates live in [`docs/architecture.md`](./docs/architecture.md), [`docs/multi-instance-bus.md`](./docs/multi-instance-bus.md), and [`BACKLOG.md`](./BACKLOG.md).
98
98
 
@@ -112,7 +112,7 @@ Use the relevant local skill before non-trivial work in its domain. Keep skill o
112
112
 
113
113
  The detailed map is canonical in [`docs/architecture.md`](./docs/architecture.md). This index is only for routing work:
114
114
 
115
- - `queue`, `runtime`, `lifecycle`, `locks`: Scheduling, session coordination, lifecycle, and locking.
115
+ - `queue`, `runtime`, `lifecycle`, `locks`, `process-identity`: Scheduling, session coordination, lifecycle, locking, and process liveness proofs.
116
116
  - `api`, `polling`, `bus*`, `ownership`, `target`, `sync`, `thread-reconciler`, `threads`, `updates`, `routing`, `media`, `turns`, `inbound`, `config`, `setup`: Telegram transport, profiles, durable admission, routing, and inbound flow.
117
117
  - `preview`, `replies`, `rendering`, `keyboard`, `delivery`, `activity`, `outbound*`, `voice`, `status`: Response and delivery surfaces.
118
118
  - `commands`, `menu*`, `model`, `prompts`: Controls and application-menu UI; core queue mechanics remain in `queue`.
@@ -122,6 +122,7 @@ The detailed map is canonical in [`docs/architecture.md`](./docs/architecture.md
122
122
  ## 6. Public And Integration Boundaries
123
123
 
124
124
  - Companion extensions use documented package subpaths such as `@llblab/pi-telegram/sections`, `/delivery`, `/voice`, `/inbound`, `/outbound`, and `/updates`; never import `lib/*.ts`.
125
+ - `Producer-owned interactions`: Extensions that own questions or approvals implement optional Telegram support on their side through public companion APIs and retain normal local behavior when Telegram is unavailable. The producer owns correlation, answer validation, action/target context, authorization, expiry/cancellation and one-shot settlement. Core must not hard-code third-party tool names, schemas or event channels, or generically mirror/resolve arbitrary local UI dialogs; a versioned producer event does not transfer that responsibility. A question answer alone is not approval authority.
125
126
  - Low-level handler buses have no caller-supplied ids; high-level registries use stable identities. Imperative delivery resolves the current runtime on every call and returns generation-bound logical handles rather than captured Pi contexts.
126
127
  - Extension sections receive only documented context ports. They do not access raw bot clients/filesystems or run a second polling loop; unregister on shutdown.
127
128
  - Unknown callback data may reach extension handlers only after built-in namespaces decline it. Follow [`docs/callback-namespaces.md`](./docs/callback-namespaces.md).
@@ -137,7 +138,7 @@ The detailed map is canonical in [`docs/architecture.md`](./docs/architecture.md
137
138
  - Keep interfaces consistent with their owning exported contract. Use local structural `*Like`/view types only for deliberate narrow projections, not duplicate source-of-truth models.
138
139
  - Remove dead code immediately. Reachability from composition roots, public exports, tests, registered surfaces, and documented APIs—not recent usefulness—determines whether code is live. `node scripts/audit-exports.mjs` provides a read-only TypeScript-symbol inventory including aliases, JS consumers and public `api/` reexports (including namespaces); `namespaceEscapes` flags whole-module alias usage. No-external-reference candidates are not deletion proof. Review generated text, dynamic namespace access and public declaration reachability before removing or hiding a symbol.
139
140
  - Treat every meaningful `lib/extension.ts` edit as a composition-pressure check, but keep one-off live adapter wiring there when extraction would only hide cross-domain state.
140
- - Follow [`docs/ui-style.md`](./docs/ui-style.md) for interface copy, emoji, buttons, menus, and dialogs. Update the registry before assigning a new UI emoji meaning. Standalone notices use one fully bold emoji-led sentence with a terminal period; menu or chooser headings use the same hierarchy with a terminal colon. Material names may add nested italic emphasis without breaking the outer bold span. Typed callback toasts preserve equivalent emoji-led plain text without a terminal sentence period; question/exclamation marks and ellipses remain. In-chat notices and explicit raw API payloads keep their punctuation.
141
+ - Follow [`docs/ui-style.md`](./docs/ui-style.md) for interface copy, emoji, buttons, menus, and dialogs. Update the registry before assigning a new UI emoji meaning. Standalone notices use one fully bold emoji-led sentence with a terminal period; menu or chooser headings use the same hierarchy with a terminal colon. Material names may add nested italic emphasis without breaking the outer bold span. Callback toasts are fleeting plain text with no emoji and no terminal sentence period (question/exclamation marks and ellipses remain), written in that final form at their call site; no delivery boundary rewrites punctuation, and navigation taps answer silently. In-chat notices and explicit raw API payloads keep their punctuation.
141
142
  - Markdown lists never contain blank lines between adjacent items; list items are not paragraphs. Use blank lines only between paragraphs or independently separated blocks. Markdown tables use compact source formatting with `---` separator cells and one surrounding space per cell. Preserve vendored references unchanged.
142
143
  - Treat Windows filesystem, named-pipe, lock, heartbeat, and atomic-rename reports as high-signal evidence; reduce them to regressions or explicit platform caveats.
143
144
  - Route significant runtime failures through the redacted recent-event recorder. Keep the compact TUI status at generic `error`; details belong in diagnostics.
@@ -159,6 +160,7 @@ While working:
159
160
  - Read large artifacts search-first and range-bounded. For `CHANGELOG.md`, inspect only the current release section unless older history is relevant.
160
161
  - Keep successful validation output compact; inspect focused failure tails. Prefer focused tests/typecheck during iteration and broad validation at a stable gate.
161
162
  - Preserve unrelated work and do not commit, publish, tag, deploy, or perform external actions without explicit authorization.
163
+ - `Contributor integration`: Integrate accepted contributions into current `dev`, preserve author commits with merge rather than squash/rebase, and add maintainer adaptations separately. Verify actual contributor-branch push permission before editing or retargeting; otherwise use a maintainer branch rooted at the contributor head. Contributor claims do not replace current-baseline regression and CI evidence.
162
164
 
163
165
  Before completion:
164
166
 
package/BACKLOG.md CHANGED
@@ -2,21 +2,4 @@
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
- ## Post-Release Transport Recovery Acceptance
6
-
7
- - Release boundary: The operator authorized the hotfix on native regression and CI evidence; real Telegram smoke/fault acceptance remains open below, not a claimed completed check. Installing/reloading Pi or injecting live faults needs separate authorization.
8
- - CI boundary: Native Linux/macOS/Windows checks exercise capped poll/admission backoff, preserved persistent-conflict stand-down and generation-fenced manual disconnect. Existing skipped strict journal witnesses remain a separate Windows boundary; green CI does not replace operator acceptance.
9
- - [ ] Obtain operator acceptance for ordinary disconnect, unconfirmed-cleanup reporting and outage recovery without manual reconnect on a disposable setup. Local fake-transport/storage regressions do not certify real Telegram or platform fault behavior; installation/reload and live fault injection require separate authorization.
10
- - Acceptance: Captured manual stop never affects a replacement connection or bypasses Thread deletion fences, uncertain stop is not retried, and accepted local work is preserved. Session-restart cleanup remains admission-required.
11
-
12
- ## Windows Reliability
13
-
14
- - [ ] Strengthen native Windows reliability beyond the 0.52.0 ordinary-queue fix. Audit filesystem paths, named pipes, locking, owner fencing, heartbeat and atomic publication; cover interrupted writes, damaged state, ownership changes and lost-ACK retries with focused regressions.
15
- - [ ] Investigate Windows-compatible strict journal evidence for Restore and cleanup without weakening source identity, scoped ACK or fail-closed protections. Inventory witnesses currently skipped on Windows and enable them only when equivalent native evidence exists.
16
- - Acceptance: Local synthetic tests distinguish ordinary delivery from strict proofs; native Windows CI exercises supported paths and refusal boundaries. Linux emulation or a green matrix with skipped Restore witnesses does not establish Windows Restore acceptance.
17
-
18
- ## Consistent Callback Notification Copy Across Other Extensions
19
-
20
- - [ ] Inventory Telegram button callback acknowledgements across all local extensions, including companion callbacks. Standardize the transient non-interactive notification bubbles (callback answers/toasts) to concise, consistent text with no trailing period.
21
- - [ ] Distinguish transient callback bubbles from in-chat notices, chooser headings and blocking alerts; reconcile the owning UI-copy contracts before implementation rather than stripping punctuation from every message.
22
- - Acceptance: All participating extensions follow the same callback-notification format, with focused copy checks for success, no-op and rejection outcomes; in-chat notice formatting and callback behavior remain unchanged.
5
+ No open work.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,21 @@
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.53.1: Text replies without false voice errors
8
+
9
+ - `Voice artifacts`: A reply plan without voice content no longer reaches outbound voice artifact delivery, so plain text replies stop recording "every voice synthesis provider failed" delivery errors; explicit `telegram_voice` markup and genuine synthesis failures keep their existing fallback behavior (#318).
10
+
11
+ ## 0.53.0: Live Thread rebinding, routing tabs and Windows support
12
+
13
+ - `Live-rebind state`: Atomic same-session Workspace binding/operation publication, fenced recipient application and bounded one-shot cleanup scheduling. Failed/unknown cleanup keeps the new binding without blocking the next attempt; chooser retirement keeps saved inputs. Shared-state retries recheck captured authority before every rename, so a revoked grant cannot overwrite retained state.
14
+ - `Live-input preparation`: Fresh recipients hold groups before append; leaders reuse unsettled originals without copy or reexecution. Saved singleton readiness binds only its genuine warm carrier; cold/replaced/discarded sources cannot mint ACKs. Both roles use exact queue scopes, receipt/readiness publication and native completion observation. Caller/worker authority gates release; failed copies use fenced disposal. Queue acceptance is never removal, replay or another queue grant.
15
+ - `Live-rebind peers`: Both-role local wiring preserves exact donor CAS and detached recipient delivery. Non-ordering Commands work protects held rendering/API and unknown effects after disposal/rebinding; cleanup refuses pending/unknown work. Only the explicitly marked, read-only recipient work observation may outlive source custody; generic observation cannot revive it. Authenticated IPC and both-role cleanup witnesses stay distinct from activation and actual Pi/client acceptance.
16
+ - `Staged live routing`: Prompt and supported command selections share save/apply/release through native owners. Albums preserve grouped receipts and unrelated input across busy work/lost replies. Binding/claim conflicts and journal/profile-key drift refuse without replay or rollback; other bindings/profiles stay exact. Templates and `/continue` retain no-fold admission.
17
+ - `Command authority`: Registry plans retain both-role custody, template identity, exact ACKs, no-fold receipts, donor CAS and independent delivery. Retired facades are removed; inspection is private behind `settle`, and ordinary command/prompt dispatch returns one owned function. Control assembly uses Queue contracts/controller and preserves append → report → dispatch. Target projection preserves field absence. Native abort retires heavy receipt-index records to refusal markers, never fallback.
18
+ - `Handler and menu fixes`: Command-template `when` skips a falsy handler (selection falls through) and guarded steps, including enclosing-node guards; `recover` runs only between failed retries. Thinking-menu buttons refuse during voice replies. Every All-tab input, albums included, gets a 🚦 Routing tab; Cancel shows a notice and removes it right away, even if slow or other tabs wait; a consumed menu-picked command also leaves All. Route menus list 🧵 threads; restart revives a waiting chooser.
19
+ - `UI and coverage`: Toasts are plain text without emoji or a final period, sent as written; menu and routing-tab navigation answers silently; list items use `<code>-</code>`. TUI status keeps acknowledged Thread-name casing. Real Pi SDK fixtures cover queued prompts/buttons across held tools and compaction; damaged-state fixtures cover invalid admission and outbound delivery. Unsupported extension selection preserves originals and bindings. Stale downgrade and Thread-store measurement scripts are removed.
20
+ - `Windows`: Restore, live rebinding, cleanup census and scoped queue ACKs run on Windows through the same strict journal reads, guarded by handle identity where no-follow opens are missing. Process birth is proven from creation ticks, so reused PIDs no longer pin dead owners' work. Workspace and app file replacement retries sharing violations, follower control replies get the 8-second liveness window, and CI runs every suite on Windows.
21
+
7
22
  ## 0.52.2: Polling recovery, safe disconnect and Windows downloads
8
23
 
9
24
  - `Polling recovery`: Non-conflict poll/admission failures back off from 1 to 30 seconds and keep retrying, allowing recovery after prolonged outages without manual reconnect. Backoff resets only after durable admission succeeds; persistent competing-client conflicts retain their existing terminal stand-down.
package/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  It is a **runtime adapter**, not a remote terminal. Start or supervise work in the Pi TUI, then continue from Telegram while away from the keyboard. In Threaded Mode, a durable Workspace binding uses Pi's stable public session identity to restore the same Telegram Thread when that session resumes; live instance ownership and the exact Telegram target still authorize routing. The bridge preserves Pi session semantics instead of pretending Telegram is a PTY, shell, process launcher, or session browser. That boundary is the product: Telegram gets safe runtime handles, not raw terminal power.
10
10
 
11
- Every completed intermediate commentary block from a Telegram-originated turn is delivered once as its own message before the existing final reply. While Telegram is connected, local, autonomous, and unclassified extension follow-up work also projects visible checkpoints and the final answer to the authorized Telegram target once and in order, preserving assistant-authored `telegram_button` comments as interactive prompt buttons. This connected companion projection is always active rather than configurable. Neither path mirrors local prompts, thinking, tool traffic, token deltas, or stale-generation work. The separate `Activity` setting defaults to `verbose` so new installations discover collapsed provider-exposed thinking and tool evidence immediately; operators can narrow it to one class or choose `quiet`. See [Outbound](docs/outbound.md#public-assistant-output) and the [configuration reference](docs/public-api.md#configuration-api).
11
+ While connected, Telegram receives intermediate updates and final replies from local Pi work as well as Telegram-originated turns. The `Activity` setting controls whether thinking and tool details are shown. See [Outbound](docs/outbound.md#public-assistant-output) for delivery behavior.
12
12
 
13
13
  This repository is an actively maintained standalone fork of [`badlogic/pi-telegram`](https://github.com/badlogic/pi-telegram). It started from upstream commit [`cb34008`](https://github.com/badlogic/pi-telegram/commit/cb34008460b6c1ca036d92322f69d87f626be0fc) and has since diverged substantially.
14
14
 
@@ -59,16 +59,10 @@ Paste the bot token. If `~/.pi/agent/telegram.json` already contains a saved tok
59
59
 
60
60
  The connected Pi instance owns Telegram polling. Use `/telegram-connect <profile>` to activate a named profile. Each profile is a parallel bot runtime with isolated polling, diagnostics, Threaded Mode state, and local bus transport; the `default` profile keeps unsuffixed runtime paths. In classic mode each profile uses a singleton lock. When Telegram private-chat Threaded Mode is available, one live instance becomes the profile's leader and later visible Pi instances register as followers. Reopening or resuming the same Pi session restores its remembered Thread at session startup. If `/resume` immediately follows `/telegram-connect`, or the bridge is already connected, connection intent follows the switch: the destination restores its own Thread/slot or receives a new binding, never the source session's Thread. Otherwise, a distinct unbound session still requires explicit `/telegram-connect`.
61
61
 
62
- An eligible single-text unbound Thread chooser offers **⛔️ Cancel routing**: the original stays private and is not sent to Pi; its Telegram message and Thread are not deleted. This does not cancel already selected or accepted work. When protected cancellation attempts remain, **Status → ❌ Pending cancellations** lets the owner review and finish them, including after restart. Historical unbound startup text has no main-menu entry or historical-review submenu. Eligible raw unsupported command/media/unknown historical private-Thread originals use a separate retain-only path, including on bound Threads without surviving Restore/temporary membership. Each worker generation reclassifies them after cold forgetting, blocking handler replay, generic Cancel and cold spending. Ordinary bound startup dispatch stops for these originals; same-kind live arrivals and owner/custody paths remain unchanged. Old historical-review buttons are refused without routing or abandoning the original; removal of the UI does not cancel previously accepted work.
63
-
64
- Eligible routing choosers and their donor sources share a **fixed 60-minute lifetime** from the first acknowledged publication, preserved across refresh/restart. Expiry drops unselected or unacknowledged routing attempts without privately archiving prompt bodies; nondelivery is accepted, so resend manually if needed. Old controls and late replies cannot replay the source. An expired disposable unbound tab is removed after all its owned groups resolve and fresh protection checks pass. Accepted queues, independent work and restored bound Threads remain intact. Confirmed expiry releases worker custody even if tab metadata fails; retries use only body-free tab/source proof, and uncertain deletion is never repeated. Explicit Cancel retains its separate private-original policy. Historical originals without a saved clock and unconfirmed publication receive no invented deadline. Local candidate checks do not certify live deletion.
65
-
66
62
  `/telegram-connect` keeps working when the shared runtime `state.json` is damaged. If it is malformed, foreign or contains invalid evidence, the connecting leader replaces it with a fresh empty state and continues. Every profile then forgets old Thread bindings, slots, unfinished Restore/temporary work and polling continuity; `telegram.json`, Pi history and the released `tmp/telegram` are untouched. Permission errors still stop connection. See [Damaged-State Reset](./docs/architecture.md#damaged-state-reset-operator-approved).
67
63
 
68
64
  The prompt queue is session-local. Before a reload or restart, let it drain or knowingly accept losing waiting work and re-send what still matters afterwards; nothing is replayed automatically.
69
65
 
70
- Journal recovery is a separate existing loss policy, not shared-envelope recovery. A snapshot removed by older broad temp cleanup is rebuilt when its complete segment history proves an empty result, while a revisionless snapshot is repaired from the first surviving segment's exact predecessor when the reconstructed tail validates. Otherwise the damaged snapshot and segments are deleted (their inputs are lost), a fresh journal is published, and startup continues with an informational diagnostic. Unsupported journal versions block recovery without rewriting or quarantining retained files; use a compatible runtime rather than deleting journals. Saved `telegram.json` configuration and runtime diagnostics are outside that journal reset. Startup best-effort removes obsolete current-runtime root/session `recovery/` folders, so those directories are not a preservation location.
71
-
72
66
  Ordinary network or admission failures use exponential retry pauses capped at 30 seconds, without permanently stopping after an arbitrary attempt count. The poller recovers when the outage clears. Explicit `/telegram-disconnect` still attempts to stop its captured local transport if Thread cleanup is unavailable; it reports unconfirmed cleanup rather than successful deletion, and an old disconnect cannot stop a new connection. Incomplete stop/release remains visible; check `/telegram-status --debug` and keep Pi open.
73
67
 
74
68
  Persistent competing `getUpdates` clients cause a bounded transport stand-down rather than endless retries. Accepted local work remains queued/executable, but Telegram delivery stops. Inspect `/telegram-status --debug`, stop the competing client, then reconnect. See [Runtime Ownership](./docs/architecture.md#runtime-ownership).
@@ -239,7 +233,7 @@ Voice notes, audio, images, PDFs, and other media can pass through configured in
239
233
 
240
234
  ### Buttons And Callbacks
241
235
 
242
- Assistant replies can place controls between paragraphs using standalone triple-backtick `telegram_button` blocks, or keep them in the footer using top-level hidden `telegram_button` comments. Both wrappers accept singleton cells and mixed JSON/CML matrices. Native in-body rows allow up to eight buttons; HTML compatibility moves these rows to the footer. In-body clicks acknowledge without recoloring the Rich body, while footer selection styles remain unchanged. Hidden comments accept a JSON object, adaptive JSON/CML matrix, or positional Compact Matrix Literal (CML). One adaptive matrix may mix named JSON objects with positional CML cells; separators are optional and one trailing comma is tolerated, including inside JSON objects. Top-level cells become full-width rows while nested rows group one or more buttons horizontally without an artificial parser-level width cap; inside a fenced block, adjacent top-level JSON/CML objects may omit the outer array and commas. Generated surfaces default to five columns and use six to eight only for short position-bearing labels. CML uses `{value}`, `{label|prompt}`, `{|prompt}`, or the corresponding three-atom form with `selected_style` set to `primary`, `success`, or `danger`; omitting the first atom leaves the existing prompt-as-label fallback in charge, while the optional style still requires a non-empty prompt. A fourth atom adds disabled state: `{|Next||1}` or `{|Next||true}` disables, `0` or `false` enables (exact lowercase), and omission stays enabled; the third atom may be empty in this form. JSON uses boolean `disabled`. Disabled controls require no prompt or selected style: `{Next|||1}` shows only a label, while `{|||1}` is a blank disabled cell. Disabled controls remain visible without registering callbacks or invoking prompts/app methods. It trims atom boundaries, preserves non-structural text literally, and decodes only `\|`, `\}`, and `\\`. Prefer one matrix comment for multiple buttons. Buttons use `label` plus `prompt`, or the compact `value` key when both are identical. The bridge strips every assistant-authored HTML comment from Telegram previews and final replies regardless of Markdown position or owning extension, while only recognized top-level comments activate buttons or voice; comment-only output sends no text message and the Pi terminal transcript remains unchanged. It renders valid inline buttons and routes callbacks back into Pi as queued prompts or extension-owned callback actions. Button-only replies receive the standard `☑️ **Choose an option:**` heading as automatic visible fallback text. Once a generated prompt button is accepted, only that exact button switches to its optional `selected_style` (`primary` blue by default, `success` green, or `danger` red) without altering its agent-authored label or emoji; every style still queues the selected prompt.
236
+ Assistant replies can include native prompt buttons between paragraphs or in a footer. Clicking a button queues its prompt or invokes its owning extension action. See the [button syntax and examples](./docs/public-api.md#tools-and-assistant-authored-actions).
243
237
 
244
238
  ### Threaded Mode And Multi-Instance Bus
245
239
 
@@ -251,7 +245,7 @@ Classic private DM mode is the base product mode. When Telegram private-chat Thr
251
245
  - Queued work for a live follower transfers through authenticated exact-journal handoff rather than replaying under the transport owner.
252
246
  - Reopening, resuming, or replacing the process for the same Pi session restores its remembered Thread; distinct sessions in one directory keep independent bindings and letter slots.
253
247
  - Unknown threads are preserved and offered explicit reroute/restore choices.
254
- - When all `A`–`Z` slots are reserved, a fresh connection rotates the oldest proven inactive, unprotected binding. This deletes the old Telegram Thread **and all its messages**, then reuses its letter; Pi session history, project files, memory, and journals are not deleted. Unfinished work normally blocks rotation. Only complete unoffered receipt groups whose exact process-birth owners are all proven dead may be discarded without replay under actual slot pressure, followed by a fresh protection check. Live/unverifiable owners, partial groups, handoffs, incomplete sources, and unknown safety evidence remain protected. Restoring a remembered session does not evict another binding. With automatic cleanup disabled, verified leader quit or confirmed-dead follower detachment retains the tab and records inactivity for later pressure rotation; reload/new/resume/fork never detach it. Within the same leader runtime, pruned followers can be rechecked for later PID absence and failed inactivity writes retried without restoring their live routing.
248
+ - When all `A`–`Z` slots are reserved, a new connection may reclaim the oldest inactive, unprotected Thread. **This deletes that Thread and all its messages**, not Pi session history or project files. Active or protected work blocks reclamation; restoring a remembered session does not evict another binding.
255
249
  - Telegram never launches hidden Pi processes.
256
250
 
257
251
  In Threaded Mode, open Settings → **🧵 Thread display** to choose `letters` (default), `names`, `directory-title`, or `directory-snake` for this bot profile; a retained **directories** value remains available as an unchanged legacy presentation. Fresh tabs are created with the active mode's title instead of being visibly renamed afterward. Telegram tab titles, Pi terminal status, live Thread choosers/notices, prompt attribution, and named `telegram_message` targeting use the same acknowledged display name; target IDs and live registrations still own routing. Names shows the generated dictionary name chosen for the slot, such as `Anchor` for slot `A`; the directory formats produce `api_tools` or `Api Tools` and append `_a` or ` A` only while two or more authenticated live instances share that exact directory. Dormant retained bindings do not keep those suffixes visible. After leader election, automatic title contraction waits one follower-staleness window for live registrations to settle, avoiding a temporary suffix removal and restoration during startup. `/name` sets a manual Thread display name; **Reset to automatic** restores the selected automatic projection. Switching preserves Thread IDs, slots, generated recovery identity, and queue ownership. Partial application reports an error and can be retried without recreating Threads.
@@ -261,6 +255,8 @@ In Threaded Mode, open Settings → **🧵 Thread display** to choose `letters`
261
255
  | Classic DM | One running Pi instance and its active session controlled from one private bot chat | One polling owner, one queue/runtime surface |
262
256
  | Threaded Mode | Several visible Pi sessions sharing one bot | One leader owns transport; each private-chat Thread retains one session-qualified Workspace binding |
263
257
 
258
+ **Live rebinding (0.53.0):** Restore uses one same-session save/apply/release channel for prompts and supported commands/templates, with separately fenced best-effort old-Thread cleanup. Existing work and exact source/receipt custody remain protected; legacy Restore readers/receivers retain continuity with older instances. Windows runs the same paths, and CI covers it. See [Multi-Instance Bus](./docs/multi-instance-bus.md#live-rebind-channel).
259
+
264
260
  ## Environment Configuration
265
261
 
266
262
  Most controls live in Pi commands or the Telegram menu. Environment variables remain for bootstrap and transport boundaries:
@@ -294,7 +290,7 @@ Stable public entrypoints are documented in [Public API](./docs/public-api.md),
294
290
 
295
291
  ## Safety Boundaries
296
292
 
297
- Durable inbound admission is a **process-crash recovery** guarantee. Atomic private-file replacement preserves acknowledged journal authority and its journal-owned `acceptedThroughUpdateId` polling cursor across ordinary process exit, crash, kill, and replacement, but the extension does not flush files or parent directories for host/kernel/filesystem/device/power-loss durability. `telegram.json` contains configuration only. Keep `~/.pi/agent` on appropriately managed storage and backups if that stronger operational guarantee is required. Before downgrading below `0.37.0`, run `node scripts/check-downgrade.mjs`; any retained cursor-schema journal blocks downgrade because an older runtime could repoll admitted updates. See [Durable Admission And Recovery](./docs/architecture.md#durable-admission-and-recovery).
293
+ Durable inbound admission is a **process-crash recovery** guarantee. Atomic private-file replacement preserves acknowledged journal authority and its journal-owned `acceptedThroughUpdateId` polling cursor across ordinary process exit, crash, kill, and replacement, but the extension does not flush files or parent directories for host/kernel/filesystem/device/power-loss durability. `telegram.json` contains configuration only. Keep `~/.pi/agent` on appropriately managed storage and backups if that stronger operational guarantee is required. Downgrading below `0.37.0` is unsafe: an older runtime cannot recover the journal's polling cursor and could repoll admitted updates. See [Durable Admission And Recovery](./docs/architecture.md#durable-admission-and-recovery).
298
294
 
299
295
  Runtime files use `tmp/pi-telegram` with only `state.json` and `logs.jsonl` at its root; pre-0.52.0 `tmp/telegram` is left untouched and never migrated. The transport section of `state.json` names a polling journal hosted in a session folder; successor leaders continue it rather than moving its cursor. Follower journals are session-owned. Unbound session families are disposable under the approved sweep policy; in-process `/new` adopts eligible pending input, while cold startup does not. See [Session-Owned Journal Storage](./docs/multi-instance-bus.md#session-owned-journal-storage) for compatibility fallbacks, loss boundaries and pending acceptance.
300
296
 
@@ -309,7 +305,7 @@ Runtime files use `tmp/pi-telegram` with only `state.json` and `logs.jsonl` at i
309
305
 
310
306
  Telegram is a companion surface around a live Pi runtime, not a second runtime. It can compact the current session, and `/new` uses Pi's public session API to start a new session in the current classic chat or Thread once the bridge is idle. Resuming, forking, browsing, and switching other sessions remain Pi TUI operations, not Telegram navigation.
311
307
 
312
- A Telegram prompt is a normal model turn in the active Pi session and therefore inherits that session's active post-compaction context; the bridge does not make token cost proportional only to the new mobile message. The bundled `telegram-bridge` Skill owns general agent operation; `show-me` turns current work and system behavior into truthful phone-width Markdown or focused browser-ready HTML while remaining useful in the terminal; `generated-control-surface` proactively compiles optional evidence-backed ephemeral controls when model interpretation remains useful; and `generative-apps` compiles stable repeated interaction into reviewed reusable applications whose bound buttons bypass model inference while ordinary prompt buttons retain it. Generative Apps may own a closed state machine or adapt another authoritative tool, service, Actor Run, or application through bounded methods. Disconnecting removes pi-telegram's delivery tools and transient routing guidance from later requests until direct ownership or follower registration returns, without changing other active Pi tools. Pi session JSONL contains model history; profile-scoped pi-telegram `logs*.jsonl` contains redacted operational events and is never model context.
308
+ Telegram prompts run in the active Pi session and use its current context, just like prompts sent from the terminal.
313
309
 
314
310
  ## Documentation Map
315
311
 
package/api/commands.ts CHANGED
@@ -6,6 +6,9 @@
6
6
 
7
7
  export {
8
8
  registerTelegramCommand,
9
+ type PreparedSelectedCommand,
10
+ type SelectedCommandExecution,
11
+ type SelectedPreparationInput,
9
12
  type TelegramExtensionCommandContext,
10
13
  type TelegramExtensionCommandRegistration,
11
14
  } from "../lib/commands.ts";
@@ -3,4 +3,4 @@
3
3
  * Zones: package boundary, extension interop
4
4
  * Exposes the stable Telegram slash-command registration surface while keeping registry internals package-private
5
5
  */
6
- export { registerTelegramCommand, type TelegramExtensionCommandContext, type TelegramExtensionCommandRegistration, } from "../lib/commands.ts";
6
+ export { registerTelegramCommand, type PreparedSelectedCommand, type SelectedCommandExecution, type SelectedPreparationInput, type TelegramExtensionCommandContext, type TelegramExtensionCommandRegistration, } from "../lib/commands.ts";
@@ -4,8 +4,8 @@
4
4
  * Owns persistent bounded thinking and tool disclosures; excludes activity normalization, assistant answer rendering, and transport authority policy
5
5
  */
6
6
  import type { TelegramActivityEvent, TelegramActivityPublicationRuntime } from "./activity.ts";
7
- import type { TelegramEditMessageTextBody, TelegramInputRichMessage, TelegramSendMessageBody, TelegramSendRichMessageBody, TelegramSentMessage } from "./telegram-api.ts";
8
7
  import { type TelegramTarget } from "./target.ts";
8
+ import type { TelegramEditMessageTextBody, TelegramInputRichMessage, TelegramSendMessageBody, TelegramSendRichMessageBody, TelegramSentMessage } from "./telegram-api.ts";
9
9
  export declare const TELEGRAM_ACTIVITY_MESSAGE_MAX_TOOLS = 6;
10
10
  export declare const TELEGRAM_REASONING_BUFFER_MAX_CHARS = 1200;
11
11
  export declare const TELEGRAM_TOOL_UPDATE_MAX_ENTRIES = 4;
@@ -28,10 +28,6 @@ export interface TelegramActivityVerbosityRuntime {
28
28
  stop: () => void;
29
29
  waitForIdle: () => Promise<void>;
30
30
  }
31
- export interface TelegramActivityVerbosityBinding extends TelegramActivityVerbosityRuntime {
32
- bind: (runtime: TelegramActivityVerbosityRuntime) => void;
33
- }
34
- export declare function createTelegramActivityVerbosityBinding(): TelegramActivityVerbosityBinding;
35
31
  export declare function createTelegramActivityVerbosityRuntime<TAuthority>(deps: {
36
32
  enqueue?: TelegramActivityPublicationRuntime["enqueue"];
37
33
  getActivityMode: () => "quiet" | "thinking" | "tools" | "verbose";
@@ -3,8 +3,8 @@
3
3
  * Zones: telegram activity, rich rendering, operational delivery
4
4
  * Owns persistent bounded thinking and tool disclosures; excludes activity normalization, assistant answer rendering, and transport authority policy
5
5
  */
6
- import { escapeHtml, renderTelegramInlineMarkdownHtml, } from "./rendering.js";
7
- import { areTelegramTargetsEqual as targetEquals } from "./target.js";
6
+ import { escapeHtml, renderTelegramInlineMarkdownHtml } from "./rendering.js";
7
+ import { areTelegramTargetsEqual as targetEquals, } from "./target.js";
8
8
  const TELEGRAM_ACTIVITY_DETAIL_MAX_CHARS = 1_200;
9
9
  const TELEGRAM_ACTIVITY_MESSAGE_MAX_CHARS = 3_900;
10
10
  export const TELEGRAM_ACTIVITY_MESSAGE_MAX_TOOLS = 6;
@@ -129,11 +129,7 @@ function renderToolActivityHtml(tool) {
129
129
  if (tool.complete && tool.result !== undefined) {
130
130
  evidence.push(`"${tool.isError ? "error" : "result"}": ${tool.result}`);
131
131
  }
132
- const status = tool.complete
133
- ? tool.isError
134
- ? "failed"
135
- : "done"
136
- : "running";
132
+ const status = tool.complete ? (tool.isError ? "failed" : "done") : "running";
137
133
  return [
138
134
  `<b>${escapeHtml(formatToolActivityLabel(tool.name))}:</b> <code>${status}</code>`,
139
135
  `<blockquote expandable>${escapeActivityEvidenceHtml(evidence.join("\n\n"))}</blockquote>`,
@@ -151,11 +147,7 @@ function createToolActivityDetail(summary, text, isOpen = false) {
151
147
  };
152
148
  }
153
149
  function renderToolActivityRichBlocks(tool) {
154
- const status = tool.complete
155
- ? tool.isError
156
- ? "failed"
157
- : "done"
158
- : "running";
150
+ const status = tool.complete ? (tool.isError ? "failed" : "done") : "running";
159
151
  const evidenceBlocks = [
160
152
  createToolActivityDetail("arguments", tool.args, true),
161
153
  ];
@@ -194,31 +186,11 @@ function toolMessageSize(tools) {
194
186
  return renderTelegramToolActivityHtml(tools).length;
195
187
  }
196
188
  function isKnownSafeRichActivityRejection(error) {
197
- return error instanceof Error && /HTTP 400: Bad Request:/i.test(error.message);
189
+ return (error instanceof Error && /HTTP 400: Bad Request:/i.test(error.message));
198
190
  }
199
191
  export function renderTelegramThinkingActivityHtml(text) {
200
192
  return `<blockquote expandable>${renderThinkingActivityEvidenceHtml(text)}</blockquote>`;
201
193
  }
202
- export function createTelegramActivityVerbosityBinding() {
203
- let runtime;
204
- return {
205
- bind(next) {
206
- runtime = next;
207
- },
208
- accept(event) {
209
- runtime?.accept(event);
210
- },
211
- reset() {
212
- runtime?.reset();
213
- },
214
- stop() {
215
- runtime?.stop();
216
- },
217
- waitForIdle() {
218
- return runtime?.waitForIdle() ?? Promise.resolve();
219
- },
220
- };
221
- }
222
194
  export function createTelegramActivityVerbosityRuntime(deps) {
223
195
  let active = true;
224
196
  let generation = 0;
@@ -261,7 +233,10 @@ export function createTelegramActivityVerbosityRuntime(deps) {
261
233
  toolOrder.length = 0;
262
234
  };
263
235
  const hasAuthority = () => authority !== undefined && deps.isAuthorityActive(authority);
264
- const isCurrent = (acceptedGeneration, admittedAuthority) => active && generation === acceptedGeneration && admittedAuthority !== undefined && deps.isAuthorityActive(admittedAuthority);
236
+ const isCurrent = (acceptedGeneration, admittedAuthority) => active &&
237
+ generation === acceptedGeneration &&
238
+ admittedAuthority !== undefined &&
239
+ deps.isAuthorityActive(admittedAuthority);
265
240
  const ensureActivity = (event, admittedTarget, admittedAuthority) => {
266
241
  if (deps.getActivityMode() === "quiet")
267
242
  return false;
@@ -341,9 +316,7 @@ export function createTelegramActivityVerbosityRuntime(deps) {
341
316
  const scheduleReasoningPublish = (event, acceptedGeneration) => {
342
317
  if (reasoningFlushTimer || reasoningBlocked)
343
318
  return;
344
- const elapsed = reasoningMessageFrames === 0
345
- ? 0
346
- : getNowMs() - lastReasoningPublishMs;
319
+ const elapsed = reasoningMessageFrames === 0 ? 0 : getNowMs() - lastReasoningPublishMs;
347
320
  const delayMs = Math.max(0, TELEGRAM_REASONING_MIN_INTERVAL_MS - elapsed);
348
321
  const schedule = deps.setReasoningTimeout ?? setTimeout;
349
322
  reasoningFlushTimer = schedule(() => {
@@ -580,8 +553,7 @@ export function createTelegramActivityVerbosityRuntime(deps) {
580
553
  }
581
554
  if (event.type === "agent-end" || event.type === "agent-settled") {
582
555
  clearReasoningFlushTimer();
583
- if (reasoningChars > lastReasoningMessageChars &&
584
- !reasoningBlocked) {
556
+ if (reasoningChars > lastReasoningMessageChars && !reasoningBlocked) {
585
557
  await publishReasoning(event, acceptedGeneration);
586
558
  }
587
559
  if (!isCurrent(acceptedGeneration, admittedAuthority))
@@ -599,11 +571,12 @@ export function createTelegramActivityVerbosityRuntime(deps) {
599
571
  const admittedAuthority = deps.captureAuthority();
600
572
  const enqueue = deps.enqueue ?? ((task) => tail.then(task));
601
573
  tail = enqueue(async () => {
602
- if (!active || generation !== acceptedGeneration || !deps.isAuthorityActive(admittedAuthority))
574
+ if (!active ||
575
+ generation !== acceptedGeneration ||
576
+ !deps.isAuthorityActive(admittedAuthority))
603
577
  return;
604
578
  await process(event, acceptedGeneration, admittedTarget, admittedAuthority);
605
- })
606
- .catch((error) => {
579
+ }).catch((error) => {
607
580
  deps.recordFailure?.("tool-send", event, error);
608
581
  });
609
582
  },
@@ -95,6 +95,7 @@ export declare function registerTelegramActivityHandler(registration: TelegramAc
95
95
  export declare function clearTelegramActivityHandlers(): void;
96
96
  /** @internal */
97
97
  export interface TelegramActivityDispatcher {
98
+ hasPending?: () => boolean;
98
99
  dispatch: (event: TelegramActivityEvent) => void;
99
100
  stop: () => void;
100
101
  }
@@ -151,6 +152,7 @@ export type TelegramAssistantStreamEvent = {
151
152
  };
152
153
  /** @internal */
153
154
  export interface TelegramActivityRuntime {
155
+ hasPending?: () => boolean | undefined;
154
156
  onSessionStart?: () => void;
155
157
  recordInputSource: (source: TelegramActivityInputSource) => void;
156
158
  onAgentStart: (activeTelegramTarget?: TelegramActivityTarget, replyToMessageId?: number) => void;
@@ -193,9 +195,16 @@ export interface TelegramActivityPublicationReservation {
193
195
  publish: (task: () => Promise<void>) => Promise<void>;
194
196
  cancel: () => void;
195
197
  }
198
+ export interface TelegramActivityPublicationWork {
199
+ settle(outcome: "settled" | "unconfirmed"): void;
200
+ }
196
201
  export interface TelegramActivityPublicationRuntime {
197
202
  enqueue: (task: () => Promise<void>) => Promise<void>;
198
203
  reserve: () => TelegramActivityPublicationReservation;
204
+ hasPending: () => boolean;
205
+ /** Observation only; detached work never occupies the ordered publication tail. */
206
+ beginWork: () => TelegramActivityPublicationWork;
207
+ hasUnconfirmed: () => boolean;
199
208
  reset: () => void;
200
209
  }
201
210
  export declare function createTelegramActivityPublicationRuntime(): TelegramActivityPublicationRuntime;
@@ -91,16 +91,14 @@ function createTelegramActivityContext(event, isActive) {
91
91
  };
92
92
  }
93
93
  function canCoalesceActivityEvents(previous, next) {
94
- if (previous.activityId !== next.activityId ||
95
- previous.type !== next.type) {
94
+ if (previous.activityId !== next.activityId || previous.type !== next.type) {
96
95
  return false;
97
96
  }
98
97
  if (previous.type === "assistant-text-delta" &&
99
98
  next.type === "assistant-text-delta") {
100
99
  return previous.contentIndex === next.contentIndex;
101
100
  }
102
- if (previous.type === "reasoning-delta" &&
103
- next.type === "reasoning-delta") {
101
+ if (previous.type === "reasoning-delta" && next.type === "reasoning-delta") {
104
102
  return previous.contentIndex === next.contentIndex;
105
103
  }
106
104
  if (previous.type === "tool-update" && next.type === "tool-update") {
@@ -113,8 +111,7 @@ function coalesceActivityEvents(previous, next) {
113
111
  next.type === "assistant-text-delta") {
114
112
  return { ...next, delta: previous.delta + next.delta };
115
113
  }
116
- if (previous.type === "reasoning-delta" &&
117
- next.type === "reasoning-delta") {
114
+ if (previous.type === "reasoning-delta" && next.type === "reasoning-delta") {
118
115
  return { ...next, delta: previous.delta + next.delta };
119
116
  }
120
117
  return next;
@@ -152,6 +149,7 @@ export function createTelegramActivityDispatcher(deps = {}) {
152
149
  }
153
150
  };
154
151
  return {
152
+ hasPending: () => Array.from(queues.values()).some((queue) => queue.active && (queue.running || queue.events.length > 0)),
155
153
  dispatch(event) {
156
154
  if (stopped)
157
155
  return;
@@ -194,6 +192,7 @@ export function createTelegramActivityBridgeRuntime(deps) {
194
192
  let runtime;
195
193
  const getRuntime = () => runtime;
196
194
  return {
195
+ hasPending: () => getRuntime()?.hasPending?.(),
197
196
  onSessionStart() {
198
197
  runtime?.onSessionShutdown();
199
198
  runtime = createTelegramActivityRuntime({
@@ -298,7 +297,9 @@ export function createTelegramActivityRuntime(deps) {
298
297
  sequence,
299
298
  source: activitySource,
300
299
  ...(activityTarget ? { target: activityTarget } : {}),
301
- ...(activityReplyToMessageId !== undefined ? { replyToMessageId: activityReplyToMessageId } : {}),
300
+ ...(activityReplyToMessageId !== undefined
301
+ ? { replyToMessageId: activityReplyToMessageId }
302
+ : {}),
302
303
  timestamp: now(),
303
304
  };
304
305
  try {
@@ -342,13 +343,15 @@ export function createTelegramActivityRuntime(deps) {
342
343
  clearActivity();
343
344
  };
344
345
  return {
346
+ hasPending: () => deps.dispatcher.hasPending?.(),
345
347
  recordInputSource(source) {
346
348
  pendingInputSource = source;
347
349
  },
348
350
  onAgentStart(activeTelegramTarget, replyToMessageId) {
349
351
  abandonCompaction();
350
352
  ensureActivity(activeTelegramTarget);
351
- activityReplyToMessageId = activitySource === "telegram" ? replyToMessageId : undefined;
353
+ activityReplyToMessageId =
354
+ activitySource === "telegram" ? replyToMessageId : undefined;
352
355
  emit({ type: "agent-start" });
353
356
  },
354
357
  onAssistantEvent(event) {
@@ -470,12 +473,15 @@ export function createTelegramActivityRuntime(deps) {
470
473
  export function createTelegramActivityPublicationRuntime() {
471
474
  let generation = 0;
472
475
  let tail = Promise.resolve();
476
+ let outstanding = 0, unconfirmed = false;
473
477
  const pending = new Set();
474
478
  const reserve = () => {
475
479
  const admittedGeneration = generation;
476
480
  let state = "pending";
477
481
  let resolve;
478
- const ready = new Promise((accept) => { resolve = accept; });
482
+ const ready = new Promise((accept) => {
483
+ resolve = accept;
484
+ });
479
485
  const cancel = () => {
480
486
  if (state !== "pending")
481
487
  return;
@@ -484,10 +490,15 @@ export function createTelegramActivityPublicationRuntime() {
484
490
  resolve(undefined);
485
491
  };
486
492
  pending.add(cancel);
487
- const result = tail.then(async () => {
493
+ outstanding += 1;
494
+ const result = tail
495
+ .then(async () => {
488
496
  const task = await ready;
489
497
  if (admittedGeneration === generation && task)
490
498
  await task();
499
+ })
500
+ .finally(() => {
501
+ outstanding -= 1;
491
502
  });
492
503
  tail = result.catch(() => { });
493
504
  return {
@@ -507,8 +518,26 @@ export function createTelegramActivityPublicationRuntime() {
507
518
  return {
508
519
  reserve,
509
520
  enqueue: (task) => reserve().publish(task),
521
+ hasPending: () => outstanding > 0,
522
+ hasUnconfirmed: () => unconfirmed,
523
+ beginWork() {
524
+ const admittedGeneration = generation;
525
+ let settled = false;
526
+ outstanding += 1;
527
+ return {
528
+ settle(outcome) {
529
+ if (settled)
530
+ return;
531
+ settled = true;
532
+ outstanding -= 1;
533
+ if (outcome === "unconfirmed" && admittedGeneration === generation)
534
+ unconfirmed = true;
535
+ },
536
+ };
537
+ },
510
538
  reset() {
511
539
  generation += 1;
540
+ unconfirmed = false;
512
541
  for (const cancel of pending)
513
542
  cancel();
514
543
  tail = Promise.resolve();
@@ -12,7 +12,10 @@ export function createTelegramAgentMessageRuntime(deps) {
12
12
  const leaderTarget = deps.getLeaderTarget();
13
13
  if (leaderTarget?.threadId) {
14
14
  targets.push({
15
- target: { chatId: leaderTarget.chatId, threadId: leaderTarget.threadId },
15
+ target: {
16
+ chatId: leaderTarget.chatId,
17
+ threadId: leaderTarget.threadId,
18
+ },
16
19
  threadName: deps.getDisplayTitle?.(leaderTarget) ?? deps.getLeaderThreadName(),
17
20
  });
18
21
  }
@@ -65,8 +68,9 @@ export function createTelegramAgentMessageRuntime(deps) {
65
68
  if (allowedChatId === undefined || !ctx) {
66
69
  throw new Error("Telegram agent turn routing is unavailable.");
67
70
  }
68
- const sourceTitle = (input.sourceTarget ? deps.getDisplayTitle?.(input.sourceTarget) : undefined)
69
- ?? input.sourceThreadName;
71
+ const sourceTitle = (input.sourceTarget
72
+ ? deps.getDisplayTitle?.(input.sourceTarget)
73
+ : undefined) ?? input.sourceThreadName;
70
74
  const sourceLabel = sourceTitle
71
75
  ?.replace(/[\r\n\[\]]+/g, " ")
72
76
  .trim()