@llblab/pi-kit 0.7.0 → 0.7.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 (30) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +1 -1
  3. package/node_modules/@llblab/pi-telegram/AGENTS.md +2 -2
  4. package/node_modules/@llblab/pi-telegram/BACKLOG.md +3 -2
  5. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +6 -1
  6. package/node_modules/@llblab/pi-telegram/README.md +1 -1
  7. package/node_modules/@llblab/pi-telegram/docs/architecture.md +8 -6
  8. package/node_modules/@llblab/pi-telegram/docs/public-api.md +4 -4
  9. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +18 -0
  10. package/node_modules/@llblab/pi-telegram/lib/bus-api.ts +32 -19
  11. package/node_modules/@llblab/pi-telegram/lib/bus.ts +5 -0
  12. package/node_modules/@llblab/pi-telegram/lib/channel-posts.ts +189 -15
  13. package/node_modules/@llblab/pi-telegram/lib/commands.ts +3 -0
  14. package/node_modules/@llblab/pi-telegram/lib/config.ts +67 -4
  15. package/node_modules/@llblab/pi-telegram/lib/extension.ts +65 -6
  16. package/node_modules/@llblab/pi-telegram/lib/locks.ts +6 -1
  17. package/node_modules/@llblab/pi-telegram/lib/outbound-attachments.ts +49 -3
  18. package/node_modules/@llblab/pi-telegram/lib/preview.ts +17 -0
  19. package/node_modules/@llblab/pi-telegram/lib/prompts.ts +1 -0
  20. package/node_modules/@llblab/pi-telegram/lib/queue.ts +66 -8
  21. package/node_modules/@llblab/pi-telegram/lib/rendering.ts +4 -1
  22. package/node_modules/@llblab/pi-telegram/lib/replies.ts +19 -0
  23. package/node_modules/@llblab/pi-telegram/lib/routing.ts +39 -0
  24. package/node_modules/@llblab/pi-telegram/lib/setup.ts +44 -4
  25. package/node_modules/@llblab/pi-telegram/lib/status.ts +41 -4
  26. package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +74 -18
  27. package/node_modules/@llblab/pi-telegram/lib/turns.ts +7 -0
  28. package/node_modules/@llblab/pi-telegram/package.json +1 -1
  29. package/package.json +2 -2
  30. /package/node_modules/@llblab/pi-telegram/lib/{logs.ts → logging.ts} +0 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  All notable changes to `@llblab/pi-kit` are documented here.
4
4
 
5
+ ## 0.7.1 - 2026-09-11
6
+
7
+ - `Telegram Hotfix`: Advances the exact Telegram pin to `0.45.1`, bringing immediate editable Guest Mode acknowledgements, concise guest delivery guidance, environment-backed bot-token references, and durable photo/video channel publication with caption edits and retry-safe media identity. The package set and resource order remain unchanged.
8
+
5
9
  ## 0.7.0 - 2026-09-10
6
10
 
7
11
  - `Current Distribution`: Advances Actors to `0.53.0`, State Flow to `0.6.0`, Telegram to `0.45.0`, and Skills to `1.15.0`. The kit now includes durable Workspace-bound Telegram Threads, expanded run orchestration, current opt-in state handoffs, and the latest portable workflows while preserving the package set, resource inventory, and explicit load order.
