@llblab/pi-kit 0.35.0 → 0.35.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 (42) hide show
  1. package/BACKLOG.md +1 -0
  2. package/CHANGELOG.md +5 -0
  3. package/README.md +1 -1
  4. package/node_modules/@llblab/pi-telegram/AGENTS.md +1 -1
  5. package/node_modules/@llblab/pi-telegram/BACKLOG.md +6 -1
  6. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +6 -0
  7. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +3 -2
  8. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +11 -3
  9. package/node_modules/@llblab/pi-telegram/dist/lib/bus-api.js +2 -2
  10. package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +2 -0
  11. package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +21 -8
  12. package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +40 -45
  13. package/node_modules/@llblab/pi-telegram/dist/lib/journal.d.ts +7 -0
  14. package/node_modules/@llblab/pi-telegram/dist/lib/journal.js +80 -0
  15. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-attachments.d.ts +12 -0
  16. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-attachments.js +33 -3
  17. package/node_modules/@llblab/pi-telegram/dist/lib/queue.d.ts +6 -2
  18. package/node_modules/@llblab/pi-telegram/dist/lib/queue.js +46 -4
  19. package/node_modules/@llblab/pi-telegram/dist/lib/replies.d.ts +5 -1
  20. package/node_modules/@llblab/pi-telegram/dist/lib/replies.js +2 -2
  21. package/node_modules/@llblab/pi-telegram/dist/lib/routing.d.ts +2 -0
  22. package/node_modules/@llblab/pi-telegram/dist/lib/routing.js +137 -13
  23. package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.d.ts +10 -5
  24. package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.js +19 -2
  25. package/node_modules/@llblab/pi-telegram/dist/lib/threads.js +23 -11
  26. package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
  27. package/node_modules/@llblab/pi-telegram/docs/architecture.md +5 -1
  28. package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +3 -1
  29. package/node_modules/@llblab/pi-telegram/docs/outbound.md +4 -2
  30. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +13 -2
  31. package/node_modules/@llblab/pi-telegram/lib/bus-api.ts +2 -1
  32. package/node_modules/@llblab/pi-telegram/lib/commands.ts +21 -8
  33. package/node_modules/@llblab/pi-telegram/lib/extension.ts +43 -55
  34. package/node_modules/@llblab/pi-telegram/lib/journal.ts +80 -0
  35. package/node_modules/@llblab/pi-telegram/lib/outbound-attachments.ts +36 -3
  36. package/node_modules/@llblab/pi-telegram/lib/queue.ts +43 -4
  37. package/node_modules/@llblab/pi-telegram/lib/replies.ts +3 -2
  38. package/node_modules/@llblab/pi-telegram/lib/routing.ts +140 -18
  39. package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +22 -5
  40. package/node_modules/@llblab/pi-telegram/lib/threads.ts +25 -15
  41. package/node_modules/@llblab/pi-telegram/package.json +1 -1
  42. package/package.json +2 -2
package/BACKLOG.md CHANGED
@@ -4,6 +4,7 @@ Package pins, resource order and bundled runtime ownership remain authoritative
4
4
 
5
5
  ## Carried checks
6
6
 
7
+ - **Installed 0.35.1 Telegram smoke (operator-owned):** After separately authorized kit installation/reload, verify Reroute removes an exhausted temporary tab and Restore keeps the new tab while deleting its unprotected predecessor. Confirm protected work survives. Guest PNG/JPEG, document and MP3 replacement plus staging cleanup remain explicitly deferred upstream live checks; verify unsupported OGG/Opus feedback separately. Upstream idle-leader acceptance and kit packaging checks do not certify this installation.
7
8
  - **Installed 0.35.0 Telegram smoke (operator-owned):** After separately authorized installation/reload of both peers, verify same-process follower session replacement keeps the Thread, letter slot and title without a second connected notice. Check shared `threads.displayMode` through Settings across profiles and confirm the retired profile key is ignored without migration. Package validation does not certify installed clients.
8
9
  - **Installed 0.34.3 typing smoke (operator-owned):** After separately authorized installation/reload of both peers, verify native typing during streaming, UI prompts and compaction without recurring local authorization churn. Check actual client versions, including iOS; successful HTTP issuance and synthetic tests do not prove client visibility. Use disposable sessions and preserve accepted work.
9
10
  - **Installed 0.34.2 smoke (operator-owned):** After separately authorized installation/reload, verify Pi 1.2 custom-message completions no longer raise the Telegram prompt-hook error, adjacent button matrices retain their rows and callbacks, and native Windows Codex CLI fallback works on an actual Windows host. Use disposable settings; package validation and synthetic SDK probes do not certify installed clients or authorize live fault injection.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  All notable changes to `@llblab/pi-kit` are documented here.
4
4
 
5
+ ## 0.35.1: Telegram Guest Media and Routing Cleanup
6
+
7
+ - `Telegram cleanup`: Advances only the exact Telegram pin to `0.57.1`. Exhausted Reroute tabs and Restore's old tab are no longer blocked by unrelated targetless creation attempts, while accepted work and uncertain deletion stay protected. Fresh local menu Reroute and supported Restore hold command effects through their cleanup decision. Other pins, package membership and resource order are unchanged.
8
+ - `Guest media`: Queued photos, documents and MP3 now replace an acknowledged Guest Mode placeholder instead of being silently skipped. Confirmed failures report errors, uncertain edits are not replayed, and unsupported OGG/Opus replacement is rejected explicitly. Guest media client acceptance remains a deferred upstream follow-up, not proof supplied by kit validation.
9
+
5
10
  ## 0.35.0: Telegram Thread Continuity and Shared Settings
6
11
 
7
12
  - `Telegram continuity`: Advances only the exact Telegram pin to `0.57.0`. Same-process follower session replacement preserves the Thread, letter slot and acknowledged title without a second connected notice; cross-process handoff notices use the acknowledged title. Other pins, package membership and resource order are unchanged.
package/README.md CHANGED
@@ -18,7 +18,7 @@ Package links lead to the owning repositories for usage, documentation, issues,
18
18
  | [`@llblab/pi-codex-usage`](https://github.com/llblab/pi-codex-usage) | `0.13.1` | OpenAI/legacy Codex subscription quota and Business credit status, with persistent priority Fast toggle mirrored in Telegram |
19
19
  | [`@llblab/pi-grow-loop`](https://github.com/llblab/pi-grow-loop) | `0.9.0` | Visible continuation scheduling and bounded worker Skills through compiled, manifest-owned resources |
20
20
  | [`@llblab/pi-state-flow`](https://github.com/llblab/pi-state-flow) | `0.26.4` | Scoped context/memory compiler with step-independent current memory, stable Passive/Active modes and visible blocked-state status |
21
- | [`@llblab/pi-telegram`](https://github.com/llblab/pi-telegram) | `0.57.0` | Telegram companion with per-prompt Steer, queued controls, Reroute/Restore, recoverable connections, previews, follower Threads, files, and voice |
21
+ | [`@llblab/pi-telegram`](https://github.com/llblab/pi-telegram) | `0.57.1` | Telegram companion with per-prompt Steer, queued controls, Reroute/Restore, recoverable connections, previews, follower Threads, files, and voice |
22
22
  | [`@llblab/skills`](https://github.com/llblab/skills) | `1.15.0` | Portable workflows for engineering, review, design, context maintenance, and other focused tasks |
23
23
 
24
24
  Versions are exact by design. An upstream release does not change an installed kit until this repository explicitly advances the dependency and publishes a new kit version. Runtime defects and package-specific feature requests belong in the linked repository; package selection and kit installation issues belong here.
@@ -90,7 +90,7 @@ Use the relevant local skill before non-trivial work in its domain. Keep skill o
90
90
  - 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`.
91
91
  - 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.
92
92
  - 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.
93
- - 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
+ - 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. Fully resolved, positively unbound temporary-tab cleanup may independently recover historical v1 queued receipts under its profile admission and journal source serialization: complete strict namespace/reference inspection, whole unoffered receipt groups, fresh process-death proof and the existing journal CAS are required. Alive/unverifiable owners and unsupported custody remain protected; this recovery never runs from read-only protection checks, and every ordinary target/source protection is recaptured before deletion. 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.
94
94
  - 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.
95
95
  - 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.
96
96
  - 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.
@@ -2,7 +2,12 @@
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
- - [ ] **Connection recovery live acceptance.** Verify operator-run leader/follower reconnect after interrupted creation on updated peers. Known target reuse and unknown-target abandonment have separate local regressions; do not infer live recovery from them. Also verify a long-deleted follower tab, follower `/start` Restore with negotiated menu-alias support, and leader Restore confirmation on the first click plus separately proven old-tab cleanup. Record leftover-tab behavior, exact peer capabilities and diagnostics; local regressions and release authorization do not replace this live evidence.
5
+ - [ ] **Routing extensions and follow-up coverage.** Preserve live/unverifiable owners, offered receipts, exact source references, session/transport fences and one-shot deletion while extending beyond the current supported handoffs.
6
+ - [ ] **Follower menu handoff.** Establish whether deletion-before-execution warrants a separately negotiated Reroute acceptance/release contract. Ordinary follower ACK confirms durable custody, not held execution; existing held-command RPCs belong to Restore relocation and cannot silently change ordinary forwarding. Require exact recipient generation, bounded observation and no replay after uncertain release.
7
+ - [ ] **Non-menu and historical-source boundary.** Resolve the intended ordering for mutating/queue-producing commands and cold/revived originals before expanding the fresh local menu path. Queue receipt acceptance and historical retention must remain authoritative; never pre-complete sources, weaken fresh-source readiness or block accepted work solely to force deletion. Preserve current behavior until a supported handoff is proven.
8
+ - [ ] **Protected-work live coverage.** Exercise Reroute and Restore with protected earlier work to confirm cleanup waits without losing work. Idle leader acceptance and native regressions do not prove every busy-session client scenario. No manual journal edits or deletion of live tabs to obtain evidence.
9
+ - [ ] **Guest inline media live acceptance.** Explicitly deferred beyond 0.57.1 by operator release approval; not completed. After separately authorized reload, verify queued PNG/JPEG, documents and MP3 replace the acknowledged placeholder, with and without caption, and staging messages are removed. Confirm OGG/Opus attachment rejection and visible voice-markup explanation. Mocked Bot API coverage does not prove live-client rendering; use disposable artifacts and preserve accepted work.
10
+ - [ ] **Connection recovery live acceptance.** Verify operator-run leader/follower reconnect after interrupted creation on updated peers. Known target reuse and unknown-target abandonment have separate local regressions; do not infer live recovery from them. Also verify a long-deleted follower tab and follower `/start` Restore with negotiated menu-alias support. Record leftover-tab behavior, exact peer capabilities and diagnostics; local regressions and release authorization do not replace this live evidence.
6
11
  - [ ] **Steer follow-up live acceptance.** Release authorization explicitly defers these checks beyond 0.56.0; do not treat automated coverage or the accepted busy-run smoke as live proof for unrun scenarios. Preserve the [queue and dispatch contract](./docs/architecture.md#queue-and-dispatch-safety).
7
12
  - [ ] **Operator scenarios.** Verify final selector/counter rendering; idle packets, native pending input, compaction, model switching, abort/Continue, complete media groups, Reroute/Restore, negotiated leader/follower transfer, reply/preview ownership and session-generation replacement. Record actual Pi/OS/Telegram versions, peer capabilities and redacted outcomes. Reload and disposable live tests require operator intent; do not manipulate runtime files to obtain acceptance.
8
13
  - [ ] **Restore recipient refusal after leadership change.** Diagnose a retained routing chooser refusing the selected current leader with unavailable recipient authority. Identify the exact context/session/Workspace mismatch before proposing recovery; do not treat an absent follower registry as the cause of a leader-target refusal, bypass fences or mutate live state.
@@ -4,6 +4,12 @@
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.57.1: Guest Media and Routing Cleanup
8
+
9
+ - `Guest attachments`: Queued photos, documents and MP3 audio now replace the acknowledged Guest Mode placeholder instead of being silently skipped. Upload failures and confirmed edit rejections report errors; uncertain edits are not replayed or overwritten. OGG/Opus attachments are rejected before queueing on this path, and voice markup explains the limitation. Live-client media acceptance remains a deferred follow-up. Fixes #346.
10
+ - `Routing cleanup`: Exhausted Reroute tabs and a restored instance's old tab are no longer blocked by unrelated creation attempts with unknown targets. Uncertain creation records stay intact; binding, slot, owner and concrete or recovered target conflicts still protect. Temporary cleanup can recover whole historical receipts from proven-dead owners; live/unverifiable owners, handoff offers and damaged sources remain protected. Idle leader Reroute and Restore passed operator live checks.
11
+ - `Command ordering`: Fresh local menu Reroute waits for exact source removal and a temporary-tab cleanup attempt before rendering. Restore confirms the new binding, decides old-tab cleanup, then releases held input. Busy work retains protection, and uncertain deletion never repeats. Other Reroute commands, follower delivery and sources without fresh completion readiness retain their existing paths.
12
+
7
13
  ## 0.57.0: Thread Continuity and Shared Settings
8
14
 
9
15
  - `Config`: Thread display now lives in shared top-level `threads.displayMode`, beside the other Settings preferences; profiles hold only bot identity and the paired owner. The old `profiles.<name>.threadDisplayMode` key is not migrated and is ignored, so configs without the new key use the Letters default until Settings saves a choice.
@@ -269,7 +269,7 @@ interface TelegramLifecycleBindingDeps {
269
269
  shutdownGenerativeAppLiveSurfaces?: () => void;
270
270
  resolveAutomaticThreadCleanupEnabled?: () => boolean | Promise<boolean>;
271
271
  buttonActionStore: OutboundHandlers.TelegramButtonActionStore;
272
- callMultipart: OutboundHandlers.TelegramVoiceReplySenderDeps["sendMultipart"];
272
+ callMultipart: (...args: Parameters<TelegramApi.TelegramBridgeApiRuntime["callMultipart"]>) => Promise<unknown>;
273
273
  sendChatAction: NonNullable<OutboundHandlers.TelegramVoiceReplySenderDeps["sendChatAction"]>;
274
274
  sendRecordVoiceAction: NonNullable<OutboundHandlers.TelegramVoiceReplySenderDeps["sendRecordVoiceAction"]>;
275
275
  sendMarkdownReply: Queue.TelegramAgentEndHookRuntimeDeps<Queue.PendingTelegramTurn, Pi.ExtensionContext, Pi.AgentEndEvent["messages"][number], Keyboard.TelegramInlineKeyboardMarkup>["sendMarkdownReply"];
@@ -278,6 +278,7 @@ interface TelegramLifecycleBindingDeps {
278
278
  onSteerTurnEnd?: TelegramQueueBindingRuntime<Pi.ExtensionContext>["onTurnEnd"];
279
279
  onPromptHandedOff?: (turn: Queue.PendingTelegramTurn, ctx: Pi.ExtensionContext) => void;
280
280
  answerGuestQuery: TelegramApi.TelegramBridgeApiRuntime["answerGuestQuery"];
281
+ editGuestInlineMessage?: TelegramApi.TelegramBridgeApiRuntime["editGuestInlineMessage"];
281
282
  deleteMessage: TelegramApi.TelegramBridgeApiRuntime["deleteMessage"];
282
283
  sendGuestReply: NonNullable<Queue.TelegramAgentEndHookRuntimeDeps<Queue.PendingTelegramTurn, Pi.ExtensionContext, Pi.AgentEndEvent["messages"][number], Keyboard.TelegramInlineKeyboardMarkup>["sendGuestReply"]>;
283
284
  editGuestReply?: Queue.TelegramAgentEndHookRuntimeDeps<Queue.PendingTelegramTurn, Pi.ExtensionContext, Pi.AgentEndEvent["messages"][number], Keyboard.TelegramInlineKeyboardMarkup>["editGuestReply"];
@@ -300,5 +301,5 @@ interface TelegramLifecycleBindingDeps {
300
301
  /** Settled-session hint for owners that wait on agent idleness, such as old-tab cleanup after a live rebind. */
301
302
  onSessionSettled?: (ctx: Pi.ExtensionContext) => void;
302
303
  }
303
- export declare function registerTelegramLifecycleRuntimeHooks({ pi, publicationRuntime, activityRuntime, activityVerbosityRuntime, diagnostics, assistantOutputRuntime, sessionLifecycleRuntime, configStore, abort, typing, lifecycle, activeTurnRuntime, telegramQueueStore, modelSwitchController, previewRuntime, promptDispatchRuntime, deferredQueueDispatchRuntime, modelContextAvailabilityRuntime, disconnectOnQuit, onSessionStarted, shutdownGenerativeAppLiveSurfaces, resolveAutomaticThreadCleanupEnabled, buttonActionStore, callMultipart, sendChatAction, sendRecordVoiceAction, sendMarkdownReply, sendTextReply, dispatchNextQueuedTelegramTurn, onSteerTurnEnd, onPromptHandedOff, answerGuestQuery, deleteMessage, sendGuestReply, editGuestReply, stopGuestPlaceholder, preparePreviewDelivery, finalizeMarkdownPreview, proactivePushTargetGetter, getAssistantRenderingMode, recordMessageOwnership, canSendAgentActivity, isSessionContextActive, getSessionGeneration, isTurnTransportActive, updateStatus, recordRuntimeEvent, onSessionSettled, }: TelegramLifecycleBindingDeps): void;
304
+ export declare function registerTelegramLifecycleRuntimeHooks({ pi, publicationRuntime, activityRuntime, activityVerbosityRuntime, diagnostics, assistantOutputRuntime, sessionLifecycleRuntime, configStore, abort, typing, lifecycle, activeTurnRuntime, telegramQueueStore, modelSwitchController, previewRuntime, promptDispatchRuntime, deferredQueueDispatchRuntime, modelContextAvailabilityRuntime, disconnectOnQuit, onSessionStarted, shutdownGenerativeAppLiveSurfaces, resolveAutomaticThreadCleanupEnabled, buttonActionStore, callMultipart, sendChatAction, sendRecordVoiceAction, sendMarkdownReply, sendTextReply, dispatchNextQueuedTelegramTurn, onSteerTurnEnd, onPromptHandedOff, answerGuestQuery, editGuestInlineMessage, deleteMessage, sendGuestReply, editGuestReply, stopGuestPlaceholder, preparePreviewDelivery, finalizeMarkdownPreview, proactivePushTargetGetter, getAssistantRenderingMode, recordMessageOwnership, canSendAgentActivity, isSessionContextActive, getSessionGeneration, isTurnTransportActive, updateStatus, recordRuntimeEvent, onSessionSettled, }: TelegramLifecycleBindingDeps): void;
304
305
  export {};
@@ -657,7 +657,7 @@ export function registerTelegramCommandsAndTools({ pi, agentDir, configStore, pe
657
657
  },
658
658
  });
659
659
  }
660
- export function registerTelegramLifecycleRuntimeHooks({ pi, publicationRuntime, activityRuntime, activityVerbosityRuntime, diagnostics, assistantOutputRuntime, sessionLifecycleRuntime, configStore, abort, typing, lifecycle, activeTurnRuntime, telegramQueueStore, modelSwitchController, previewRuntime, promptDispatchRuntime, deferredQueueDispatchRuntime, modelContextAvailabilityRuntime, disconnectOnQuit, onSessionStarted, shutdownGenerativeAppLiveSurfaces, resolveAutomaticThreadCleanupEnabled, buttonActionStore, callMultipart, sendChatAction, sendRecordVoiceAction, sendMarkdownReply, sendTextReply, dispatchNextQueuedTelegramTurn, onSteerTurnEnd, onPromptHandedOff, answerGuestQuery, deleteMessage, sendGuestReply, editGuestReply, stopGuestPlaceholder, preparePreviewDelivery, finalizeMarkdownPreview, proactivePushTargetGetter, getAssistantRenderingMode, recordMessageOwnership, canSendAgentActivity, isSessionContextActive = () => true, getSessionGeneration, isTurnTransportActive, updateStatus, recordRuntimeEvent, onSessionSettled, }) {
660
+ export function registerTelegramLifecycleRuntimeHooks({ pi, publicationRuntime, activityRuntime, activityVerbosityRuntime, diagnostics, assistantOutputRuntime, sessionLifecycleRuntime, configStore, abort, typing, lifecycle, activeTurnRuntime, telegramQueueStore, modelSwitchController, previewRuntime, promptDispatchRuntime, deferredQueueDispatchRuntime, modelContextAvailabilityRuntime, disconnectOnQuit, onSessionStarted, shutdownGenerativeAppLiveSurfaces, resolveAutomaticThreadCleanupEnabled, buttonActionStore, callMultipart, sendChatAction, sendRecordVoiceAction, sendMarkdownReply, sendTextReply, dispatchNextQueuedTelegramTurn, onSteerTurnEnd, onPromptHandedOff, answerGuestQuery, editGuestInlineMessage, deleteMessage, sendGuestReply, editGuestReply, stopGuestPlaceholder, preparePreviewDelivery, finalizeMarkdownPreview, proactivePushTargetGetter, getAssistantRenderingMode, recordMessageOwnership, canSendAgentActivity, isSessionContextActive = () => true, getSessionGeneration, isTurnTransportActive, updateStatus, recordRuntimeEvent, onSessionSettled, }) {
661
661
  const agentEndResetter = Runtime.createTelegramAgentEndResetter({
662
662
  abort,
663
663
  typing,
@@ -677,7 +677,7 @@ export function registerTelegramLifecycleRuntimeHooks({ pi, publicationRuntime,
677
677
  recordOwnership: recordMessageOwnership,
678
678
  recordRuntimeEvent,
679
679
  });
680
- const sendGuestAttachment = async (turn, attachment, caption) => {
680
+ const sendGuestAttachment = async (turn, attachment, caption, options) => {
681
681
  const stagingTarget = proactivePushTargetGetter();
682
682
  const stagingChatId = stagingTarget?.chatId;
683
683
  if (stagingChatId === undefined) {
@@ -685,11 +685,19 @@ export function registerTelegramLifecycleRuntimeHooks({ pi, publicationRuntime,
685
685
  }
686
686
  await OutboundAttachments.deliverTelegramGuestCachedAttachment({
687
687
  guestQueryId: turn.guestQueryId,
688
+ guestInlineMessageId: turn.guestInlineMessageId,
689
+ editGuestInlineMessage,
690
+ isDeliveryActive: options?.isDeliveryActive,
688
691
  stagingChatId,
689
692
  stagingTarget,
690
693
  attachment,
691
694
  caption,
692
- sendMultipart: callMultipart,
695
+ sendMultipart: (method, fields, fileField, path, name) => callMultipart(method, fields, fileField, path, name, {
696
+ assertAuthority() {
697
+ if (options?.isDeliveryActive?.() === false)
698
+ throw new Error("Guest attachment upload lost authority.");
699
+ },
700
+ }),
693
701
  answerGuestQuery: (guestQueryId, result) => answerGuestQuery(guestQueryId, undefined, { result }),
694
702
  answerGuestText: (guestQueryId, text) => answerGuestQuery(guestQueryId, text),
695
703
  fallbackText: caption ||
@@ -339,9 +339,9 @@ export function createTelegramBusAwareApiRuntime(deps) {
339
339
  ? deps.directRuntime.answerGuestQueryForInlineMessage(guestQueryId, text, options)
340
340
  : rejectTelegramDirectOwnership("answerGuestQueryForInlineMessage");
341
341
  },
342
- editGuestInlineMessage(inlineMessageId, content) {
342
+ editGuestInlineMessage(inlineMessageId, content, options) {
343
343
  return deps.ownsDirect()
344
- ? deps.directRuntime.editGuestInlineMessage(inlineMessageId, content)
344
+ ? deps.directRuntime.editGuestInlineMessage(inlineMessageId, content, options)
345
345
  : rejectTelegramDirectOwnership("editGuestInlineMessage");
346
346
  },
347
347
  async deleteMessage(chatId, messageId) {
@@ -627,6 +627,8 @@ export interface TelegramSelectedCommandAdmission {
627
627
  /** Independent exact recipient/target lifetime, including inside captured menu transport adapters. */
628
628
  assertRecipientCurrent(): void;
629
629
  reportCompleted(): boolean;
630
+ /** Optional recipient release barrier after acceptance; rejection suppresses effects without replay. */
631
+ beforeEffects?(): Promise<void>;
630
632
  }
631
633
  export declare function createTelegramCommandHandler<TMessage extends TelegramCommandRuntimeMessage, TContext>(deps: TelegramCommandRuntimeDeps<TMessage, TContext>): ((commandName: string | undefined, message: TMessage, ctx: TContext, commandArgs?: string) => Promise<boolean>) & {
632
634
  /** Command-only producer execution; Routing must supply the captured plan and released exact-source authority. */
@@ -1667,7 +1667,7 @@ export function createTelegramCommandHandler(deps) {
1667
1667
  const captured = { ...deps }, parsed = { ...command }, message = parsed.name === "status"
1668
1668
  ? copySelectedMenuCarrier(original)
1669
1669
  : copySelectedReplyAnchor(original);
1670
- const assertRecipientCurrent = admission.assertRecipientCurrent, report = admission.reportCompleted;
1670
+ const assertRecipientCurrent = admission.assertRecipientCurrent, report = admission.reportCompleted, beforeEffects = admission.beforeEffects;
1671
1671
  const effects = parsed.name === "status"
1672
1672
  ? undefined
1673
1673
  : {
@@ -1693,8 +1693,13 @@ export function createTelegramCommandHandler(deps) {
1693
1693
  throw new Error(`Selected ${parsed.name} plan owner is unavailable.`);
1694
1694
  };
1695
1695
  return issueSelectedOnce(assertCurrent, async () => {
1696
- if (plan.completion === "before-effect" && !report.call(admission))
1697
- return false;
1696
+ if (plan.completion === "before-effect") {
1697
+ if (!report.call(admission))
1698
+ return false;
1699
+ if (beforeEffects)
1700
+ await beforeEffects();
1701
+ assertRecipientCurrent();
1702
+ }
1698
1703
  return prepared.run({
1699
1704
  message,
1700
1705
  command: parsed,
@@ -1720,7 +1725,7 @@ export function createTelegramCommandHandler(deps) {
1720
1725
  ...copySelectedReplyAnchor(original),
1721
1726
  from: { ...original.from },
1722
1727
  };
1723
- const assertRecipientCurrent = admission.assertRecipientCurrent, reportCompleted = admission.reportCompleted;
1728
+ const assertRecipientCurrent = admission.assertRecipientCurrent, reportCompleted = admission.reportCompleted, beforeEffects = admission.beforeEffects;
1724
1729
  const assertAdmissionCurrent = createSelectedSemanticGuard(admission, () => captured.assertExecutionCurrent?.(original), false);
1725
1730
  const assertSemanticCurrent = () => {
1726
1731
  assertAdmissionCurrent();
@@ -1732,7 +1737,7 @@ export function createTelegramCommandHandler(deps) {
1732
1737
  persistAllowedUserId: async () => {
1733
1738
  throw new Error(`Selected ${name} cannot acquire pairing authority.`);
1734
1739
  },
1735
- }, { assertSemanticCurrent, assertRecipientCurrent, reportCompleted }));
1740
+ }, { assertSemanticCurrent, assertRecipientCurrent, reportCompleted, beforeEffects }));
1736
1741
  };
1737
1742
  const prepareSelectedConfirmation = (name) => (command, messages, ctx, admission) => {
1738
1743
  const original = messages[0], captured = { ...deps };
@@ -2036,7 +2041,7 @@ export function createTelegramCommandHandler(deps) {
2036
2041
  typeof admission.reportCompleted !== "function")
2037
2042
  return undefined;
2038
2043
  const assertSourceCurrent = admission.assertSourceCurrent, assertRecipientCurrent = admission.assertRecipientCurrent;
2039
- const reportCompleted = admission.reportCompleted, assertExecutionCurrent = deps.assertExecutionCurrent;
2044
+ const reportCompleted = admission.reportCompleted, beforeEffects = admission.beforeEffects, assertExecutionCurrent = deps.assertExecutionCurrent;
2040
2045
  const assertIssuanceCurrent = () => {
2041
2046
  assertSourceCurrent.call(admission);
2042
2047
  assertExecutionCurrent?.call(deps, original);
@@ -2060,8 +2065,14 @@ export function createTelegramCommandHandler(deps) {
2060
2065
  openSettingsMenu: menuDeps.openSettingsMenu &&
2061
2066
  ((value, context) => menuDeps.openSettingsMenu(value, context, recipientOptions)),
2062
2067
  };
2063
- return issueSelectedOnce(assertIssuanceCurrent, async () => reportCompleted.call(admission) &&
2064
- handleTelegramCommandRuntime(name, message, ctx, recipientDeps, args));
2068
+ return issueSelectedOnce(assertIssuanceCurrent, async () => {
2069
+ if (!reportCompleted.call(admission))
2070
+ return false;
2071
+ if (beforeEffects)
2072
+ await beforeEffects();
2073
+ assertRecipientCurrent();
2074
+ return handleTelegramCommandRuntime(name, message, ctx, recipientDeps, args);
2075
+ });
2065
2076
  },
2066
2077
  });
2067
2078
  }
@@ -2157,6 +2168,8 @@ async function handleTelegramHelpCommand(commandName, message, ctx, deps, admiss
2157
2168
  assertSemanticCurrent();
2158
2169
  if (!admission.reportCompleted())
2159
2170
  return false;
2171
+ if (admission.beforeEffects)
2172
+ await admission.beforeEffects();
2160
2173
  admission.assertRecipientCurrent();
2161
2174
  }
2162
2175
  const assertEffectCurrent = admission?.assertRecipientCurrent ??
@@ -1033,6 +1033,29 @@ export default function (pi) {
1033
1033
  delivery: deliveryLifecycleRuntime,
1034
1034
  api: telegramApiTargetActivityRuntime,
1035
1035
  });
1036
+ const prepareTemporaryThreadJournalInspection = function (target, requiredJournalBindingKeys, ownInputs) {
1037
+ const binding = resolveTelegramUpdateJournalBinding();
1038
+ const botToken = configStore.getBotToken();
1039
+ if (!binding || !botToken)
1040
+ return undefined;
1041
+ const profile = configStore.getActiveProfileName() ?? "default";
1042
+ const botIdentity = Journal.createTelegramUpdateJournalBotIdentity({ botToken, botId: getTelegramBotId() });
1043
+ return {
1044
+ directory: Paths.resolveTelegramTempDir(), profile, botIdentity,
1045
+ pollingPath: telegramLeaderJournalPath.resolve(configStore.getActiveProfileName()),
1046
+ requiredJournalBindingKeys: [binding.recoveryKey, ...requiredJournalBindingKeys],
1047
+ ...(target.threadId !== undefined && ownInputs
1048
+ ? { cleanup: { target: { chatId: target.chatId, threadId: target.threadId }, ownInputs } } : {}),
1049
+ limits: { maxDirectoryEntries: 10_000, maxFiles: 4096, maxBytes: 64 * 1024 * 1024,
1050
+ maxEntries: 10_000, maxWork: 100_000 },
1051
+ withSourceReference(path, operation) {
1052
+ return telegramJournalReferenceRegistry.withReference({
1053
+ referenceClass: "operator-disposition",
1054
+ recoveryKey: Journal.createTelegramUpdateJournalBindingKey({ path, profileName: profile, botIdentity }),
1055
+ }, operation);
1056
+ },
1057
+ };
1058
+ };
1036
1059
  const inboundRouteRuntime = Routing.createTelegramInboundRouteRuntime({
1037
1060
  configStore,
1038
1061
  callApi: callTelegramApi,
@@ -1112,53 +1135,24 @@ export default function (pi) {
1112
1135
  });
1113
1136
  },
1114
1137
  inspectTemporaryThreadSources(target, requiredJournalBindingKeys, ownInputs) {
1115
- const binding = resolveTelegramUpdateJournalBinding();
1116
- const botToken = configStore.getBotToken();
1117
- if (!binding || !botToken)
1138
+ const input = prepareTemporaryThreadJournalInspection(target, requiredJournalBindingKeys, ownInputs);
1139
+ if (!input)
1118
1140
  return undefined;
1119
- const profile = configStore.getActiveProfileName() ?? "default";
1120
- const botIdentity = Journal.createTelegramUpdateJournalBotIdentity({
1121
- botToken,
1122
- botId: getTelegramBotId(),
1123
- });
1124
1141
  return withTelegramJournalSourceSerialization(function () {
1125
- return Journal.isTelegramThreadCleanupJournalNamespaceClear({
1126
- directory: Paths.resolveTelegramTempDir(),
1127
- profile,
1128
- botIdentity,
1129
- pollingPath: telegramLeaderJournalPath.resolve(configStore.getActiveProfileName()),
1130
- requiredJournalBindingKeys: [
1131
- binding.recoveryKey,
1132
- ...requiredJournalBindingKeys,
1133
- ],
1134
- ...(target.threadId !== undefined && ownInputs
1135
- ? {
1136
- cleanup: {
1137
- target: { chatId: target.chatId, threadId: target.threadId },
1138
- ownInputs,
1139
- },
1140
- }
1141
- : {}),
1142
- limits: {
1143
- maxDirectoryEntries: 10_000,
1144
- maxFiles: 4096,
1145
- maxBytes: 64 * 1024 * 1024,
1146
- maxEntries: 10_000,
1147
- maxWork: 100_000,
1148
- },
1149
- withSourceReference(path, operation) {
1150
- return telegramJournalReferenceRegistry.withReference({
1151
- referenceClass: "operator-disposition",
1152
- recoveryKey: Journal.createTelegramUpdateJournalBindingKey({
1153
- path,
1154
- profileName: profile,
1155
- botIdentity,
1156
- }),
1157
- }, operation);
1158
- },
1159
- })
1160
- ? []
1161
- : undefined;
1142
+ return Journal.isTelegramThreadCleanupJournalNamespaceClear(input) ? [] : undefined;
1143
+ });
1144
+ },
1145
+ recoverTemporaryThreadDeadQueueOwners(target, requiredJournalBindingKeys, ownInputs, isCurrent) {
1146
+ const input = prepareTemporaryThreadJournalInspection(target, requiredJournalBindingKeys, ownInputs);
1147
+ if (!input || !isCurrent())
1148
+ return 0;
1149
+ return Journal.recoverTelegramThreadCleanupDeadQueueOwners({
1150
+ ...input, isCurrent, withSourceSerialization: withTelegramJournalSourceSerialization,
1151
+ recoveryOwner: {
1152
+ instanceId: telegramInstanceId, processId: telegramProcessId,
1153
+ processBirthId: telegramQueueProcessBirthId,
1154
+ sessionGeneration: telegramSessionContextStore.getGeneration(),
1155
+ },
1162
1156
  });
1163
1157
  },
1164
1158
  hasWorkspaceRestoreAuthority() {
@@ -2485,6 +2479,7 @@ export default function (pi) {
2485
2479
  deleteMessage: deleteTelegramMessage,
2486
2480
  sendGuestReply,
2487
2481
  editGuestReply,
2482
+ editGuestInlineMessage: telegramApiRuntime.editGuestInlineMessage,
2488
2483
  stopGuestPlaceholder: guestPlaceholderRuntime.stop,
2489
2484
  finalizeMarkdownPreview,
2490
2485
  preparePreviewDelivery,
@@ -751,6 +751,13 @@ export declare function isTelegramThreadCleanupJournalNamespaceClear(input: Tele
751
751
  }[];
752
752
  };
753
753
  }): boolean;
754
+ /** Demand-driven recovery only; caller holds temporary-cleanup admission. Never a deletion grant. */
755
+ export declare function recoverTelegramThreadCleanupDeadQueueOwners(input: Parameters<typeof isTelegramThreadCleanupJournalNamespaceClear>[0] & {
756
+ recoveryOwner: TelegramUpdateJournalQueueOwnerIdentity;
757
+ isCurrent: () => boolean;
758
+ withSourceSerialization: NonNullable<TelegramUpdateJournalStoreOptions["withSourceSerialization"]>;
759
+ getQueueProcessLiveness?: TelegramUpdateJournalStoreOptions["getQueueProcessLiveness"];
760
+ }): number;
754
761
  export declare function publishTelegramUpdateJournalSegment(path: string, segment: TelegramUpdateJournalSegment): TelegramUpdateJournalSegmentPublicationResult;
755
762
  export declare function createTelegramUpdateQueueHandoffToken(): string;
756
763
  export declare function createTelegramUpdateJournalBotIdentity(input: {
@@ -1988,6 +1988,86 @@ export function isTelegramThreadCleanupJournalNamespaceClear(input) {
1988
1988
  return namespace.sources.every((source) => source.evidence.kind === "absent" ||
1989
1989
  source.evidence.file.entries.every((entry) => isUnrelatedPlainInput(entry, source.path)));
1990
1990
  }
1991
+ /** Demand-driven recovery only; caller holds temporary-cleanup admission. Never a deletion grant. */
1992
+ export function recoverTelegramThreadCleanupDeadQueueOwners(input) {
1993
+ return input.withSourceSerialization(() => {
1994
+ const assertCurrent = () => {
1995
+ if (!input.isCurrent())
1996
+ throw new Error("Temporary cleanup queue recovery lost authority.");
1997
+ };
1998
+ assertCurrent();
1999
+ if (!input.cleanup)
2000
+ return 0;
2001
+ // Validate exact references and complete namespace coverage before considering any mutation.
2002
+ if (isTelegramThreadCleanupJournalNamespaceClear(input))
2003
+ return 0;
2004
+ const namespace = inspectTelegramSessionJournalNamespace(input);
2005
+ if (namespace.retainedInputs?.some(value => value.state !== "committed") ||
2006
+ input.requiredJournalBindingKeys.some(key => !namespace.sources.some(source => source.path === getTelegramUpdateJournalBindingPath(key) && source.evidence.kind === "present")))
2007
+ return 0;
2008
+ const plans = [];
2009
+ for (const source of namespace.sources) {
2010
+ if (source.evidence.kind !== "present")
2011
+ continue;
2012
+ const file = source.evidence.file;
2013
+ if (file.version !== TELEGRAM_UPDATE_JOURNAL_VERSION)
2014
+ continue;
2015
+ const groups = new Map();
2016
+ for (const entry of file.entries) {
2017
+ if (!entry.queueReceiptId)
2018
+ continue;
2019
+ const group = groups.get(entry.queueReceiptId) ?? [];
2020
+ group.push(entry);
2021
+ groups.set(entry.queueReceiptId, group);
2022
+ }
2023
+ for (const group of groups.values()) {
2024
+ const entry = group[0];
2025
+ if (entry.state !== "queued" || !entry.queueReceiptId || !entry.queueOwner ||
2026
+ !entry.queueKind || entry.queueHandoff || entry.inputClaim)
2027
+ continue;
2028
+ if (group.some(value => value.state !== "queued" || value.queueHandoff || value.inputClaim ||
2029
+ value.queueKind !== entry.queueKind || !isDeepStrictEqual(value.queueOwner, entry.queueOwner)))
2030
+ continue;
2031
+ plans.push({ path: source.path, recovery: {
2032
+ queueKind: entry.queueKind, receiptId: entry.queueReceiptId,
2033
+ sourceUpdateIds: group.map(value => value.updateId).sort((a, b) => a - b),
2034
+ deadOwner: cloneJournalQueueOwner(entry.queueOwner), recoveryOwner: { ...input.recoveryOwner },
2035
+ } });
2036
+ }
2037
+ }
2038
+ const liveness = input.getQueueProcessLiveness ?? getTelegramProcessLiveness;
2039
+ const dead = plans.filter(plan => {
2040
+ assertCurrent();
2041
+ const result = liveness(plan.recovery.deadOwner);
2042
+ assertCurrent();
2043
+ return result === "dead";
2044
+ });
2045
+ let removed = 0;
2046
+ for (const plan of dead) {
2047
+ assertCurrent();
2048
+ removed += input.withSourceReference(plan.path, () => {
2049
+ const journal = createTelegramUpdateJournalStore({
2050
+ path: plan.path, profileName: input.profile, botIdentity: input.botIdentity,
2051
+ queueRuntimeIdentity: input.recoveryOwner,
2052
+ // This private synchronous store stays inside the cross-family serializer already held above.
2053
+ withSourceSerialization(operation) { assertCurrent(); return operation(); },
2054
+ sourceAccess: { directory: dirname(plan.path), limits: input.limits },
2055
+ getQueueProcessLiveness(owner) {
2056
+ assertCurrent();
2057
+ const result = liveness(owner);
2058
+ assertCurrent();
2059
+ return result;
2060
+ },
2061
+ withWriterAdmission(operation) { assertCurrent(); return operation(); },
2062
+ });
2063
+ // The journal transaction rechecks whole-group identity, absence of handoff and fresh process-death proof.
2064
+ return journal.recoverDeadQueueOwner(plan.recovery).recoveredUpdateIds.length;
2065
+ });
2066
+ }
2067
+ assertCurrent();
2068
+ return removed;
2069
+ });
2070
+ }
1991
2071
  function cloneEntry(entry) {
1992
2072
  return {
1993
2073
  ...entry,
@@ -73,6 +73,7 @@ export interface TelegramQueuedOutboundAttachmentView {
73
73
  export interface TelegramOutboundAttachmentQueueTargetView {
74
74
  queuedAttachments: TelegramQueuedOutboundAttachmentView[];
75
75
  guestQueryId?: string;
76
+ guestInlineMessageId?: string;
76
77
  }
77
78
  export interface TelegramQueuedOutboundAttachmentTurnView extends TelegramOutboundAttachmentQueueTargetView {
78
79
  chatId: number;
@@ -161,6 +162,17 @@ export declare function queueTelegramOutboundAttachments(options: {
161
162
  }): Promise<TelegramOutboundAttachmentToolResult>;
162
163
  export declare function deliverTelegramGuestCachedAttachment(options: {
163
164
  guestQueryId: string;
165
+ guestInlineMessageId?: string;
166
+ editGuestInlineMessage?: (inlineMessageId: string, content: {
167
+ media: {
168
+ type: "photo" | "document" | "audio";
169
+ media: string;
170
+ caption?: string;
171
+ };
172
+ }, options: {
173
+ assertAuthority: () => void;
174
+ }) => Promise<void>;
175
+ isDeliveryActive?: () => boolean;
164
176
  stagingChatId: number;
165
177
  stagingTarget?: TelegramTarget;
166
178
  attachment: TelegramQueuedOutboundAttachmentView;
@@ -375,6 +375,9 @@ export async function queueTelegramOutboundAttachments(options) {
375
375
  maxAttachmentSizeBytes: options.maxAttachmentSizeBytes,
376
376
  statPath: options.statPath,
377
377
  });
378
+ if (options.activeTurn.guestInlineMessageId && pendingAttachments.some((attachment) => getTelegramGuestAttachmentTransport(attachment.path).fileField === "voice")) {
379
+ throw new Error("Telegram Guest Mode cannot replace an inline placeholder with a voice note; no attachment was queued. Use MP3 audio or a normal bot chat.");
380
+ }
378
381
  options.activeTurn.queuedAttachments.push(...pendingAttachments);
379
382
  const added = pendingAttachments.map((attachment) => attachment.path);
380
383
  return {
@@ -389,6 +392,16 @@ export async function queueTelegramOutboundAttachments(options) {
389
392
  }
390
393
  export async function deliverTelegramGuestCachedAttachment(options) {
391
394
  const transport = getTelegramGuestAttachmentTransport(options.attachment.path);
395
+ const isActive = () => options.isDeliveryActive?.() !== false;
396
+ const assertActive = () => {
397
+ if (!isActive())
398
+ throw new Error("Guest attachment delivery lost authority.");
399
+ };
400
+ if (!isActive())
401
+ return;
402
+ if (options.guestInlineMessageId && (!options.editGuestInlineMessage || transport.fileField === "voice")) {
403
+ throw new Error("Guest inline attachment editing is unavailable for this media.");
404
+ }
392
405
  let stagingMessageId;
393
406
  let answerAttempted = false;
394
407
  try {
@@ -397,6 +410,8 @@ export async function deliverTelegramGuestCachedAttachment(options) {
397
410
  ...getTelegramMultipartTargetFields(options.stagingTarget),
398
411
  }, transport.fileField, options.attachment.path, options.attachment.fileName));
399
412
  stagingMessageId = message.message_id;
413
+ if (!isActive())
414
+ return;
400
415
  const caption = options.caption
401
416
  ? Array.from(options.caption).slice(0, 1024).join("")
402
417
  : undefined;
@@ -446,11 +461,26 @@ export async function deliverTelegramGuestCachedAttachment(options) {
446
461
  ...(caption ? { caption } : {}),
447
462
  };
448
463
  }
464
+ if (!isActive())
465
+ return;
449
466
  answerAttempted = true;
450
- await options.answerGuestQuery(options.guestQueryId, result);
467
+ if (options.guestInlineMessageId && options.editGuestInlineMessage) {
468
+ if (result.type === "voice")
469
+ throw new Error("Voice notes cannot replace inline placeholders.");
470
+ const media = result.type === "photo" ? result.photo_file_id
471
+ : result.type === "audio" ? result.audio_file_id : result.document_file_id;
472
+ await options.editGuestInlineMessage(options.guestInlineMessageId, {
473
+ media: { type: result.type, media, ...(caption ? { caption } : {}) },
474
+ }, { assertAuthority: assertActive });
475
+ }
476
+ else {
477
+ await options.answerGuestQuery(options.guestQueryId, result);
478
+ }
451
479
  }
452
480
  catch (error) {
453
- if (!answerAttempted && options.answerGuestText && options.fallbackText) {
481
+ if (!isActive())
482
+ return;
483
+ if (!options.guestInlineMessageId && !answerAttempted && options.answerGuestText && options.fallbackText) {
454
484
  answerAttempted = true;
455
485
  await options.answerGuestText(options.guestQueryId, options.fallbackText);
456
486
  }
@@ -459,7 +489,7 @@ export async function deliverTelegramGuestCachedAttachment(options) {
459
489
  }
460
490
  }
461
491
  finally {
462
- if (stagingMessageId !== undefined) {
492
+ if (stagingMessageId !== undefined && isActive()) {
463
493
  try {
464
494
  await options.deleteMessage(options.stagingChatId, stagingMessageId);
465
495
  }
@@ -506,10 +506,14 @@ export interface TelegramAgentEndRuntimeDeps<TTurn extends PendingTelegramTurn,
506
506
  }) => Promise<void>;
507
507
  sendGuestReply?: (guestQueryId: string, markdown: string) => Promise<void>;
508
508
  /** Replaces the early guest ACK with the final text. */
509
- editGuestReply?: (inlineMessageId: string, markdown: string) => Promise<void>;
509
+ editGuestReply?: (inlineMessageId: string, markdown: string, options?: {
510
+ assertAuthority?: () => void;
511
+ }) => Promise<void>;
510
512
  /** Cancels the animated guest placeholder before the final edit. */
511
513
  stopGuestPlaceholder?: (inlineMessageId: string) => Promise<void>;
512
- sendGuestAttachment?: (turn: TTurn, attachment: QueuedAttachment, caption?: string) => Promise<void>;
514
+ sendGuestAttachment?: (turn: TTurn, attachment: QueuedAttachment, caption?: string, options?: {
515
+ isDeliveryActive?: () => boolean;
516
+ }) => Promise<void>;
513
517
  sendGuestVoiceReply?: (turn: TTurn, plan: TelegramAgentEndOutboundReplyPlan<TReplyMarkup>, caption?: string) => Promise<void>;
514
518
  planOutboundReply?: (markdown: string) => TelegramAgentEndOutboundReplyPlan<TReplyMarkup>;
515
519
  sendOutboundReplyArtifacts?: (turn: TTurn, plan: TelegramAgentEndOutboundReplyPlan, options?: {