package/README.md CHANGED
@@ -15,7 +15,7 @@ Package links lead to the owning repositories for usage, documentation, issues,
15
15
  | [`@llblab/pi-codex-usage`](https://github.com/llblab/pi-codex-usage) | `0.9.4` | Compact Codex/Spark subscription-limit status |
16
16
  | [`@llblab/pi-grow-loop`](https://github.com/llblab/pi-grow-loop) | `0.7.5` | Visible continuation scheduling and bounded worker Skills |
17
17
  | [`@llblab/pi-state-flow`](https://github.com/llblab/pi-state-flow) | `0.6.0` | Opt-in validated state handoffs between agent runs |
18
- | [`@llblab/pi-telegram`](https://github.com/llblab/pi-telegram) | `0.45.0` | Telegram companion, queues, files, voice, controls, and Generative Apps guidance |
18
+ | [`@llblab/pi-telegram`](https://github.com/llblab/pi-telegram) | `0.45.1` | Telegram companion, queues, files, voice, controls, and Generative Apps guidance |
19
19
  | [`@llblab/skills`](https://github.com/llblab/skills) | `1.15.0` | Portable workflows for engineering, review, design, context maintenance, and other focused tasks |
20
20
 
21
21
  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.
@@ -68,7 +68,7 @@ Use the relevant local skill before non-trivial work in its domain. Keep skill o
68
68
  ### 4.2 Ownership, Sessions, And Trust
69
69
 
70
70
  - The bridge is session-local and paired to one allowed Telegram user. Preserve `{ chatId, threadId? }` through every inbound, queue, callback, reaction, media, preview, reply, menu, voice, attachment, and direct-delivery path.
71
- - First-contact pairing grants in-memory authority only after profile/token/execution-fenced durable publication confirms that exact user; it never overwrites another configured owner. Sender admission precedes user message/edit/callback/reaction delegation, including foreign ownership and unbound-Thread fallback paths. Reactions require an existing exact human owner; private chat type is not authorization. Queued config persistence must not replay observed authority as local grant edits or erase later local unpair. Setup and retry details belong in [`docs/architecture.md`](./docs/architecture.md#setup-flow).
71
+ - First-contact pairing grants in-memory authority only after profile/token/execution-fenced durable publication confirms that exact user; it never overwrites another configured owner. `profiles.<name>.botToken` may store an exact `$NAME`/`${NAME}` environment reference instead of a copied secret: resolve it only at validation or activation boundaries, fail closed with a redacted named-variable diagnostic when unresolved, and keep literal tokens compatible. Sender admission precedes user message/edit/callback/reaction delegation, including foreign ownership and unbound-Thread fallback paths. Reactions require an existing exact human owner; private chat type is not authorization. Queued config persistence must not replay observed authority as local grant edits or erase later local unpair. Setup and retry details belong in [`docs/architecture.md`](./docs/architecture.md#setup-flow).
72
72
  - Telegram transport ownership is not semantic queue ownership. Losing the exact transport lock must not erase accepted local queue work or stop valid local Pi dispatch; direct Bot API mutations fail closed until exact direct or follower authority exists.
73
73
  - `tmp/telegram/owners.json` is the sole transport-owner authority. Cross-process read/check/write operations serialize transactionally and acquisition, refresh, release, takeover, and irreversible leader work fence the exact owner/epoch. `state.json` and `logs.jsonl` are diagnostics, never routing authority.
74
74
  - 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.
@@ -96,7 +96,7 @@ Use the relevant local skill before non-trivial work in its domain. Keep skill o
96
96
  - `/stop`, `/abort`, `/next`, and `/continue` respectively reset+abort, abort while preserving queue, force the next turn, and enqueue a control-lane continuation. Abort-history folding applies only to Telegram-owned active turns.
97
97
  - Telegram extension side effects must not hold Pi's core lifecycle hostage after semantic completion. Preserve ordering in extension-owned background work, record failures, and fence target/profile/transport/session authority.
98
98
  - Complete assistant/guest model answers use Telegram-native Rich Markdown. Harness-owned menus, status, diagnostics, thinking, and tool evidence remain explicit HTML/plain or their documented native surface. Before Telegram preview or final delivery, strip every assistant-authored HTML comment regardless of Markdown position while keeping action activation top-level-only; a comment-only result sends no text message. Preserve literal code outside comments and structurally safe chunking; never split invalid markup.
99
- - `preview` owns streaming lifecycle only, not assistant rendering. Finalization waits for active preview flushes and must not issue pre/post-final draft-clear calls that create transient Telegram draft UI.
99
+ - `preview` owns streaming lifecycle only, not assistant rendering. Finalization waits for active preview flushes and must not issue pre/post-final draft-clear calls that create transient Telegram draft UI. Turns that already answer as one atomic reply (voice replies, Guest Mode queries) never stream previews.
100
100
  - Native `sendChatAction(typing)` is the automatic activity signal for unsettled agent and compaction work while Telegram transport is authorized. Extension-owned blocking UI prompts pause it and completion resumes it while either work owner remains active. Do not invent extra in-chat work indicators or emit activity for startup/connect/reload/recovery alone.
101
101
  - Public activity handlers and connected companion delivery are asynchronous, target-bound, generation-fenced surfaces. Connected companion projection has no independent opt-out: disconnect or authority loss is its boundary. Token deltas, hidden reasoning, unknown sources, and stale authority never enter public projection.
102
102
  - Thread display defaults to the profile-scoped Letters strategy, with Directories as the other automatic choice. A durable manual Thread display name retained on its Workspace binding overrides either until exact reset; keep generated/recovery identity separate from manual and acknowledged display fields. UI labels, emoji semantics, navigation, settings controls, callback namespaces, voice behavior, command templates, and assistant markup follow the linked `/docs` contracts. Generated human-readable prompt-button labels use `emoji + space + text`; emoji-free text is only a reasoned no-semantic-marker fallback. Non-spatial generated controls default to top-level vertical cells, with nested rows reserved for unmistakably compact peers. Do not restate other evolving UI details here.
@@ -2,6 +2,7 @@
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
+ - [ ] `Channel multimedia posts` (`0.45.1`, live-acceptance-gated): `telegram_message` channel delivery accepts one local `.jpg`/`.jpeg`/`.png`/`.webp` photo or `.mp4` video, uploads it through the multipart transport as `sendPhoto`/`sendVideo` with `text` as the HTML caption, validates kind and size (photo ≤ 10 MiB, video ≤ 50 MiB) plus ≤ 1024 visible caption characters before issuance, and rejects unsupported types and albums instead of downgrading them to links. The channel-post journal binds kind/file name/byte size/SHA-256 and caption, so duplicate requests and lost acknowledgements never re-upload; media-post edits replace the caption through `editMessageCaption`, and Markdown spoilers render as `<tg-spoiler>`. Live image publication passed on `@llb_log`. Regressions cover confirmed publication, duplicate requests, lost ACK, pre-issuance rejection, caption edits, and reconnect replacement. Remaining: operator-authorized disposable-channel acceptance of rejected upload, duplicate request, and caption edit.
5
6
  - [ ] `Manual Thread naming` (`gated-but-preparable`, release priority): Local bot-owned `/name Name` and bare `/name` flows avoid model dispatch. One expiring exact-target input dialog immediately accepts the next valid name, always offers cancel, and offers **Reset to automatic** only while a manual override exists; duplicate/stale callbacks cannot repeat mutation. Durable manual override supersedes Letters/Directories, reset is leader/follower generation- and target-fenced, Letters is the default, and legacy Names resolves to Letters without rewriting recovery identity. Local review findings are remediated, including Bot-API-wait target-replacement regressions for leader/follower rename and reset. Remaining: disposable live acceptance for command-menu ordering, dialog, invalid input, duplicate callbacks, leader/follower rename and reset.
6
7
  - [ ] `OMP schema acceptance` ([#267](https://github.com/llblab/pi-telegram/issues/267), `human-/environment-gated`): Local emitted-schema, Pi process, and llama.cpp source checks now prove explicit recursive JSON values, root `$defs`, only supported local `#/...` references, and no bare boolean schema. Confirm one connected `telegram_bind` request through the reporter's OMP + llama-server build before closing interoperability acceptance; do not treat this progressive external check as a Pi release blocker.
7
8
  - [ ] [`Workspace operator gates`](./docs/multi-instance-bus.md#approved-next-contract-directory-names-and-reclaimable-slots): Complete the remaining external evidence for display modes, unique slots, and durable recovery. Local recovery and snapshot-equality reviews are closed; do not repeat them without changed relevant inputs. No live deletion, commit, publication, restart, or automatic retirement activation belongs to local implementation authority.
@@ -24,11 +25,11 @@ _This file owns unresolved project work only. Completed behavior belongs in `CHA
24
25
  - [ ] `Admission integration and acceptance` (`local-actionable`, after runtime): Wire pending/denied/allowed decisions before every user-content routing shortcut and bootstrap path, apply the reviewed durable grant boundary, update setup/UI documentation, and cover backlog/restart, no-UI, conflicting owner, failure/retry, and configured-owner compatibility. Run full validation and independent post-integration review. Actual terminal-dialog and Telegram acceptance remain operator-authorized disposable tests, not local-mock claims.
25
26
  - [ ] `Agent-owned channel posts` (`external-blocked`, required for 0.45.0): Extend explicit numeric/`@username` channel delivery with one bounded profile-scoped durable journal containing only posts created by this agent path. A disconnected v1 store now validates exact profile/token identity, capacity and records; its idempotent prepare/begin/confirm CAS leaves issued work outcome-unknown until an exact Bot API result is recorded and survives restart. The public-`@username` direct-leader path now prepares and begins the exact tool-call operation before `sendRichMessage`, confirms the returned numeric channel/message identity, and refuses replay of retained unknown outcomes. `telegram_channel_posts` now exposes bounded local listing without Bot API reads. `telegram_channel_post` now drives exact retained edit/delete transitions through direct-leader `editMessageText`/`deleteMessage`; tool-call identity fences unknown outcomes and successful confirmation is idempotent. Numeric channel delivery now requires explicit `channel: true`, direct-leader ownership, and matching `getChat` channel identity before using the same journal; confirmed records retain bounded observed username/title metadata. Cross-process races now prove one durable publication/edit/delete issuance and no post-restart regrant. Strict reads reject links, foreign/loose files, identity races, unsupported no-follow platforms, and oversized input before parse. There is no legacy post schema to migrate: absent state starts empty, unknown versions/fields fail closed, and the 0.44 downgrade check leaves the inert file byte-for-byte untouched because old code cannot issue post effects. Explicit successful listing is the only tool surface that returns retained authored Markdown; channel send/list/mutation failures and runtime events now use fixed messages without content, token, path, or arbitrary transport detail. The production-wired publication helper now proves lost-success-ACK and ambiguous-send retries invoke transport once, then return retained success or refuse from outcome-unknown. Fresh replacement-store regressions now prove lost successful ACK and ambiguous send cannot replay across helper instance replacement/restart. A native entrypoint regression now disconnects/reconnects the direct leader and proves both lost success and ambiguous outcomes reuse durable authority without a second send. Local implementation and evidence are complete. Closure requires operator-authorized live create/edit/delete checks against a disposable channel with real sender rights; no live channel effect is authorized by this backlog work. Persist intent before the non-idempotent send; after a successful Bot API response retain canonical numeric channel ID, observed username/title when available, exact message ID, authored content, timestamps and current state. Commit-unknown publication must remain outcome-unknown and never auto-replay; edit/delete retries require exact current records. Do not enumerate admin channels, ingest arbitrary channel history, or treat missing local records as Telegram absence. Direct leader ownership, explicit user intent, sender rights, redaction, cross-process serialization, retention limits, migration/downgrade behavior and lost-ACK duplicate prevention are release gates.
26
27
  - [ ] `Thread Cleanup Manager` (`local-actionable`): A disconnected pure planner now returns candidates only from unique exact inactive binding/target snapshots with matching all-clear live-owner, accepted-work, and delivery-authority evidence; malformed/unknown/competing reservation, provision, or cleanup state yields no candidate, and ordering never treats age as authority. A disconnected strict profile/token-scoped work-set now persists exact candidates, records one exact Workspace deletion permit as `prepared → outcome-unknown` across restart, rejects mismatched permits, and records idempotent `deleted` confirmation under bounded atomic serialization. A disconnected executor now requires an injected exclusive Workspace deletion boundary across fresh evidence planning, exact retained-snapshot comparison, permit acquisition/recording, delete callback, and confirmation; the future fence owner must acquire/recheck in ledger order rather than nesting fence acquisition inside an admission lease; regressions prove drift stops before permit, blocked/already-issued cannot fabricate deletion authority, and ambiguous delete remains outcome-unknown without replay. A production-shaped evidence adapter now preserves each full Workspace record while resolving external protection, then snapshots exact cleanup fields plus reservations, provisions, and cleanup intents; resolver failures become `unknown` and source failures propagate fail-closed. Production composition now runs a truthful **Review inactive tabs** control under profile-wide Workspace admission, durably prepares one canonical 128-bit-digest work-set, and opens a separate summary with proven count, explicit no-deletion state, and Back navigation. An exact 62-byte confirmation callback contract now accepts only canonical work-set IDs and renders **Clean inactive tabs** only when a destructive port exists; production intentionally omits that port, so review remains non-destructive. Do not route manual cleanup through `acquireRetirementFence()`: retirement remains pressure-only. The single admission-ledger fence now accepts discriminated `pressure-retirement | manual-thread-cleanup` authority, resolves legacy missing kind as pressure, exposes `acquireThreadCleanupFence()`, carries kind into exact permit identity, and still allows only one profile-wide fence. Kind-specific cleanup adopt/issue/absence/release/complete APIs now preserve existing retirement callers; pressure methods reject manual fences and cleanup methods reject pressure fences before mutation. A disconnected permit runtime now acquires cleanup fences, revalidates under them, releases drifted unissued fences, returns already-issued without replay, and retains `commit-ready` until an injected durable commit succeeds. `threads` now exposes an exact inactive-binding cleanup commit that refuses stale/protected snapshots, atomically removes one binding, and treats fence-protected absence as commit-unknown retry success. Commit composition removes the binding first, then confirms the work-set; failure retains `commit-ready`. A hidden coordinator now validates canonical review identity, resolves full bindings, records the sole permit before one injected delete call, commits binding then work-set, and completes the fence. Fake-port regressions prove successful completion, commit-ready recovery without a second delete, and permanent no-replay after ambiguous delete. A typed Settings-port adapter now exposes this coordinator only when explicitly composed; Settings reports deleted, outcome-unknown, and blocked counts. Fake-port tests exercise the full callback lifecycle while production composition still omits `cleanInactiveThreads`. Settings-port recovery now adopts only an exact retained cleanup fence when an injected authority proof permits takeover; a live/unverifiable predecessor remains outcome-unknown. A proven successor preserves phase, resumes `commit-ready`, completes binding/work-set commit, and never repeats deletion. The retained candidate is now sufficient exact commit evidence: `threads` compares its cwd/workspace/instance/global-slot/binding/target/inactivity/update identity before removal and accepts fence-protected absence on retry. `commit-ready` recovery no longer needs the deleted full binding or calls its resolver. Cross-process worker races now prove two stale `prepared` contenders cause exactly one fake transport deletion. The work-set permit CAS is authoritative: a loser cannot delete after its permit record is rejected, and a redundant fence over an already-deleted entry settles without transport replay. Work-set publication fault hooks now prove both pre-rename failure and lost post-rename acknowledgement: `commit-ready` survives, durable `deleted` is reconciled, the redundant fence completes, and transport remains one-shot. Fence acquisition authority-change races return blocked rather than escaping the Settings callback. Binding-snapshot fault tests now cover the existing synchronous commit fence: pre-rename ownership loss reloads and restores the exact binding for `commit-ready` retry, while a thrown post-rename acknowledgement reloads durable absence and reports success. Together with work-set prefixes, every local binding/work-set/fence commit order now avoids transport replay. Focused security/recovery review found and closed a profile-composition wedge: candidate/work-set profile must equal the runtime profile before fence acquisition, retained fences include the same profile in exact recovery, and adoption captures one stable successor owner snapshot. Cross-profile input now blocks before revalidation or ledger mutation. Local mechanics are approved with notes. The optional Settings port now returns redacted operator recovery classes (`commit-ready`, `deletion-outcome-unknown`, `authority-blocked`) derived from exact retained fence authority; no target, path, token, or transport detail is exposed. Production remains gated on disabled composition and authorized live acceptance. Review admission must release before cleanup-fence acquisition; under that fence, revalidate exact full binding/protection, issue the sole permit, record it in the work-set, call delete once, confirm absence, commit binding/work-set, then complete the fence. Admit only exact bindings with durable inactivity evidence, matching profile/slot/target/generation, no live or unverifiable owner, and no accepted/active work, provisioning, handoff, or competing cleanup. Revalidate immediately before server-side `deleteForumTopic`; retain outcome-unknown attempts without replay, and never infer eligibility from age, silence, delayed heartbeat, or client cache state. Bot API deletion may converge clients but cannot prove mobile/desktop tab synchronization.
27
- - [ ] `Environment-backed bot tokens`: Let each profile store an exact Pi-native `$ENV_VAR` or `${ENV_VAR}` reference in `telegram.json` instead of copying the resolved secret; preserve the originating alias when `/telegram-setup` prefills from a supported environment variable, resolve only at validation/activation boundaries, fail closed with a redacted named-variable diagnostic when unresolved, retain literal-token compatibility, and cover named profiles, reload, persistence, status, and secret-redaction paths.
28
+ - [ ] `Environment-backed bot tokens` (`live-acceptance-gated`): Each profile stores an exact `$ENV_VAR` or `${ENV_VAR}` reference in `telegram.json` instead of copying the resolved secret. The config store resolves references only at validation/activation boundaries, including pairing identity hashing and workspace admission keys, while persistence keeps the reference; `/telegram-setup` prefills the first supported alias, validates the resolved value, and persists the alias; literal tokens remain compatible; unresolved or malformed references fail closed with a redacted named-variable diagnostic in setup, connect, locked-polling start, and status. Tests cover default and named profiles, reload/persistence, resolved pairing admission, setup prefill/validation/diagnostic, and status rendering. Remaining: on the live bridge, run `/telegram-setup` with no stored token and `TELEGRAM_BOT_TOKEN` set and confirm `telegram.json` stores `$TELEGRAM_BOT_TOKEN`; then unset the variable and confirm setup and `/telegram-status` name the variable without exposing a value; finally switch a named profile to an env reference. Operator-authorized disposable checks only.
28
29
  - [ ] [`Inference bypass Generative Apps`](./docs/generative-apps.md): Let the agent install and bind one managed JavaScript app owner for a generated-prompt prefix so deterministic controls can bypass model inference without creating another button grammar.
29
30
  - [ ] Harden the implemented `telegram_bind` installation/invocation kernel with removal operations, stronger cross-process replacement recovery, and bounded diagnostics while preserving lifecycle cancellation, worker-isolated methods, explicit staged replacement, canonical `<agent-dir>/genapps/<app>/<app>.mjs` identity, mandatory named `init`, no manifest/package metadata, non-symlink roots/sources, and fail-closed silent replacement.
30
31
  - [ ] Harden the implemented pre-queue `app::method` / strict-JSON bound-action route with revision capture for agent-mediated initial surfaces, profile/target authority, follower transport evidence, voice output delivery, and commit-unknown diagnostics while preserving generation-plus-revision stale-click rejection, fail-closed malformed/absent methods, ordinary model prompts, and native single-colon callbacks.
31
32
  - [ ] Harden the implemented cross-process transition lock, dead-owner recovery, expected-generation/revision comparison, repaired partial-tail/current-state recovery, transactional `init` reset, and output-only methods with process-birth proof, bounded lock diagnostics, more interruption points, and explicit commit-unknown evidence.
32
33
  - [ ] Complete the capability-owned Music Player adapter evidence beyond the successful real `ffplay` singleton install and no-model-turn bound `next`, `pause`, resume/`play`, and terminal `stop` Controls: exercise generated relative-volume controls over arbitrary absolute Actor percentages, `toggle`, `previous`, `status`, compatible singleton reuse, checkpoint restart, missing/terminal Run, unavailable backend, process timeout/cancellation/stream bounds, and redacted errors while keeping actor reality authoritative and rejecting generic remote-terminal methods.
33
34
  - [ ] Extend the implemented new-message default plus opt-in `viewMode: "edit"` bound-action update into optional output-only `refresh` scheduling from `refreshAfterMs`, clamped to at least two seconds and serialized after prior completion; retain one latest logical view handle per app/profile/target, skip unchanged frame digests, honor Telegram retry/backoff, cancel on lifecycle replacement, stop and forget the handle on known deletion or message-not-found, and never recreate a deleted live view without a fresh user action.
34
- - [ ] Cover CML and JSON equivalence, both mutually exclusive Tool shapes, direct agent-authored app discovery, install/copy and existing-app `init`, scalar/object/no-argument methods, agent-side diagnostic invocation, bounded CLI adaptation, refresh coalescing/rate limits/backoff/deletion, identity/path traversal, duplicate/overlapping prefixes, state recovery, handler failure, stale clicks, session replacement, follower routing, and the invariant that bound actions perform no model turn.
35
+ - [ ] Cover CML and JSON equivalence, both mutually exclusive Tool shapes, direct agent-authored app discovery, install/copy and existing-app `init`, scalar/object/no-argument methods, agent-side diagnostic invocation, bounded CLI adaptation, refresh coalescing/rate limits/backoff/deletion, identity/path traversal, duplicate/overlapping prefixes, state recovery, handler failure, stale clicks, session replacement, follower routing, and the invariant that bound actions perform no model turn. `tests/generative-apps.test.ts` now covers scalar/object/no-argument argument shapes, throwing-method failure containment (state and journal untouched), and bounded argument/output/state/module limits before durable mutation; the remaining items stay open.
@@ -4,7 +4,12 @@
4
4
 
5
5
  ## Unreleased
6
6
 
7
- - No unreleased changes.
7
+ ## 0.45.1: Guest Mode And Channel Media Hotfixes
8
+
9
+ - `Environment-backed bot tokens`: `telegram.json` profiles may store an exact `$NAME`/`${NAME}` reference instead of a copied token. Resolution happens only at validation/activation boundaries, including pairing identity hashing; setup prefills the first supported alias, validates the resolved value, and persists the alias; literals stay compatible; unresolved references fail closed with a redacted named-variable diagnostic in setup, connect, and status.
10
+ - `Guest Mode`: Records a failed guest answer as a delivery runtime event instead of rejecting the agent-end hook, so an expired guest query no longer surfaces as a Pi extension error or skips the next queued turn; the Pi-failure notice answer is contained identically. Every guest query now receives a bold `⚙️` ACK and later replaces it through `editMessageText` with the returned `inline_message_id`; an owner-run live acceptance confirmed this update path.
11
+ - `Guest Speed`: Guest turns never emit streaming draft previews, because one guest query allows exactly one answer and cannot be patched afterward. Their prompt ends with the compact transport hint `[guest] delivery: answer quickly with one concise, self-contained reply`, aligned with the existing `[voice] delivery: …` context style.
12
+ - `Channel Media`: `telegram_message` channel delivery uploads one .jpg/.jpeg/.png/.webp photo or .mp4 video with `text` as its HTML caption, validating kind, size (photo ≤ 10 MiB, video ≤ 50 MiB), and 1024 visible caption characters; unsupported types and albums are rejected. The channel-post journal binds kind/file name/size/SHA-256 plus caption, so duplicates and lost acknowledgements never re-upload, and media edits replace captions via `editMessageCaption`. Markdown spoilers render as `<tg-spoiler>`; live image publication passed.
8
13
 
9
14
  ## 0.45.0: Durable Workspace Threads
10
15
 
@@ -47,7 +47,7 @@ Run this inside Pi:
47
47
  /telegram-setup
48
48
  ```
49
49
 
50
- Paste the bot token. If `~/.pi/agent/telegram.json` already contains a saved token, setup offers it as the default. If no saved token exists, setup can prefill from `TELEGRAM_BOT_TOKEN`, `TELEGRAM_BOT_KEY`, `TELEGRAM_TOKEN`, or `TELEGRAM_KEY`. Bot/session identity persists under `profiles.default`; shared handlers and assistant/voice/time settings remain top-level. `/telegram-setup default` and `/telegram-connect default` are exact aliases for the bare commands. Use `/telegram-setup <name>` only when you want an additional bot profile. Cancelling or failing named-profile token validation leaves the currently active profile and polling runtime unchanged; setup reports the profile as saved and connected only after polling startup succeeds.
50
+ Paste the bot token. If `~/.pi/agent/telegram.json` already contains a saved token, setup offers it as the default. If no saved token exists, setup prefills the first supported alias (`TELEGRAM_BOT_TOKEN`, `TELEGRAM_BOT_KEY`, `TELEGRAM_TOKEN`, or `TELEGRAM_KEY`) as an environment reference such as `$TELEGRAM_BOT_TOKEN`, validates the resolved value, and persists the reference instead of copying the secret. Bot/session identity persists under `profiles.default`; shared handlers and assistant/voice/time settings remain top-level. `/telegram-setup default` and `/telegram-connect default` are exact aliases for the bare commands. Use `/telegram-setup <name>` only when you want an additional bot profile. Cancelling or failing named-profile token validation leaves the currently active profile and polling runtime unchanged; setup reports the profile as saved and connected only after polling startup succeeds.
51
51
 
52
52
  ### 3. Connect this Pi instance and its active session
53
53
 
@@ -76,13 +76,13 @@ The repository uses a **Flat Domain DAG**:
76
76
  - `workspace-retirement`: Profile/leader-fenced pressure preparation over the store snapshot. It counts standalone reservations, selects one candidate only at full slot capacity, rechecks protection, and persists/resumes an exact durable intent. Workspace bindings accumulate their historical follower-journal routing keys and distinguish complete fresh metadata from incomplete legacy evidence. Its read-only accepted-work policy resolves those binding-specific journals plus the shared leader journal and combines them with local exact targets, failing closed when source coverage or target decoding is incomplete. It can prune a known empty follower-journal key only from complete readable evidence plus explicit writer quiescence under exact binding/profile/epoch fences and an exact-target admission lease held through durable publication. Incomplete legacy bindings consume discovered hashed journals as target-scoped evidence but remain incomplete so every later retirement repeats discovery. The shared Workspace operation runtime serializes topic lifecycle, reroute restoration/reclamation, provisioning, delayed post-provision reconciliation, follower/manual cleanup, rename, and display mutation through one exposed gate. Detached mutation work must reacquire fresh admission rather than inherit a lease already released by its caller. A successor may durably adopt one exact stale-epoch intent after profile/binding/protection revalidation; direct old-epoch execution remains blocked. The isolated executor consumes that gate and requires the durable admission ledger. It acquires or exactly adopts the matching fence, rechecks protection after admissions close, advances to `deletion-issued` before invoking an executor-only `deleteForumTopic` port with the sole permit, and never reissues from that phase. Success or exact absence advances to `commit-ready`; store commit failure retains the fence, and exact completion follows durable binding+intent removal. A successor resolves an issued unknown outcome only through a separate exact-absence probe. Leader composition exposes exact registry, active/queued work, known journals, and profile-exact legacy discovery as protection evidence. Missing queue targets and incomplete reads remain unknown. The common direct Bot API client counts exact JSON/multipart targets until settlement; message-scoped edits/deletes without a thread conservatively protect every binding in their chat. Known historical follower owner keys decode to process-birth identity before liveness checks. Durable intents block matching claims and binding mutations. Preparation/adoption/execution remains disconnected from leader runtime by release scope. Independent review cleared the admission-composition blocker; live deletion and operator acceptance remain separate gates.
77
77
  - `threads`: Telegram UI thread/tab binding state mapped to Bot API `message_thread_id` / `ForumTopic` transport. Owns exact-`cwd` Workspace bindings and transient claims, first-proven inactivity metadata, fail-closed retirement occupancy snapshots, and exact durable retirement intents, leader/current-instance identity state, active-turn → follower → leader target preference, matching status projection assembly, profile-bound same-process handoff, exact-claim global-slot allocation and conservative missing/duplicate legacy migration, collision-safe compact thread-name selection, Workspace-aware rename persistence, and primitive provision helpers. Its optional external-slot source makes every generic allocation, Workspace claim, and occupancy snapshot reserve retained admission-fence slots; malformed, unreadable, or non-uppercase evidence fails allocation closed. Its synchronous provision-commit helper transfers exact targeted creation-title evidence into the claim-committed Workspace binding and consumes matching pending evidence; callers retain admission, epoch checks, and durable publication. It should not turn dormant bindings into routing authority, own destructive cleanup policy, or grow into the general Telegram synchronization domain.
78
78
  - `updates` / `routing`: update classification, authorization, callbacks, edits, reactions, forwarding, and inbound composition. `updates` owns production journal workers, leader/follower admission lifecycle construction, binding and settlement selection, queue-handoff projection across recipient journals/admission/IPC/live queue state, process/session queue-owner projection, post-public source binding, exact-signal late settlement, durable receipt readiness, same-process claim reconstruction, and structural worker state. `routing` converts message, callback, guest, section, reroute, and control admissions into exact receipts; its complete unbound-target and reroute restore/reclaim handlers run under the shared profile-wide Workspace operation boundary before store access.
79
- - `media` / `text-groups` / `time-injection` / `turns` / `inbound`: inbound extraction, rich reply plaintext, grouped debounce, split-text coalescing, optional time context, handlers, and prompt assembly/editing. Group replay replaces stale generation-local message/report bindings without duplicating content.
79
+ - `media` / `text-groups` / `time-injection` / `turns` / `inbound`: inbound extraction, rich reply plaintext, grouped debounce, split-text coalescing, optional time context, handlers, and prompt assembly/editing, including the `[guest]` Guest Mode speed note appended to guest turn text. Group replay replaces stale generation-local message/report bindings without duplicating content.
80
80
  - `queue`: queue contracts, transport stamps, lanes, readiness, mutations, dispatch, enqueueing, and lifecycle sequencing. Durable admission uses deterministic receipts, canonical source sets, replay dedupe, multiple folded-history receipts, append-before-dispatch reporting, exact handoff/control/discard settlement, and a readiness gate. Receipt-bearing inactive-profile work is preserved after current-profile work rather than dropped.
81
81
  - `runtime`: session-local coordination primitives: counters, flags, setup guard, abort handler, typing timers, dispatch flags, and reset binding.
82
82
  - `model` / `menu-model` / `menu-thinking` / `menu-status` / `menu-queue` / `menu-settings` / `menu` / `commands`: model identity, thinking levels, scoped model handling, menu render/callback behavior, slash commands, bot commands, and interactive controls.
83
83
  - `sections`: Telegram menu-section registry, opaque section callback tokens, render/callback dispatch, safe section ports, and diagnostics.
84
84
  - `keyboard`: shared inline-keyboard reply-markup shape only; feature domains own labels, callback data, and behavior.
85
- - `preview` / `replies` / `rendering`: throttled native Rich Markdown draft delivery, native final reply delivery, reply parameters, transport-limit chunking, and remaining Telegram HTML rendering for bridge-owned UI/compatibility surfaces.
85
+ - `preview` / `replies` / `rendering`: throttled native Rich Markdown draft delivery, native final reply delivery, reply parameters, transport-limit chunking, and remaining Telegram HTML rendering (bold, italic, strikethrough, spoilers, code, links) for bridge-owned UI/compatibility surfaces.
86
86
  - `delivery`: public extension operational-view delivery, active-turn/instance/aggregate/authorized target policy, logical chunk handles, per-target ordering, runtime generation fencing, and the process-local runtime membrane. Its bridge adapter composes the established UI/compat reply renderer with narrow bus-aware Telegram API and ownership ports; it never exposes bot clients or Pi contexts.
87
87
  - `activity`: public normalized Pi lifecycle registration, activity/source identity, assistant segment and reasoning normalization, executed-tool events, non-blocking per-handler queues, delivery contexts, compatibility adapters, and shutdown fencing. The same domain extends assistant-output observation for connected companion projection: eligible completed local/autonomous public segments retain source order and deduplicate event identity. `bindings` assembles observation, authority, sender, and failure-projection ports; routing owns exact delivery authority, outbound composes established transformations and reply delivery, and Bot API domains implement transport. No separate proactive state-machine domain exists.
88
88
  - `outbound-markup`: top-level assistant action comment/fence parsing, shared JSON/CML grammar, attribute parsing, voice reply planning, and preview/delivery stripping.
@@ -133,9 +133,11 @@ First-contact pairing publishes through a profile-exact config-store operation,
133
133
  `/telegram-setup` progressively resolves the bot token:
134
134
 
135
135
  1. Use the locally saved token when present.
136
- 2. Otherwise use the first supported Telegram token environment variable.
136
+ 2. Otherwise use the first supported Telegram token environment variable, prefilled as an exact `$NAME` reference instead of the resolved secret.
137
137
  3. Otherwise show the example placeholder.
138
138
 
139
+ `profiles.<name>.botToken` may hold a literal token (compatibility) or an exact `$NAME`/`${NAME}` environment reference. References are resolved only at validation and activation boundaries: setup validates the resolved value while persisting the alias, the config store exposes the resolved token to transport and identity hashing, and every other boundary keeps the stored reference. An unresolved reference fails closed with a redacted named-variable diagnostic, and a `$`-prefixed value that is not a valid reference is malformed rather than a literal secret.
140
+
139
141
  `ctx.ui.input()` only supports placeholder text, so setup uses `ctx.ui.editor()` when a real default must appear already filled in. Bare and explicit `default` setup/connect commands address the same `profiles.default` entry. Persisted config is written through a private temp file plus atomic rename and left with `0600` permissions. On first load, legacy root identity moves into `profiles.default` in that same serialized atomic transaction when no conflicting canonical value exists; identical duplicates collapse, complementary fields merge, and conflicts reject the load without modifying the file.
140
142
 
141
143
  ### Automatic Pairing Confirmation Design
@@ -537,10 +539,10 @@ Assistant delivery guarantees:
537
539
 
538
540
  - Model-authored Markdown is the source of truth; the bridge does not pre-render assistant Markdown to HTML unless the operator selects `assistant.rendering: "html"` for compatibility.
539
541
  - Before native Rich Markdown delivery, the bridge normalizes known Bot-API-fragile source forms without changing visible meaning, including space-after-marker blockquotes and dollar-prefixed ticker atoms that Telegram may otherwise treat as unterminated math.
540
- - Prompt context blocks use compact metadata (`[tag|key:value]`) as the stable inbound contract. `[telegram...]` names the current surface only: owner/current turns use `[telegram]` or `[telegram|thread:<name>]`; guest-mode turns use `[telegram|guest:<group-title-or-peer-username-or-id>]`. In a private Guest Mode turn the paired owner's `from` identity is never the guest: the remote private-chat identity wins, then non-owner caller metadata, with a non-bot replied peer available only as a final identity fallback when stronger conversation evidence is absent; username falls back to the remote display name and numeric id. Reply attribution still belongs independently in `[reply|from:...]`, and a replied bot can never define or replace the current `[telegram|guest:...]` location identity. Source authors for quoted/forwarded material and their files are carried by `[reply|from:<username-or-id>]`, `[forward|from:<username-or-id>]`, and `[attachments|from:<username-or-id>]`, while plain `[attachments]` remains current-turn attachments and is ordered before reply/forward/source context. Media embedded in inbound Telegram `rich_message` blocks is downloaded like ordinary message media and stays attached to its forward-source block instead of being mislabeled as current-user material.
542
+ - Prompt context blocks use compact metadata (`[tag|key:value]`) as the stable inbound contract. `[telegram...]` names the current surface only: owner/current turns use `[telegram]` or `[telegram|thread:<name>]`; guest-mode turns use `[telegram|guest:<group-title-or-peer-username-or-id>]`. In a private Guest Mode turn the paired owner's `from` identity is never the guest: the remote private-chat identity wins, then non-owner caller metadata, with a non-bot replied peer available only as a final identity fallback when stronger conversation evidence is absent; username falls back to the remote display name and numeric id. Reply attribution still belongs independently in `[reply|from:...]`, and a replied bot can never define or replace the current `[telegram|guest:...]` location identity. Source authors for quoted/forwarded material and their files are carried by `[reply|from:<username-or-id>]`, `[forward|from:<username-or-id>]`, and `[attachments|from:<username-or-id>]`, while plain `[attachments]` remains current-turn attachments and is ordered before reply/forward/source context. Media embedded in inbound Telegram `rich_message` blocks is downloaded like ordinary message media and stays attached to its forward-source block instead of being mislabeled as current-user material. Guest-mode turns append a `[guest]` block to their turn text stating the one-reply/limited-window constraint and instructing a fast, concise, self-contained answer; the note travels with the turn text rather than the system prompt.
541
543
  - Quoted rich replies use Telegram `rich_message` blocks as the prompt-context source when available, so `[reply]` context receives rendered plain text instead of raw `InputRichMessage.markdown` fallback text. Replied media runs through the same inbound handlers and voice transcription providers as current-message media, with provenance-scoped `[outputs|from:…]` appended inside the reply block.
542
544
  - Long native Markdown replies are split only at Telegram Rich Message transport limits; oversized fenced code, display-math, and fully wrapped inline-formatting blocks are rewrapped per chunk so persisted Rich Markdown chunks remain structurally valid.
543
- - When Draft previews are enabled, streaming previews pass structurally closed assistant Markdown prefixes through to `sendRichMessageDraft` with ownership checks, voice suppression, and serialized flushes. Unclosed inline spans, links, fenced code, comments, and display-math blocks are held back until a safe boundary exists. Draft failures are recorded and the failing frame is skipped instead of degrading to raw plain-message previews, because partial Markdown can be invalid while the final message remains valid.
545
+ - When Draft previews are enabled, streaming previews pass structurally closed assistant Markdown prefixes through to `sendRichMessageDraft` with ownership checks, voice and guest-turn suppression, and serialized flushes. Telegram Guest Mode allows exactly one answer that cannot be patched afterward, so guest turns never start preview state even while `assistant.draftPreviews` is enabled. Unclosed inline spans, links, fenced code, comments, and display-math blocks are held back until a safe boundary exists. Draft failures are recorded and the failing frame is skipped instead of degrading to raw plain-message previews, because partial Markdown can be invalid while the final message remains valid.
544
546
  - Preview flushes are serialized so older edits cannot race newer drafts; final delivery waits for active draft flushes and does not perform a post-final draft-clear call. Successful final text delivery clears the local pending preview text only while the captured session and transport remain active, so a late delivery or Rich-attachment cleanup cannot erase replacement preview state.
545
547
 
546
548
  UI/compat rendering guarantees:
@@ -554,7 +556,7 @@ Final delivery attaches reply metadata only where requested. Reply parameters ap
554
556
 
555
557
  Outbound files staged during an active Telegram turn are delivered after that turn completes but before any separate final text. Final delivery clears an existing preview first so an edited older message cannot appear above a later upload. Files use `telegram_attach`, are checked atomically per tool call, and use configurable size limits before photo/document upload. When no Telegram turn is active, `telegram_attach` sends files immediately to the paired/default chat, an assigned follower thread, or an explicit `chat_id` plus optional `thread_id`; `telegram_message` provides direct local/TUI Markdown text delivery for explicit user requests and runs the same `telegram_button` markup planner so buttons attach to that text message. Direct local/TUI delivery is singleton-controlled: classic mode requires this Pi instance to own `/telegram-connect`, while Threaded Mode followers must be registered and route through the leader-owned transport. Already accepted active-turn reply/attachment delivery remains session-local.
556
558
 
557
- The channel-post journal records only this agent path's own publication intent. `prepare` is exact and idempotent; `beginPublication` durably changes one prepared operation to outcome-unknown before any future non-idempotent send; only `confirmPublished` records the returned numeric channel/message identity. A retry never regains issuance from outcome-unknown or published state. The store validates profile and token fingerprint, refuses malformed/duplicate/over-capacity state, and lists newest retained records without claiming Telegram history. The public-`@username` direct-leader sender now uses the tool-call ID to prepare and begin this journal transition before `sendRichMessage`, then records the returned numeric channel/message identity plus bounded username/title observed by `getChat`. Retained outcome-unknown state refuses automatic replay. Bounded agent-facing listing is available locally. Exact edit/delete journal transitions fence each tool-call mutation as outcome-unknown before direct-leader `editMessageText` or `deleteMessage`, and require the same mutation ID to confirm edited or deleted state. Numeric-channel publication requires explicit `channel: true`; direct-leader `getChat` must prove the exact negative ID and channel type before issuance, and the send response must preserve that identity. Cross-process regressions prove one winner for concurrent publication/edit/delete issuance and no restart regrant. Reads fail closed before parsing links, foreign/loose files, unsupported no-follow platforms, identity races, or oversized input. No legacy post schema is migrated: absent state starts empty and unknown versions/fields fail closed. The 0.44 downgrade checker leaves this inert journal untouched because old code cannot issue its effects. Only an explicit successful list returns retained authored Markdown. Channel publication/list/mutation failures and runtime events use fixed messages without retained content, token, path, or arbitrary transport detail. Production-helper regressions model a lost successful caller ACK and an ambiguous send response: the same operation returns retained success or refuses outcome-unknown without a second transport call. The retry uses a freshly opened replacement store, proving durable behavior across helper replacement/restart. A native entrypoint regression disconnects/reconnects direct ownership and proves both retained success and ambiguous outcome avoid a second `sendRichMessage`. Sender-rights and live rollout evidence remain separate 0.45.0 work.
559
+ The channel-post journal records only this agent path's own publication intent. `prepare` is exact and idempotent; `beginPublication` durably changes one prepared operation to outcome-unknown before any future non-idempotent send; only `confirmPublished` records the returned numeric channel/message identity. A retry never regains issuance from outcome-unknown or published state. The store validates profile and token fingerprint, refuses malformed/duplicate/over-capacity state, and lists newest retained records without claiming Telegram history. The public-`@username` direct-leader sender now uses the tool-call ID to prepare and begin this journal transition before `sendRichMessage`, then records the returned numeric channel/message identity plus bounded username/title observed by `getChat`. Retained outcome-unknown state refuses automatic replay. Bounded agent-facing listing is available locally. Exact edit/delete journal transitions fence each tool-call mutation as outcome-unknown before direct-leader `editMessageText` or `deleteMessage`, and require the same mutation ID to confirm edited or deleted state. Numeric-channel publication requires explicit `channel: true`; direct-leader `getChat` must prove the exact negative ID and channel type before issuance, and the send response must preserve that identity. Cross-process regressions prove one winner for concurrent publication/edit/delete issuance and no restart regrant. Reads fail closed before parsing links, foreign/loose files, unsupported no-follow platforms, identity races, or oversized input. No legacy post schema is migrated: absent state starts empty and unknown versions/fields fail closed. The 0.44 downgrade checker leaves this inert journal untouched because old code cannot issue its effects. Only an explicit successful list returns retained authored Markdown. Channel publication/list/mutation failures and runtime events use fixed messages without retained content, token, path, or arbitrary transport detail. Production-helper regressions model a lost successful caller ACK and an ambiguous send response: the same operation returns retained success or refuses outcome-unknown without a second transport call. The retry uses a freshly opened replacement store, proving durable behavior across helper replacement/restart. A native entrypoint regression disconnects/reconnects direct ownership and proves both retained success and ambiguous outcome avoid a second `sendRichMessage`. Sender-rights and live rollout evidence remain separate 0.45.0 work. Channel media posts upload one local `.jpg`/`.jpeg`/`.png`/`.webp` photo or `.mp4` video through the multipart transport with `text` as the HTML caption; kind, byte size (photo ≤ 10 MiB, video ≤ 50 MiB), and 1024 visible caption characters are validated before issuance, and unsupported types or albums are rejected rather than downgraded to links. The journal binds the inspected kind/file name/byte size/SHA-256 media identity and caption, so duplicate requests and lost acknowledgements never re-upload. A media-post edit replaces the caption through `editMessageCaption` with the same Markdown-to-HTML rendering, where `||spoiler||` now renders as `<tg-spoiler>`.
558
560
 
559
561
  Assistant-authored final-message actions use hidden top-level comments, with an additional fenced wrapper for in-body buttons:
560
562
 
@@ -67,9 +67,9 @@ Every assistant-authored HTML comment is transport-private on Telegram: previews
67
67
 
68
68
  - `telegram_bind({ app, script, argument? } | { app, method, argument? })` installs and initializes one canonical managed Generative App module under `<agent-dir>/genapps/<app>/<app>.mjs`, or invokes one named method on an installed app. Installation rejects silent replacement and noncanonical/symlink sources. Methods receive immutable JSON state, one optional JSON argument, cancellation, revision, and a bounded non-shell process port; successful state changes commit to `state.json` plus `states.jsonl`, while output-only methods leave history unchanged. After one-shot `tgbtn` resolution, a complete `app::method` or `app::method(<strict JSON>)` prompt invokes the installed app before Pi queue admission and sends its planned Markdown/buttons directly; malformed or failed bound actions never fall back to a model prompt. Direct app-output buttons retain hidden source revisions and stale actions fail before method execution; sibling processes serialize transitions and recover dead lock owners. Bound actions send a fresh message by default and retain the clicked button's selected state on its prior surface. A result may opt into `viewMode: "edit"` to replace the callback message and keyboard in place, with one fresh-send fallback only for that explicit action. Agent-mediated initial-surface revisions, process-birth lock proof, automatic refresh, and voice output remain open.
69
69
  - `telegram_attach(paths, chat_id?, thread_id?, caption?)` is the stable artifact delivery tool for generated files. During Telegram turns it queues files before any separate final text; with `assistant.rendering: "rich"`, exactly one PNG/JPEG, MP4, or MP3 artifact plus non-empty final Markdown can become one reply-anchored Rich Message with media first. HTML mode, multiple/unsupported files, Guest Mode, and voice outputs retain their established paths. Outside Telegram turns the tool sends files directly to the paired/default chat, the registered follower's assigned thread, or an explicit `chat_id` plus optional `thread_id` when this Pi instance owns `/telegram-connect` or is registered with the multi-instance bus.
70
- - `telegram_channel_post(action, operation_id, markdown?)` edits or deletes one exact `published` record returned by `telegram_channel_posts`. Edit requires Markdown and delete forbids it. The direct leader fences the tool call as outcome-unknown before `editMessageText` or `deleteMessage`, so ambiguous failures are never replayed automatically.
71
- - `telegram_channel_posts(chat_id?, limit?)` lists newest bounded records from the active profile's agent-owned post journal. It returns publication, edit/delete outcome-unknown, confirmed, and deleted local records only; it never reads or claims completeness for Telegram channel history. This explicit successful listing is the only tool response that exposes retained authored Markdown; channel tool failures use fixed redacted messages.
72
- - `telegram_message(text, chat_id?, channel?, thread_id?, thread?)` sends a direct Telegram Markdown message when this Pi instance owns `/telegram-connect` or is registered with the multi-instance bus. A public `@username`, or an exact negative numeric channel ID with `channel: true`, is passed as `chat_id` without a local registry; channel delivery requires the direct leader, and Telegram enforces whether the bot has channel posting permission. `thread` accepts a live numeric Thread id or its current acknowledged display title; name matching is case-insensitive and fails closed when absent or ambiguous, while delivery captures the numeric target. During an active Telegram turn, omitted targeting and an explicit target equal to that turn are rejected so the ordinary final-reply path remains the sole current-target response; an explicit different chat/thread target remains allowed. Outside active turns, paired/default local/TUI delivery remains unchanged. Top-level `telegram_button` comments inside `text` are parsed with the same planner used for normal replies and attached to that message; buttons are never standalone Telegram messages.
70
+ - `telegram_channel_post(action, operation_id, markdown?)` edits or deletes one exact `published` record returned by `telegram_channel_posts`. Edit requires Markdown and delete forbids it. A media-post edit replaces the caption through `editMessageCaption`, while a text post uses `editMessageText`; both render Markdown formatting, including spoilers, as Telegram HTML. The direct leader fences the tool call as outcome-unknown before the mutation call, so ambiguous failures are never replayed automatically.
71
+ - `telegram_channel_posts(chat_id?, limit?)` lists newest bounded records from the active profile's agent-owned post journal. It returns publication, edit/delete outcome-unknown, confirmed, and deleted local records only, including retained media kind/file name/size/SHA-256 identity; it never reads or claims completeness for Telegram channel history. This explicit successful listing is the only tool response that exposes retained authored Markdown; channel tool failures use fixed redacted messages, while pre-issuance media/caption validation errors stay actionable.
72
+ - `telegram_message(text, chat_id?, media?, channel?, thread_id?, thread?)` sends a direct Telegram Markdown message when this Pi instance owns `/telegram-connect` or is registered with the multi-instance bus. A public `@username`, or an exact negative numeric channel ID with `channel: true`, is passed as `chat_id` without a local registry; channel delivery requires the direct leader, and Telegram enforces whether the bot has channel posting permission. Channel delivery accepts `media` as one local `.jpg`/`.jpeg`/`.png`/`.webp` photo or `.mp4` video (photo ≤ 10 MiB, video ≤ 50 MiB), uploaded through multipart `sendPhoto`/`sendVideo` with `text` as its HTML caption (≤ 1024 visible characters); unsupported media types and albums are rejected before issuance, and the durable channel-post journal binds media identity and caption so duplicate or lost-acknowledgement retries never re-upload. `thread` accepts a live numeric Thread id or its current acknowledged display title; name matching is case-insensitive and fails closed when absent or ambiguous, while delivery captures the numeric target. During an active Telegram turn, omitted targeting and an explicit target equal to that turn are rejected so the ordinary final-reply path remains the sole current-target response; an explicit different chat/thread target remains allowed. Outside active turns, paired/default local/TUI delivery remains unchanged. Top-level `telegram_button` comments inside `text` are parsed with the same planner used for normal replies and attached to that message; buttons are never standalone Telegram messages.
73
73
  - The bundled `telegram-bridge` Skill owns action syntax, target routing, Threaded Mode, formatting, Generative App operation, and profile-specific debugging guidance. The bundled `show-me` Skill owns portable evidence-honest explanations and adapts them to phone-width Markdown or self-contained HTML artifacts when Telegram is the active surface. The regular prompt routes applicable turns to these and the other bundled Skills. `telegram_attach`, `telegram_bind`, and `telegram_message` remain registered but are model-active only while this instance owns direct transport or holds a live follower registration; disconnect/loss suppresses their schemas and prompt metadata, and recovery restores only the operator's previously active pi-telegram subset.
74
74
  - `telegram_voice` hidden comments request Telegram-native voice delivery through `{text}`, `{text|lang}`, `{text|lang|rate}`, or a JSON object. JSON is the fallback for multiline content, named fields, or escaping; equivalent `text` or `value` supplies the spoken payload, with explicit `text` taking precedence.
75
75
  - `telegram_button` hidden comments create footer buttons; standalone column-zero triple-backtick `telegram_button` blocks create button rows between paragraphs in Native Rich Markdown. Both accept the same singleton or mixed JSON/CML matrix and share prompt/app routing. Native rows allow at most eight buttons and must fit one Rich Message chunk; invalid or incomplete blocks register nothing. Drafts hide action fences. HTML compatibility projects fenced controls into the footer. In-body clicks acknowledge without recoloring the Rich body; selected-style highlighting remains footer-only. One marker accepts a JSON object, adaptive JSON/CML matrix, or positional [Compact Matrix Literal](./compact-matrix-literal.md). Named JSON objects and positional cells may coexist in one matrix or row; separators are optional and one trailing comma is tolerated at matrix, row, and JSON-object boundaries. Top-level cells become full-width rows, while nested rows group one or more buttons horizontally without an artificial parser-width cap. CML uses `{value}`, `{label|prompt}`, prompt-only `{|prompt}`, or the corresponding three-atom form with `selected_style`; an omitted label uses the existing prompt-as-label fallback, and the optional third atom requires a non-empty prompt and accepts only `primary`, `success`, or `danger`. A fourth atom accepts `1` or `true` (disabled), and `0` or `false` (enabled), with exact lowercase spelling; an omitted fourth position stays enabled, and the third atom may be empty in this form (`{|Next||1}`). JSON uses boolean `disabled`. Disabled cells need no prompt or selected style: `{Next|||1}` is label-only and `{|||1}` is blank (JSON `{"label":"Next","disabled":true}` and `{"disabled":true}`). The Telegram renderer supplies a non-breaking space only when the label is empty. Disabled cells stay visible but carry `disabled: {}` instead of callback data and register no prompt or bound action; invalid disabled values reject the candidate matrix. It trims atom boundaries and supports only the minimal escapes `\|`, `\}`, and `\\`. Prefer one matrix comment for multiple buttons. Use JSON `label` plus `prompt`, or `value` when both strings are identical. Action markers are colon-free; colon-prefixed payloads are rejected. Use top-level column-zero action wrappers, outside quotes, lists, or enclosing code examples. Ordinary code fences and larger outer fences preserve literal examples; bare JSON/CML in prose never activates.
@@ -86,7 +86,7 @@ Stable config keys:
86
86
 
87
87
  ```ts
88
88
  interface TelegramBotProfile {
89
- botToken: string;
89
+ botToken: string; // literal token or exact $NAME / ${NAME} environment reference
90
90
  botUsername?: string; // runtime-managed
91
91
  botId?: number; // runtime-managed
92
92
  allowedUserId?: number;
@@ -520,6 +520,9 @@ interface TelegramCommandsAndToolsBindingDeps {
520
520
  sendChannelMarkdownMessage: NonNullable<
521
521
  OutboundAttachments.TelegramOutboundMessageToolRegistrationDeps["sendChannelMarkdownMessage"]
522
522
  >;
523
+ sendChannelMediaMessage: NonNullable<
524
+ OutboundAttachments.TelegramOutboundMessageToolRegistrationDeps["sendChannelMediaMessage"]
525
+ >;
523
526
  listChannelPosts: ChannelPosts.TelegramChannelPostJournalStore["list"];
524
527
  mutateChannelPost(input: { action: "edit" | "delete"; operationId: string;
525
528
  mutationId: string; markdown?: string }): Promise<ChannelPosts.TelegramChannelPostRecord>;
@@ -551,6 +554,7 @@ export function registerTelegramCommandsAndTools({
551
554
  buttonActionStore,
552
555
  sendMarkdownReply,
553
556
  sendChannelMarkdownMessage,
557
+ sendChannelMediaMessage,
554
558
  listChannelPosts,
555
559
  mutateChannelPost,
556
560
  callMultipart,
@@ -597,6 +601,7 @@ export function registerTelegramCommandsAndTools({
597
601
  sendMarkdownMessage: (chatId, markdown, options) =>
598
602
  sendMarkdownReply(chatId, undefined, markdown, options),
599
603
  sendChannelMarkdownMessage,
604
+ sendChannelMediaMessage,
600
605
  recordRuntimeEvent,
601
606
  });
602
607
  const queueAgentConnectionContext = (connected: boolean): void => {
@@ -676,6 +681,10 @@ export function registerTelegramCommandsAndTools({
676
681
  setConfig: setupConfigStore.set,
677
682
  setupGuard: setup,
678
683
  getMe: TelegramApi.fetchTelegramBotIdentity,
684
+ resolveBotToken: (value) =>
685
+ Config.resolveTelegramBotToken(value, process.env),
686
+ describeBotToken: (value) =>
687
+ Config.getTelegramBotTokenDiagnostic(value, process.env),
679
688
  persistConfig: persistSetupConfig,
680
689
  startPolling: lockedPollingRuntime.start,
681
690
  updateStatus,
@@ -692,6 +701,7 @@ export function registerTelegramCommandsAndTools({
692
701
  getStatusLines,
693
702
  reloadConfig: configStore.load,
694
703
  hasBotToken: configStore.hasBotToken,
704
+ getBotTokenDiagnostic: configStore.getBotTokenDiagnostic,
695
705
  startPolling: async (ctx, options) => {
696
706
  setRequestedThreadNameForPollingStart?.(options?.requestedThreadName);
697
707
  try {
@@ -807,6 +817,12 @@ interface TelegramLifecycleBindingDeps {
807
817
  Keyboard.TelegramInlineKeyboardMarkup
808
818
  >["sendGuestReply"]
809
819
  >;
820
+ editGuestReply?: Queue.TelegramAgentEndHookRuntimeDeps<
821
+ Queue.PendingTelegramTurn,
822
+ Pi.ExtensionContext,
823
+ Pi.AgentEndEvent["messages"][number],
824
+ Keyboard.TelegramInlineKeyboardMarkup
825
+ >["editGuestReply"];
810
826
  preparePreviewDelivery?: Queue.TelegramAgentEndRuntimeDeps<Queue.PendingTelegramTurn>["preparePreviewDelivery"];
811
827
  finalizeMarkdownPreview: Queue.TelegramAgentEndHookRuntimeDeps<
812
828
  Queue.PendingTelegramTurn,
@@ -859,6 +875,7 @@ export function registerTelegramLifecycleRuntimeHooks({
859
875
  answerGuestQuery,
860
876
  deleteMessage,
861
877
  sendGuestReply,
878
+ editGuestReply,
862
879
  preparePreviewDelivery,
863
880
  finalizeMarkdownPreview,
864
881
  proactivePushTargetGetter,
@@ -1072,6 +1089,7 @@ export function registerTelegramLifecycleRuntimeHooks({
1072
1089
  sendRichAttachmentReply: richAttachmentSender,
1073
1090
  answerGuestQuery,
1074
1091
  sendGuestReply,
1092
+ editGuestReply,
1075
1093
  sendGuestAttachment,
1076
1094
  sendGuestVoiceReply,
1077
1095
  planOutboundReply: outboundReplyPlanner,
@@ -8,11 +8,15 @@ import {
8
8
  markTelegramBusCrossTargetDelivery,
9
9
  stripTelegramBusApiMetadata,
10
10
  } from "./bus.ts";
11
- import { isTelegramMessageNotModifiedError } from "./telegram-api.ts";
11
+ import {
12
+ buildTelegramAnswerGuestQueryBody,
13
+ isTelegramMessageNotModifiedError,
14
+ } from "./telegram-api.ts";
12
15
  import type {
13
16
  TelegramAnswerGuestQueryOptions,
14
17
  TelegramApiCallOptions,
15
18
  TelegramBridgeApiRuntime,
19
+ TelegramEditGuestInlineMessageContent,
16
20
  TelegramEditMessageTextBody,
17
21
  TelegramSendMessageBody,
18
22
  TelegramSendMessageDraftBody,
@@ -342,24 +346,33 @@ export function createTelegramBusAwareApiRuntime(
342
346
  await deps.directRuntime.answerGuestQuery(guestQueryId, text, options);
343
347
  return;
344
348
  }
345
- const body: Record<string, unknown> = { guest_query_id: guestQueryId };
346
- if (options?.result) {
347
- body.result = options.result;
348
- } else if (text !== undefined || options?.richMessage) {
349
- const inputContent: Record<string, unknown> = options?.richMessage
350
- ? { rich_message: options.richMessage }
351
- : { message_text: text };
352
- if (!options?.richMessage && options?.parseMode) {
353
- inputContent.parse_mode = options.parseMode;
354
- }
355
- body.result = {
356
- type: "article",
357
- id: "1",
358
- title: "Response",
359
- input_message_content: inputContent,
360
- };
361
- }
362
- await deps.callFollowerApi("call", ["answerGuestQuery", body]);
349
+ await deps.callFollowerApi("call", [
350
+ "answerGuestQuery",
351
+ buildTelegramAnswerGuestQueryBody(guestQueryId, text, options),
352
+ ]);
353
+ },
354
+ answerGuestQueryForInlineMessage(
355
+ guestQueryId: string,
356
+ text?: string,
357
+ options?: TelegramAnswerGuestQueryOptions,
358
+ ): Promise<string | undefined> {
359
+ // Guest answers can only be edited while this instance owns direct
360
+ // transport; follower forwarding cannot preserve the inline message id.
361
+ return deps.ownsDirect()
362
+ ? deps.directRuntime.answerGuestQueryForInlineMessage(
363
+ guestQueryId,
364
+ text,
365
+ options,
366
+ )
367
+ : rejectTelegramDirectOwnership("answerGuestQueryForInlineMessage");
368
+ },
369
+ editGuestInlineMessage(
370
+ inlineMessageId: string,
371
+ content: TelegramEditGuestInlineMessageContent,
372
+ ): Promise<void> {
373
+ return deps.ownsDirect()
374
+ ? deps.directRuntime.editGuestInlineMessage(inlineMessageId, content)
375
+ : rejectTelegramDirectOwnership("editGuestInlineMessage");
363
376
  },
364
377
  async deleteMessage(chatId: number, messageId: number): Promise<void> {
365
378
  if (deps.ownsDirect())
@@ -2603,6 +2603,8 @@ function parseQueueHandoffPayload(
2603
2603
  !Number.isSafeInteger(value.replyToMessageId) ||
2604
2604
  (value.guestQueryId !== undefined &&
2605
2605
  typeof value.guestQueryId !== "string") ||
2606
+ (value.guestInlineMessageId !== undefined &&
2607
+ typeof value.guestInlineMessageId !== "string") ||
2606
2608
  !Number.isSafeInteger(value.queueOrder) ||
2607
2609
  (value.queueLane !== "control" &&
2608
2610
  value.queueLane !== "priority" &&
@@ -2632,6 +2634,9 @@ function parseQueueHandoffPayload(
2632
2634
  ...(typeof value.guestQueryId === "string"
2633
2635
  ? { guestQueryId: value.guestQueryId }
2634
2636
  : {}),
2637
+ ...(typeof value.guestInlineMessageId === "string"
2638
+ ? { guestInlineMessageId: value.guestInlineMessageId }
2639
+ : {}),
2635
2640
  queueOrder: value.queueOrder as number,
2636
2641
  queueLane,
2637
2642
  laneOrder: value.laneOrder as number,