@llblab/pi-kit 0.1.0 → 0.1.3

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 (32) hide show
  1. package/BACKLOG.md +1 -1
  2. package/CHANGELOG.md +12 -0
  3. package/README.md +4 -4
  4. package/node_modules/@llblab/pi-codex-usage/CHANGELOG.md +4 -1
  5. package/node_modules/@llblab/pi-codex-usage/index.ts +4 -1
  6. package/node_modules/@llblab/pi-codex-usage/package.json +3 -2
  7. package/node_modules/@llblab/pi-grow-loop/CHANGELOG.md +4 -0
  8. package/node_modules/@llblab/pi-grow-loop/package.json +3 -2
  9. package/node_modules/@llblab/pi-telegram/BACKLOG.md +0 -1
  10. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +28 -2
  11. package/node_modules/@llblab/pi-telegram/README.md +5 -3
  12. package/node_modules/@llblab/pi-telegram/docs/architecture.md +10 -0
  13. package/node_modules/@llblab/pi-telegram/docs/outbound.md +1 -1
  14. package/node_modules/@llblab/pi-telegram/docs/public-api.md +2 -2
  15. package/node_modules/@llblab/pi-telegram/docs/ui-style.md +7 -6
  16. package/node_modules/@llblab/pi-telegram/docs/voice.md +11 -1
  17. package/node_modules/@llblab/pi-telegram/index.ts +2 -0
  18. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +18 -2
  19. package/node_modules/@llblab/pi-telegram/lib/commands.ts +5 -0
  20. package/node_modules/@llblab/pi-telegram/lib/generative-apps.ts +8 -15
  21. package/node_modules/@llblab/pi-telegram/lib/journal.ts +24 -0
  22. package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +19 -3
  23. package/node_modules/@llblab/pi-telegram/lib/menu-model.ts +4 -2
  24. package/node_modules/@llblab/pi-telegram/lib/menu-thinking.ts +7 -6
  25. package/node_modules/@llblab/pi-telegram/lib/outbound-attachments.ts +7 -2
  26. package/node_modules/@llblab/pi-telegram/lib/outbound.ts +14 -2
  27. package/node_modules/@llblab/pi-telegram/lib/pi.ts +70 -4
  28. package/node_modules/@llblab/pi-telegram/lib/prompts.ts +56 -19
  29. package/node_modules/@llblab/pi-telegram/package.json +1 -1
  30. package/node_modules/@llblab/pi-telegram/skills/generated-control-surface/SKILL.md +2 -2
  31. package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/SKILL.md +10 -2
  32. package/package.json +11 -5
package/BACKLOG.md CHANGED
@@ -1,3 +1,3 @@
1
1
  # Backlog
2
2
 
3
- - [ ] `Release`: Configure npm Trusted Publishing and publish the first validated package release after explicit authorization.
3
+ No open work.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  All notable changes to `@llblab/pi-kit` are documented here.
4
4
 
5
+ ## 0.1.3 - 2026-08-20
6
+
7
+ - `Extension Releases`: Advances `@llblab/pi-codex-usage` to `0.9.3` for current Pi header-type compatibility and Trusted Publisher releases, and `@llblab/pi-grow-loop` to `0.7.2` for audited Trusted Publisher releases.
8
+
9
+ ## 0.1.2 - 2026-08-20
10
+
11
+ - `Trusted Release Automation`: Adds tag-gated validation, npm Trusted Publisher provenance, immutable package/tag identity checks, public package verification, and workflow-owned GitHub Release creation through `.github/workflows/release.yml`.
12
+
13
+ ## 0.1.1 - 2026-08-20
14
+
15
+ - `Telegram Release`: Advances `@llblab/pi-telegram` to `0.36.7`, bringing proactive prompt-button delivery, explicit connection-state context, scoped Telegram control activation, provider-neutral voice configuration guidance, separated tool errors, host compatibility, prompt-block compatibility, and orphan-journal protection into the bundled kit.
16
+
5
17
  ## 0.1.0 - 2026-08-18
6
18
 
7
19
  - `Package`: Established a version-pinned Pi distribution bundling Actors, Codex usage, Grow Loop, and Telegram extensions with their declared Skills behind one installable package.
package/README.md CHANGED
@@ -7,9 +7,9 @@
7
7
  | Package | Version | Resources |
8
8
  | --- | ---: | --- |
9
9
  | `@llblab/pi-actors` | `0.49.1` | Extension and Skills |
10
- | `@llblab/pi-codex-usage` | `0.9.2` | Extension |
11
- | `@llblab/pi-grow-loop` | `0.7.1` | Extension and Skills |
12
- | `@llblab/pi-telegram` | `0.36.1` | Extension and Skills |
10
+ | `@llblab/pi-codex-usage` | `0.9.3` | Extension |
11
+ | `@llblab/pi-grow-loop` | `0.7.2` | Extension and Skills |
12
+ | `@llblab/pi-telegram` | `0.36.7` | Extension and Skills |
13
13
 
14
14
  Versions are exact by design. Updating an extension does not change an installed kit until this repository explicitly advances that dependency and publishes a new kit version.
15
15
 
@@ -18,7 +18,7 @@ Versions are exact by design. Updating an extension does not change an installed
18
18
  From npm after publication:
19
19
 
20
20
  ```bash
21
- pi install npm:@llblab/pi-kit@0.1.0
21
+ pi install npm:@llblab/pi-kit@0.1.1
22
22
  ```
23
23
 
24
24
  For local development:
@@ -1,6 +1,9 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## 0.9.3: Trusted Release Automation
4
+
5
+ - Added tag-gated release automation with exact package/lock/tag identity checks, full package validation, npm Trusted Publisher provenance, public registry verification, and workflow-owned GitHub Release creation through `.github/workflows/release.yml`.
6
+ - Normalized nullable provider headers into the string-only request header contract, preserving compatibility with current Pi model-registry types while omitting intentionally removed headers.
4
7
 
5
8
  ## 0.9.1: Clear Weekly Quota Status
6
9
 
@@ -652,7 +652,10 @@ async function resolvePiCodexAuth(
652
652
  continue;
653
653
  }
654
654
 
655
- const headers = { ...(auth.headers ?? {}) };
655
+ const headers: Record<string, string> = {};
656
+ for (const [name, value] of Object.entries(auth.headers ?? {})) {
657
+ if (value !== null) headers[name] = value;
658
+ }
656
659
  if (!hasHeader(headers, "Authorization") && auth.apiKey) {
657
660
  headers.Authorization = `Bearer ${auth.apiKey}`;
658
661
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-codex-usage",
3
- "version": "0.9.2",
3
+ "version": "0.9.3",
4
4
  "private": false,
5
5
  "description": "Minimal Pi extension that shows primary Codex ChatGPT subscription usage limits",
6
6
  "keywords": [
@@ -28,8 +28,9 @@
28
28
  "check": "node --experimental-strip-types -e \"await import('./index.ts'); console.log('pi-codex-usage: extension import ok')\"",
29
29
  "test": "node --experimental-strip-types --test test/*.test.ts",
30
30
  "typecheck": "tsc --noEmit",
31
+ "audit": "npm audit --omit=peer",
31
32
  "pack:dry": "npm pack --dry-run",
32
- "validate": "npm run typecheck && npm run test && npm run check && npm run pack:dry"
33
+ "validate": "npm run typecheck && npm run test && npm run check && npm run audit && npm run pack:dry"
33
34
  },
34
35
  "files": [
35
36
  "index.ts",
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.2: Trusted Release Automation
4
+
5
+ - [Release] Add tag-gated package validation, npm Trusted Publisher provenance, immutable package/tag identity checks, public registry verification, and workflow-owned GitHub Release creation through `.github/workflows/release.yml`.
6
+
3
7
  ## 0.7.1: Banner Presentation Hotfix
4
8
 
5
9
  - [Docs] Display the packaged project banner at the top of the README through its stable repository URL so GitHub and npm readers see the release identity immediately.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-grow-loop",
3
- "version": "0.7.1",
3
+ "version": "0.7.2",
4
4
  "private": false,
5
5
  "description": "Semantic loop-engineering for agent-owned, visible, interruptible continuation in Pi",
6
6
  "keywords": [
@@ -29,8 +29,9 @@
29
29
  "check": "node --experimental-strip-types -e \"await import('./index.ts'); console.log('pi-grow-loop: extension import ok')\"",
30
30
  "typecheck": "tsc --noEmit",
31
31
  "test": "node --experimental-strip-types --test test/*.test.ts",
32
+ "audit": "npm audit --omit=peer",
32
33
  "pack:dry": "npm pack --dry-run",
33
- "validate": "npm run test && npm run typecheck && npm run check && npm run pack:dry"
34
+ "validate": "npm run test && npm run typecheck && npm run check && npm run audit && npm run pack:dry"
34
35
  },
35
36
  "files": [
36
37
  "banner.jpg",
@@ -2,7 +2,6 @@
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
- - [ ] `Automated release publication live proof`: Exercise the implemented tag-triggered release path on `v0.36.1` and verify reusable full validation, exact tag/version/lock/changelog preflight, npm Trusted Publisher provenance, idempotent package identity, public tarball contents, and one workflow-owned GitHub Release before declaring manual npm publication retired.
6
5
  - [ ] `Operator-confirmed pairing`: Replace first-contact automatic ownership with an explicit pending request that the trusted Pi interface can allow or reject before `allowedUserId` is persisted or any Telegram prompt executes; retain manual preconfiguration, bound pending-request lifetime and multiplicity, fail closed across session/transport replacement, and cover unauthorized-first-contact races.
7
6
  - [ ] `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.
8
7
  - [ ] [`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.
@@ -2,7 +2,33 @@
2
2
 
3
3
  > Each release keeps at most 8 outcome records of at most 512 characters.
4
4
 
5
- ## Unreleased
5
+ ## 0.36.7: Context-Aware Proactive Controls
6
+
7
+ - `Proactive Prompt Buttons`: Plans valid top-level `telegram_button` comments before local/autonomous assistant segments enter Rich or HTML proactive delivery, preserving the ordinary callback store and renderer path instead of silently hiding correct controls.
8
+ - `Connection-Aware Agent Context`: Successful setup/connect and disconnect commands queue a hidden connection-state note for delivery with the next agent turn without triggering one. Connected system guidance and Telegram Skills now distinguish transport capability from user intent, reserving proactive Telegram controls for Telegram-originated turns or explicit requests.
9
+
10
+ ## 0.36.6: Orphan Journal Guard
11
+
12
+ - `Orphan Journal Guard`: Rejects a missing journal snapshot when immutable revision segments still exist, preventing later admission from replacing uncertain durable authority with a new revisionless snapshot while preserving snapshot-first compaction and fail-closed recovery.
13
+ - `Voice Configuration Guidance`: Guides agents from voice-configuration requests to provider-neutral `telegram.json` command templates, ordered STT fallbacks, OGG/Opus validation, and companion-provider alternatives while preserving `hidden` as the useful default for explicit `telegram_voice` delivery.
14
+
15
+ ## 0.36.5: Separated Tool Errors
16
+
17
+ - `Separated Tool Errors`: Prefixes Telegram attachment and direct-message Tool failures with one normalized leading newline so Pi renders error evidence on a visually separated line beneath the Tool name without changing the underlying diagnostic text.
18
+
19
+ ## 0.36.4: Async Host Settings Compatibility
20
+
21
+ - `Async Host Settings`: Normalizes synchronous or asynchronous settings construction plus legacy enabled-model methods or generic `get` / `set` services at the Pi adapter boundary before model-menu reads and scoped-model persistence, improving compatible-host interoperability without changing native Pi behavior or weakening required `agent_settled` semantics.
22
+
23
+ ## 0.36.3: Ordered Prompt Compatibility
24
+
25
+ - `Prompt Compatibility`: Preserves plain and ordered-block system prompts across Pi-compatible runtimes; Telegram guidance remains a distinct block when the host supplies blocks, while unavailable transport strips only Telegram tool metadata without collapsing unrelated prompt context.
26
+
27
+ ## 0.36.2: Provider-Compatible Controls
28
+
29
+ - `Provider-Compatible Bind Schema`: Exposes `telegram_bind` through one top-level JSON object schema while retaining runtime enforcement of mutually exclusive install and invocation forms, avoiding providers that reject top-level union schemas before any tool call can run.
30
+ - `Reaction Shortcut Naming`: Removes obsolete reaction-action labels from setup guidance now that queue reaction semantics use their current naming.
31
+ - `Compact Thinking Picker`: Keeps `off` on a distinct full-width row and groups the six ordered reasoning intensities into two three-button rows, reducing menu height without changing thinking-level values or callbacks.
6
32
 
7
33
  ## 0.36.1: Adaptive Control Surfaces
8
34
 
@@ -12,7 +38,7 @@
12
38
  - `Destructive Skip State`: Changes the active queue Skip selector from yellow to red and records the indicator in the UI registry, making deferred prompt removal visually distinct from Priority, Scoped, and other elevated but non-destructive states.
13
39
  - `Worker Cancellation Fence`: Makes Generative App workers acknowledge child-process abort handling before parent termination, with a bounded forced fallback for synchronously blocked methods, preventing Linux CI load from orphaning a cancelled process long enough to commit its delayed side effect.
14
40
  - `Capability App Routing`: Telegram interaction now prefers an already-advertised capability-owned Generative App for repeated controls before synthesizing one-shot prompt buttons, while the bridge remains generic and neither discovers capability-specific apps nor owns their state or identities.
15
- - `Hybrid Surface Breadcrumbs`: Generated Control Surface and Generative Apps now cross-route over one logical button matrix and shared JSON/CML runtime notation: ephemeral agent composition stays separate from reusable scripts, while one app view may combine deterministic bound methods with ordinary model-mediated prompts and no third button Skill is introduced.
41
+ - `Hybrid Surface Routing`: Generated Control Surface and Generative Apps now cross-route over one logical button matrix and shared JSON/CML runtime notation: ephemeral agent composition stays separate from reusable scripts, while one app view may combine deterministic bound methods with ordinary model-mediated prompts and no third button Skill is introduced.
16
42
 
17
43
  ## 0.36.0: Generative Apps
18
44
 
@@ -8,7 +8,7 @@
8
8
 
9
9
  It is a **runtime adapter**, not a remote terminal. Start or supervise work in the Pi TUI, then continue from Telegram while away from the keyboard. Each Telegram destination follows a running Pi instance and sends prompts into that instance's currently active session; it is not permanently bound to one session file or session identity. The bridge preserves Pi session semantics instead of pretending Telegram is a PTY, shell, process launcher, or session browser. That boundary is the product: Telegram gets safe runtime handles, not raw terminal power.
10
10
 
11
- Every completed intermediate commentary block from a Telegram-originated turn is delivered once as its own message before the existing final reply. Proactive push is enabled by default for local, autonomous, and unclassified extension follow-up work: `assistant.proactivePush` projects visible checkpoints and the final answer to the authorized Telegram target once and in order; set it explicitly to `false` to disable only that local/autonomous projection. Neither path mirrors local prompts, thinking, tool traffic, token deltas, or stale-generation work. The separate `Activity` setting defaults to `verbose` so new installations discover collapsed provider-exposed thinking and tool evidence immediately; operators can narrow it to one class or choose `quiet`. See [Outbound](docs/outbound.md#public-assistant-output) and the [configuration reference](docs/public-api.md#configuration-api).
11
+ Every completed intermediate commentary block from a Telegram-originated turn is delivered once as its own message before the existing final reply. Proactive push is enabled by default for local, autonomous, and unclassified extension follow-up work: `assistant.proactivePush` projects visible checkpoints and the final answer to the authorized Telegram target once and in order, preserving assistant-authored `telegram_button` comments as interactive prompt buttons; set it explicitly to `false` to disable only that local/autonomous projection. Neither path mirrors local prompts, thinking, tool traffic, token deltas, or stale-generation work. The separate `Activity` setting defaults to `verbose` so new installations discover collapsed provider-exposed thinking and tool evidence immediately; operators can narrow it to one class or choose `quiet`. See [Outbound](docs/outbound.md#public-assistant-output) and the [configuration reference](docs/public-api.md#configuration-api).
12
12
 
13
13
  This repository is an actively maintained fork of [`badlogic/pi-telegram`](https://github.com/badlogic/pi-telegram). It started from upstream commit [`cb34008`](https://github.com/badlogic/pi-telegram/commit/cb34008460b6c1ca036d92322f69d87f626be0fc) and has since diverged substantially.
14
14
 
@@ -28,6 +28,8 @@ pi install git:github.com/llblab/pi-telegram
28
28
 
29
29
  The 0.21 extension platform requires Pi `0.80.6` or newer. Its Activity API uses the public `agent_settled` lifecycle event to keep retries/continuations under one activity identity and release that identity only after the run fully settles.
30
30
 
31
+ Pi is the primary and only officially supported host. Narrow host-neutral adapters preserve ordered prompt blocks and normalize synchronous or asynchronous legacy/generic settings services for Pi-compatible hosts, but this is best-effort compatibility rather than an OMP support guarantee. Alternate-host shims must still reproduce required Pi lifecycle semantics—especially `agent_settled`—and their maintainers own ongoing validation.
32
+
31
33
  ## Quick Start
32
34
 
33
35
  ### 1. Create a Telegram bot
@@ -86,7 +88,7 @@ Enable the optional capabilities the bridge needs in the [@BotFather](https://t.
86
88
 
87
89
  1. Enable guest mode so the bot can answer mentions and replies in chats where it is not a member.
88
90
  2. Enable private-chat Threaded Mode; when it is available, one live instance becomes the profile's leader and later visible Pi instances register as followers. Without it, the bridge stays in classic single-owner DM mode.
89
- 3. Make the bot an administrator in any chat where the queue reaction shortcuts (👍 prioritize, 👎 suppress) should work. Reaction updates require admin rights, so the shortcuts silently do nothing in non-admin chats; private chats deliver reactions without admin rights.
91
+ 3. Make the bot an administrator in any chat where the queue reaction shortcuts should work. Reaction updates require admin rights, so the shortcuts silently do nothing in non-admin chats; private chats deliver reactions without admin rights.
90
92
 
91
93
  ## What It Feels Like
92
94
 
@@ -216,7 +218,7 @@ Inbound files land under `<agent-dir>/tmp/telegram` and default to a 50 MiB limi
216
218
 
217
219
  ### Voice And Media
218
220
 
219
- Voice notes, audio, images, PDFs, and other media can pass through configured inbound handlers, programmatic handlers, or registered STT providers. Outbound voice can use configured `outboundHandlers` or registered TTS providers; `pi-telegram` owns reply policy and Telegram transport, while providers own synthesis. Explicit `telegram_voice` actions accept either a JSON object or compact double-quoted attributes, with equivalent `text` and `value` payload keys and one colon-free action marker.
221
+ Voice notes, audio, images, PDFs, and other media can pass through configured inbound handlers, programmatic handlers, or registered STT providers. Outbound voice can use configured `outboundHandlers` or registered TTS providers; `pi-telegram` owns reply policy and Telegram transport, while providers own synthesis. Configure provider-neutral local/API pipelines and ordered fallbacks through [`telegram.json` command templates](./docs/voice.md#choose-an-integration-path). The default `hidden` reply mode still supports intentional voice delivery through explicit `telegram_voice` actions; `mirror` and `always` add automatic voice policy. Explicit actions accept either a JSON object or compact double-quoted attributes, with equivalent `text` and `value` payload keys and one colon-free action marker.
220
222
 
221
223
  ### Buttons And Callbacks
222
224
 
@@ -88,6 +88,16 @@ The repository uses a **Flat Domain DAG**:
88
88
  - `bindings` / `lifecycle` / `prompts` / `prompt-templates` / `pi`: Pi-facing command/tool/hook registration and cohesive cross-domain binding assembly, including queue mutation/dispatch/watchdog composition over admission and transport ports; session-generation fencing and start/shutdown sequencing across Queue, grouped input, Delivery, polling, capability monitor, follower refresh, and assistant-output projection; Telegram prompt guidance; prompt-template discovery/expansion; and centralized direct Pi SDK imports.
89
89
  - `command-templates`: shell-free command-template helpers, composition expansion, placeholder substitution, executable resolution, warnings, and retry/timeout semantics.
90
90
 
91
+ ### Host Compatibility Boundary
92
+
93
+ Pi is the primary and only officially supported host. `pi-telegram` may still accept narrow, host-neutral representation differences at its existing Pi-facing boundary when they preserve native Pi behavior and do not create a second runtime policy layer:
94
+
95
+ - `prompts` preserves either Pi's plain system-prompt string or an ordered block array supplied by a compatible host, appending Telegram guidance without collapsing host-owned blocks.
96
+ - `pi` normalizes settings-manager construction that is either synchronous or asynchronous, then adapts either Pi's legacy enabled-model methods or a generic `get` / `set` settings service before model-menu reads and scoped-model persistence use it. Hosts without an explicit reload method rely on fresh asynchronous construction; durable writes still require `flush`.
97
+ - `lifecycle` continues to require Pi's semantic `agent_settled` boundary. It does not infer terminal settlement from host-specific `agent_end`, retry, or stop events; a compatibility shim must reproduce that contract before it can safely support activity identity and unrecovered-error finalization.
98
+
99
+ This boundary uses no host-name detection, host package dependency, prototype patching, hidden agent process, PTY, or terminal forwarding. Representation adapters are best-effort compatibility rather than an OMP support guarantee. Alternate hosts and community contributors own validation of their compatibility shims and must supply every lifecycle semantic that the bridge requires.
100
+
91
101
  ### Guarded Invariants
92
102
 
93
103
  Architecture invariant tests protect:
@@ -12,7 +12,7 @@ Every completed `assistant-segment` with `placement: "intermediate"` from a Tele
12
12
 
13
13
  Proactive projection defaults on for local, autonomous, and unclassified extension follow-up Pi work. With `assistant.proactivePush` omitted or set to `true`, every completed public block—including visible commentary/checkpoints and the final answer—is projected to the instance's authorized target in source order; set it explicitly to `false` to opt out of only this local/autonomous projection. Both paths consume normalized complete Activity segments rather than raw token deltas, reasoning, or tool traffic.
14
14
 
15
- Projected blocks use `assistant.rendering` independently of voice policy. Rich mode sends native Rich Markdown and HTML mode keeps the established HTML renderer; projection does not synthesize voice or attach queued files merely because Rich rendering is active. Ordered admission revalidates the exact target, profile/token transport generation, leader epoch or follower registration generation, and session generation before each send. Active-turn final delivery waits for admitted commentary inside its existing background delivery task, preserving commentary-before-final order without blocking Pi lifecycle completion. A `commit-unknown` outcome never permits replay.
15
+ Projected blocks use `assistant.rendering` independently of voice policy. Rich mode sends native Rich Markdown and HTML mode keeps the established HTML renderer; assistant-authored `telegram_button` comments are planned into prompt buttons before either renderer runs, while projection does not synthesize voice or attach queued files merely because Rich rendering is active. Ordered admission revalidates the exact target, profile/token transport generation, leader epoch or follower registration generation, and session generation before each send. Active-turn final delivery waits for admitted commentary inside its existing background delivery task, preserving commentary-before-final order without blocking Pi lifecycle completion. A `commit-unknown` outcome never permits replay.
16
16
 
17
17
  ## Technical Activity
18
18
 
@@ -40,8 +40,8 @@ import {
40
40
  Stable commands inside Pi:
41
41
 
42
42
  - `/telegram-setup` — configure/update the bot token.
43
- - `/telegram-connect` — start polling here and acquire external Telegram control ownership. Accepted queue/reply state stays local if ownership later moves elsewhere.
44
- - `/telegram-disconnect` — after destructive confirmation, stop polling and release ownership without deleting or silencing accepted local queue state. In Threaded Mode it deletes this instance's current Telegram thread; a follower waits for its active leader to confirm generation-fenced cleanup before stopping. Graceful Pi `quit` performs the same teardown without prompting, while `reload`, `new`, `resume`, and `fork` preserve same-process handoff.
43
+ - `/telegram-connect` — start polling here and acquire external Telegram control ownership. Accepted queue/reply state stays local if ownership later moves elsewhere. A successful command queues a hidden connection-state note for delivery with the agent's next turn without triggering one; it says Telegram is connected and that connectivity alone is not user intent.
44
+ - `/telegram-disconnect` — after destructive confirmation, stop polling and release ownership without deleting or silencing accepted local queue state. A successful command queues the corresponding hidden, non-triggering disconnected context note; cancelled or failed disconnects do not publish a false state transition. In Threaded Mode it deletes this instance's current Telegram thread; a follower waits for its active leader to confirm generation-fenced cleanup before stopping. Graceful Pi `quit` performs the same teardown without prompting, while `reload`, `new`, `resume`, and `fork` preserve same-process handoff.
45
45
  - `/telegram-status` — show connection, polling, execution, queue, and recent event diagnostics; debug output separates poller and worker progress, durable automatic-retry state, exact foreign queued-owner identity, and negotiated protocol/build/capabilities.
46
46
 
47
47
  ### Telegram commands
@@ -168,22 +168,23 @@ Examples:
168
168
  - `⚫️ Priority` / `🟣 Normal`
169
169
  - `1` / `🟣 2` / `3`
170
170
 
171
- ## Vertical Option Lists
171
+ ## Option Lists
172
172
 
173
- Vertical option lists choose one value from a potentially longer list, for example model selection, thinking level, voice reply mode, or time injection mode.
173
+ Option lists choose one value from a fixed set, for example model selection, thinking level, voice reply mode, or time injection mode.
174
174
 
175
175
  Rules:
176
176
 
177
- - Put each option on its own row.
177
+ - Put each option on its own row when labels are long, the set may grow, or scanning benefits from full width.
178
+ - A fixed set of short, ordered peer values may use compact rows of up to three buttons.
179
+ - Keep a semantically distinct value such as thinking `off` on its own full-width row before grouped intensity values.
178
180
  - Mark only the current value with `🟢`.
179
181
  - Leave non-current values without emoji.
180
182
  - Use lowercase labels when the option is a value.
181
183
 
182
184
  Examples:
183
185
 
184
- - `hidden`
185
- - `🟢 mirror`
186
- - `always`
186
+ - Vertical: `hidden`, `🟢 mirror`, `always`.
187
+ - Thinking: full-width `off`, then `minimal` / `low` / `🟢 medium`, then `high` / `xhigh` / `max`.
187
188
 
188
189
  ## Generated Prompt Buttons
189
190
 
@@ -6,10 +6,20 @@ Voice messages flow through an **inbound transcription → outbound voice reply*
6
6
 
7
7
  1. **Inbound:** A voice message arrives via Telegram. Inbound handlers transcribe it to text.
8
8
  2. **Processing:** The transcription becomes the agent prompt. The bridge tags the turn if it originated from voice.
9
- 3. **Outbound:** If voice replies are enabled, the agent's text response is converted to voice and sent back. No text draft appears in Telegram during generation.
9
+ 3. **Outbound:** When automatic voice delivery is selected or the agent emits an explicit `telegram_voice` action, the agent's text is converted to voice and sent back.
10
10
 
11
11
  The bridge owns Telegram transport, queue integration, reply-mode policy, preview suppression, fallback text delivery, and Settings UI. Provider extensions own STT/TTS calls, speech rewriting, provider-specific menus, transcript preference, and OGG/Opus conversion.
12
12
 
13
+ ### Choose an integration path
14
+
15
+ Use the smallest path that fits the operator's available capabilities:
16
+
17
+ 1. **`telegram.json` command templates:** Compose trusted local executables through `inboundHandlers` and `outboundHandlers`. This works equally well for local models and API-backed scripts, supports ordered STT fallbacks, and requires no companion extension.
18
+ 2. **Companion extension:** Register programmatic STT/TTS providers when installation, provider-owned settings, lifecycle integration, or zero-config reuse justifies code.
19
+ 3. **Hybrid:** Keep explicit operator command templates first and let installed providers supply progressive fallbacks.
20
+
21
+ pi-telegram does not catalog speech providers. Configuration agents should discover applicable Skills or trusted local executables, verify required environment variables by presence without exposing values, preserve unrelated `telegram.json` state, ensure TTS ends as OGG/Opus, and test each stage before the live Telegram path. `hidden` remains the safe and useful default: it disables only automatic voice replies, while explicit `telegram_voice` actions continue to use the configured synthesis pipeline.
22
+
13
23
  ## Voice Detection
14
24
 
15
25
  Voice messages arrive as `message.voice` in Telegram updates. The bridge's media processing detects these and sets `kind: "voice"` on the downloaded file. Regular audio files (`message.audio`) get `kind: "audio"`; `mirror` mode treats both voice notes and audio uploads as voice input for reply-policy tagging.
@@ -539,6 +539,8 @@ export default function (pi: Pi.ExtensionAPI) {
539
539
  sendRichMessage,
540
540
  editMessage: editTelegramMessageText,
541
541
  getAssistantRenderingMode: configControls.getAssistantRenderingMode,
542
+ planButtonReply:
543
+ Outbound.createTelegramButtonReplyPlanner(buttonActionStore),
542
544
  execCommand: CommandTemplates.execCommandTemplate,
543
545
  getHandlers: configStore.getOutboundHandlers,
544
546
  recordRuntimeEvent,
@@ -514,6 +514,18 @@ export function registerTelegramCommandsAndTools({
514
514
  sendMarkdownReply(chatId, undefined, markdown, options),
515
515
  recordRuntimeEvent,
516
516
  });
517
+ const queueAgentConnectionContext = (connected: boolean): void => {
518
+ pi.sendMessage(
519
+ {
520
+ customType: "telegram-connection-state",
521
+ content: connected
522
+ ? Prompts.TELEGRAM_CONNECTED_CONTEXT_MESSAGE
523
+ : Prompts.TELEGRAM_DISCONNECTED_CONTEXT_MESSAGE,
524
+ display: false,
525
+ },
526
+ { deliverAs: "nextTurn" },
527
+ );
528
+ };
517
529
  Commands.registerTelegramBridgeCommands(pi, {
518
530
  promptForConfig: async (ctx, profileName) => {
519
531
  const nextProfileName = profileName ?? undefined;
@@ -581,8 +593,11 @@ export function registerTelegramCommandsAndTools({
581
593
  recordRuntimeEvent,
582
594
  });
583
595
  const completion = await runSetup(ctx);
584
- if (profileName && completion.status === "success") {
585
- ctx.ui.notify(`Profile "${profileName}" saved and connected.`, "info");
596
+ if (completion.status === "success") {
597
+ queueAgentConnectionContext(true);
598
+ if (profileName) {
599
+ ctx.ui.notify(`Profile "${profileName}" saved and connected.`, "info");
600
+ }
586
601
  }
587
602
  },
588
603
  getStatusLines,
@@ -599,6 +614,7 @@ export function registerTelegramCommandsAndTools({
599
614
  stopPolling: stopPolling ?? lockedPollingRuntime.stop,
600
615
  recoverPollingStart,
601
616
  getDisconnectThreadName,
617
+ queueAgentConnectionContext,
602
618
  updateStatus,
603
619
  getProfileNames: () =>
604
620
  Config.getTelegramProfileNames(configStore.getStoredConfig()),
@@ -334,6 +334,7 @@ export interface TelegramBridgeCommandRegistrationDeps {
334
334
  error: unknown,
335
335
  ) => Promise<TelegramPollingStartRecoveryResult>;
336
336
  getDisconnectThreadName?: () => string | undefined;
337
+ queueAgentConnectionContext?: (connected: boolean) => void;
337
338
  updateStatus: (ctx: ExtensionCommandContext) => void;
338
339
  getProfileNames?: () => string[];
339
340
  activateDefaultProfileConfig?: (ctx: ExtensionCommandContext) => Promise<void>;
@@ -468,6 +469,9 @@ export function registerTelegramBridgeCommands(
468
469
  if (result?.message) {
469
470
  ctx.ui.notify(result.message, result.ok ? "info" : "warning");
470
471
  }
472
+ if (!result || result.ok) {
473
+ deps.queueAgentConnectionContext?.(true);
474
+ }
471
475
  deps.updateStatus(ctx);
472
476
  },
473
477
  });
@@ -490,6 +494,7 @@ export function registerTelegramBridgeCommands(
490
494
  try {
491
495
  const message = await deps.stopPolling();
492
496
  if (message) ctx.ui.notify(message, "info");
497
+ deps.queueAgentConnectionContext?.(false);
493
498
  } catch (error) {
494
499
  const detail = error instanceof Error ? error.message : String(error);
495
500
  ctx.ui.notify(
@@ -871,21 +871,14 @@ export function registerTelegramBindTool(
871
871
  label: "Telegram Bind",
872
872
  description:
873
873
  "Install, explicitly replace, or invoke one named method on a managed Generative App; successful output displays directly in the active Telegram turn unless display is false.",
874
- parameters: Type.Union([
875
- Type.Object({
876
- app: Type.String(),
877
- script: Type.String(),
878
- replace: Type.Optional(Type.Boolean()),
879
- display: Type.Optional(Type.Boolean()),
880
- argument: Type.Optional(Type.Unknown()),
881
- }, { additionalProperties: false }),
882
- Type.Object({
883
- app: Type.String(),
884
- method: Type.String(),
885
- display: Type.Optional(Type.Boolean()),
886
- argument: Type.Optional(Type.Unknown()),
887
- }, { additionalProperties: false }),
888
- ]),
874
+ parameters: Type.Object({
875
+ app: Type.String(),
876
+ script: Type.Optional(Type.String()),
877
+ method: Type.Optional(Type.String()),
878
+ replace: Type.Optional(Type.Boolean()),
879
+ display: Type.Optional(Type.Boolean()),
880
+ argument: Type.Optional(Type.Unknown()),
881
+ }, { additionalProperties: false }),
889
882
  async execute(_toolCallId, params) {
890
883
  try {
891
884
  const result = await bindGenerativeApp({
@@ -1757,6 +1757,30 @@ export function createTelegramUpdateJournalStore(
1757
1757
  } catch (error) {
1758
1758
  if (error instanceof TelegramUpdateJournalError) throw error;
1759
1759
  if ((error as { code?: unknown })?.code === "ENOENT") {
1760
+ const segmentDirectory = getTelegramUpdateJournalSegmentDirectory(path);
1761
+ let orphanedSegmentNames: string[];
1762
+ try {
1763
+ orphanedSegmentNames = readdirSync(segmentDirectory).filter((name) =>
1764
+ /^\d{16}\.json$/u.test(name),
1765
+ );
1766
+ } catch (segmentError) {
1767
+ if ((segmentError as { code?: unknown })?.code === "ENOENT") {
1768
+ return { file: emptyFile(), exists: false, serializedBytes: 0 };
1769
+ }
1770
+ throw createJournalError(
1771
+ "io",
1772
+ segmentDirectory,
1773
+ "could not be read while the journal snapshot is missing",
1774
+ segmentError,
1775
+ );
1776
+ }
1777
+ if (orphanedSegmentNames.length > 0) {
1778
+ throw createJournalError(
1779
+ "invalid",
1780
+ path,
1781
+ `is missing while ${segmentDirectory} retains revision segments`,
1782
+ );
1783
+ }
1760
1784
  return { file: emptyFile(), exists: false, serializedBytes: 0 };
1761
1785
  }
1762
1786
  throw createJournalError("io", path, "could not be read", error);
@@ -40,8 +40,15 @@ export function createAgentStartDedupHook(
40
40
  };
41
41
  }
42
42
 
43
+ type TelegramBeforeAgentStartEvent = Omit<
44
+ BeforeAgentStartEvent,
45
+ "systemPrompt"
46
+ > & {
47
+ systemPrompt: string | string[];
48
+ };
49
+
43
50
  export interface TelegramBeforeAgentStartResult {
44
- systemPrompt?: string;
51
+ systemPrompt?: string | string[];
45
52
  }
46
53
 
47
54
  type TelegramBeforeAgentStartReturn =
@@ -72,7 +79,7 @@ export interface TelegramLifecycleRegistrationDeps {
72
79
  ctx: ExtensionContext,
73
80
  ) => Promise<void> | void;
74
81
  onBeforeAgentStart: (
75
- event: BeforeAgentStartEvent,
82
+ event: TelegramBeforeAgentStartEvent,
76
83
  ctx: ExtensionContext,
77
84
  ) => TelegramBeforeAgentStartReturn;
78
85
  onModelSelect: (
@@ -581,7 +588,16 @@ export function registerTelegramLifecycleHooks(
581
588
  if (!isActive(ctx)) return;
582
589
  await deps.onSessionCompact?.(event, ctx);
583
590
  });
584
- pi.on("before_agent_start", async (event, ctx) => {
591
+ // The Pi SDK still types this result as a string; compatible runtimes may
592
+ // preserve ordered system prompt blocks through the same public hook.
593
+ const registerBeforeAgentStart = pi.on.bind(pi) as unknown as (
594
+ event: "before_agent_start",
595
+ handler: (
596
+ event: TelegramBeforeAgentStartEvent,
597
+ ctx: ExtensionContext,
598
+ ) => TelegramBeforeAgentStartReturn,
599
+ ) => void;
600
+ registerBeforeAgentStart("before_agent_start", async (event, ctx) => {
585
601
  return deps.onBeforeAgentStart(event, ctx);
586
602
  });
587
603
  pi.on("model_select", async (event, ctx) => {
@@ -119,7 +119,9 @@ export interface TelegramModelMenuStateBuilderDeps<
119
119
  TelegramModelMenuStateBuilderContext<TModel>,
120
120
  > {
121
121
  runtime: TelegramModelMenuRuntime<TModel>;
122
- createSettingsManager: (cwd: string) => MenuSettingsManager;
122
+ createSettingsManager: (
123
+ cwd: string,
124
+ ) => MenuSettingsManager | PromiseLike<MenuSettingsManager>;
123
125
  getActiveModel: (ctx: TContext) => TModel | undefined;
124
126
  }
125
127
 
@@ -490,7 +492,7 @@ export function createTelegramModelMenuStateBuilder<
490
492
  threadId?: number,
491
493
  ) => Promise<TelegramModelMenuState<TModel>> {
492
494
  return async (chatId, ctx, threadId) => {
493
- const settingsManager = deps.createSettingsManager(ctx.cwd);
495
+ const settingsManager = await deps.createSettingsManager(ctx.cwd);
494
496
  return deps.runtime.buildState({
495
497
  chatId,
496
498
  threadId,
@@ -128,13 +128,14 @@ export function buildThinkingMenuReplyMarkup(
128
128
  currentThinkingLevel: ThinkingLevel,
129
129
  ): TelegramReplyMarkup {
130
130
  const rows = [[{ text: "⬆️ Main menu", callback_data: "menu:back" }]];
131
+ const levelButtons = THINKING_LEVELS.map((level) => ({
132
+ text: level === currentThinkingLevel ? `🟢 ${level}` : level,
133
+ callback_data: `thinking:set:${level}`,
134
+ }));
131
135
  rows.push(
132
- ...THINKING_LEVELS.map((level) => [
133
- {
134
- text: level === currentThinkingLevel ? `🟢 ${level}` : level,
135
- callback_data: `thinking:set:${level}`,
136
- },
137
- ]),
136
+ levelButtons.slice(0, 1),
137
+ levelButtons.slice(1, 4),
138
+ levelButtons.slice(4, 7),
138
139
  );
139
140
  return { inline_keyboard: rows };
140
141
  }
@@ -436,6 +436,11 @@ async function buildTelegramOutboundAttachmentViews(options: {
436
436
  return pendingAttachments;
437
437
  }
438
438
 
439
+ function formatTelegramOutboundToolError(error: unknown): Error {
440
+ const message = error instanceof Error ? error.message : String(error);
441
+ return new Error(`\n${message.replace(/^\n+/u, "") || "Telegram outbound operation failed."}`);
442
+ }
443
+
439
444
  export function registerTelegramOutboundAttachmentTool(
440
445
  pi: ExtensionAPI,
441
446
  deps: TelegramOutboundAttachmentToolRegistrationDeps,
@@ -496,7 +501,7 @@ export function registerTelegramOutboundAttachmentTool(
496
501
  phase: "queue",
497
502
  count: params.paths.length,
498
503
  });
499
- throw error;
504
+ throw formatTelegramOutboundToolError(error);
500
505
  }
501
506
  },
502
507
  });
@@ -548,7 +553,7 @@ export function registerTelegramOutboundMessageTool(
548
553
  });
549
554
  } catch (error) {
550
555
  deps.recordRuntimeEvent?.("message", error, { phase: "direct" });
551
- throw error;
556
+ throw formatTelegramOutboundToolError(error);
552
557
  }
553
558
  },
554
559
  });
@@ -958,6 +958,10 @@ export function createTelegramAssistantOutputSender<
958
958
  ) => Promise<TelegramSentMessage>;
959
959
  editMessage: (body: TelegramEditMessageTextBody) => Promise<unknown>;
960
960
  getAssistantRenderingMode: () => "rich" | "html";
961
+ planButtonReply?: (markdown: string) => {
962
+ markdown: string;
963
+ replyMarkup?: TReplyMarkup;
964
+ };
961
965
  execCommand: TelegramOutboundTextReplyRuntimeDeps<TReplyMarkup>["execCommand"];
962
966
  getHandlers?: TelegramOutboundTextReplyRuntimeDeps<TReplyMarkup>["getHandlers"];
963
967
  recordRuntimeEvent?: TelegramOutboundTextReplyRuntimeDeps<TReplyMarkup>["recordRuntimeEvent"];
@@ -997,11 +1001,19 @@ export function createTelegramAssistantOutputSender<
997
1001
  getHandlers: deps.getHandlers,
998
1002
  recordRuntimeEvent: deps.recordRuntimeEvent,
999
1003
  });
1004
+ const buttonReply = deps.planButtonReply?.(event.text) ?? {
1005
+ markdown: event.text,
1006
+ };
1000
1007
  await outboundRuntime.sendMarkdownReply(
1001
1008
  target.chatId,
1002
1009
  undefined,
1003
- event.text,
1004
- { target },
1010
+ buttonReply.markdown,
1011
+ {
1012
+ target,
1013
+ ...(buttonReply.replyMarkup
1014
+ ? { replyMarkup: buttonReply.replyMarkup }
1015
+ : {}),
1016
+ },
1005
1017
  );
1006
1018
  };
1007
1019
  }
@@ -151,16 +151,82 @@ export function createExtensionApiRuntimePorts(
151
151
  };
152
152
  }
153
153
 
154
- export function createSettingsManager(cwd: string): PiSettingsManager {
155
- return SettingsManager.create(cwd);
154
+ type PiSettingsManagerFactory = {
155
+ create: (cwd: string) => unknown | PromiseLike<unknown>;
156
+ };
157
+
158
+ type HostSettingsManager = {
159
+ reload?: () => void | PromiseLike<void>;
160
+ flush?: () => void | PromiseLike<void>;
161
+ getEnabledModels?: () => unknown;
162
+ setEnabledModels?: (patterns: string[] | undefined) => void;
163
+ get?: (key: string) => unknown;
164
+ set?: (key: string, value: unknown) => void;
165
+ };
166
+
167
+ function readEnabledModels(value: unknown): string[] | undefined {
168
+ if (value === undefined) return undefined;
169
+ if (Array.isArray(value) && value.every((entry) => typeof entry === "string")) {
170
+ return [...value];
171
+ }
172
+ throw new TypeError("Host settings enabledModels must be a string array or undefined.");
173
+ }
174
+
175
+ export function normalizeSettingsManager(manager: unknown): PiSettingsManager {
176
+ if (typeof manager !== "object" || manager === null) {
177
+ throw new TypeError("Host settings manager must be an object.");
178
+ }
179
+ const host = manager as HostSettingsManager;
180
+ if (typeof host.flush !== "function") {
181
+ throw new TypeError("Host settings manager must provide flush().");
182
+ }
183
+ const read = typeof host.getEnabledModels === "function"
184
+ ? () => host.getEnabledModels!.call(host)
185
+ : typeof host.get === "function"
186
+ ? () => host.get!.call(host, "enabledModels")
187
+ : undefined;
188
+ const write = typeof host.setEnabledModels === "function"
189
+ ? (patterns: string[] | undefined) =>
190
+ host.setEnabledModels!.call(host, patterns)
191
+ : typeof host.set === "function"
192
+ ? (patterns: string[] | undefined) =>
193
+ host.set!.call(host, "enabledModels", patterns ?? [])
194
+ : undefined;
195
+ if (!read || !write) {
196
+ throw new TypeError(
197
+ "Host settings manager must provide enabled-model read and write capabilities.",
198
+ );
199
+ }
200
+ return {
201
+ reload: async () => {
202
+ await host.reload?.call(host);
203
+ },
204
+ flush: async () => {
205
+ await host.flush!.call(host);
206
+ },
207
+ getEnabledModels: () => readEnabledModels(read()),
208
+ setEnabledModels: write,
209
+ };
210
+ }
211
+
212
+ export async function createSettingsManager(
213
+ cwd: string,
214
+ ): Promise<PiSettingsManager> {
215
+ // Pi returns its legacy settings surface synchronously. Compatible hosts may
216
+ // resolve a generic settings service asynchronously; normalize both once at
217
+ // the SDK boundary instead of leaking host distinctions into menu domains.
218
+ const factory = SettingsManager as unknown as PiSettingsManagerFactory;
219
+ return normalizeSettingsManager(await factory.create(cwd));
156
220
  }
157
221
 
158
222
  export function createScopedModelPatternPersister(deps: {
159
- createSettingsManager: (cwd: string) => PiSettingsManager;
223
+ createSettingsManager: (
224
+ cwd: string,
225
+ ) => PiSettingsManager | PromiseLike<PiSettingsManager>;
160
226
  clearCachedModelMenuInputs: () => void;
161
227
  }): (patterns: string[], ctx: ExtensionContext) => Promise<void> {
162
228
  return async (patterns, ctx) => {
163
- const settingsManager = deps.createSettingsManager(ctx.cwd);
229
+ const settingsManager = await deps.createSettingsManager(ctx.cwd);
164
230
  settingsManager.setEnabledModels(
165
231
  patterns.length > 0 ? patterns : undefined,
166
232
  );
@@ -7,9 +7,14 @@
7
7
  import type { BeforeAgentStartEvent } from "./pi.ts";
8
8
  import { TELEGRAM_PREFIX } from "./turns.ts";
9
9
 
10
+ export const TELEGRAM_CONNECTED_CONTEXT_MESSAGE =
11
+ "Telegram session connected. Use Telegram features for Telegram-originated turns or explicit Telegram requests; connectivity alone is not user intent.";
12
+ export const TELEGRAM_DISCONNECTED_CONTEXT_MESSAGE =
13
+ "Telegram session disconnected. Do not use Telegram delivery, actions, or Telegram-specific reply features unless the user reconnects it.";
14
+
10
15
  const LOCAL_SYSTEM_PROMPT_SUFFIX = `
11
16
 
12
- Telegram bridge available. Load the \`telegram-bridge\` Skill for Telegram delivery, actions, Threaded Mode, or diagnosis. Do not use Telegram from local/TUI prompts unless explicitly asked.`;
17
+ ${TELEGRAM_CONNECTED_CONTEXT_MESSAGE} Load the \`telegram-bridge\` Skill for Telegram-originated turns or explicit requests involving Telegram delivery, actions, Threaded Mode, or diagnosis. Do not use Telegram-specific features from unrelated local/TUI prompts.`;
13
18
 
14
19
  const TELEGRAM_TURN_SYSTEM_PROMPT_SUFFIX = `
15
20
 
@@ -31,6 +36,15 @@ export const TELEGRAM_MESSAGE_PROMPT_GUIDELINES = [
31
36
  "During an active Telegram turn, omit telegram_message for the current target and answer normally; use thread only when the user requests delivery to a different live Pi thread.",
32
37
  ] as const;
33
38
 
39
+ const TELEGRAM_TOOL_METADATA_LINES = Object.fromEntries(
40
+ [
41
+ `- telegram_attach: ${TELEGRAM_ATTACH_PROMPT_SNIPPET}`,
42
+ `- telegram_message: ${TELEGRAM_MESSAGE_PROMPT_SNIPPET}`,
43
+ ...TELEGRAM_ATTACH_PROMPT_GUIDELINES.map((line) => `- ${line}`),
44
+ ...TELEGRAM_MESSAGE_PROMPT_GUIDELINES.map((line) => `- ${line}`),
45
+ ].map((line) => [line, true]),
46
+ ) as Record<string, true>;
47
+
34
48
  const TELEGRAM_MODEL_CONTEXT_TOOL_NAMES = new Set([
35
49
  "telegram_attach",
36
50
  "telegram_bind",
@@ -132,13 +146,30 @@ export function createTelegramModelContextAvailabilityRuntime(deps: {
132
146
  };
133
147
  }
134
148
 
149
+ export type TelegramSystemPrompt = string | string[];
150
+
151
+ type TelegramBeforeAgentStartEvent = Omit<
152
+ BeforeAgentStartEvent,
153
+ "systemPrompt"
154
+ > & {
155
+ systemPrompt: TelegramSystemPrompt;
156
+ };
157
+
158
+ type TelegramBeforeAgentStartResult = {
159
+ systemPrompt: TelegramSystemPrompt;
160
+ };
161
+
162
+ type TelegramBeforeAgentStartHook = (
163
+ event: TelegramBeforeAgentStartEvent,
164
+ ) => TelegramBeforeAgentStartResult;
165
+
135
166
  export function buildTelegramBridgeSystemPrompt(options: {
136
167
  prompt: string;
137
- systemPrompt: string;
168
+ systemPrompt: TelegramSystemPrompt;
138
169
  telegramPrefix?: string;
139
170
  localSystemPromptSuffix: string;
140
171
  telegramTurnSystemPromptSuffix: string;
141
- }): { systemPrompt: string } {
172
+ }): TelegramBeforeAgentStartResult {
142
173
  const telegramPrefix = options.telegramPrefix ?? TELEGRAM_PREFIX;
143
174
  const telegramHead = telegramPrefix.endsWith("]")
144
175
  ? telegramPrefix.slice(0, -1)
@@ -151,8 +182,14 @@ export function buildTelegramBridgeSystemPrompt(options: {
151
182
  ? `${options.telegramTurnSystemPromptSuffix}\n- The current user message came from Telegram.`
152
183
  : "";
153
184
  return {
154
- systemPrompt:
155
- options.systemPrompt + options.localSystemPromptSuffix + telegramSuffix,
185
+ systemPrompt: Array.isArray(options.systemPrompt)
186
+ ? [
187
+ ...options.systemPrompt,
188
+ options.localSystemPromptSuffix + telegramSuffix,
189
+ ]
190
+ : options.systemPrompt +
191
+ options.localSystemPromptSuffix +
192
+ telegramSuffix,
156
193
  };
157
194
  }
158
195
 
@@ -162,7 +199,7 @@ export function createTelegramBeforeAgentStartHook(
162
199
  localSystemPromptSuffix?: string;
163
200
  telegramTurnSystemPromptSuffix?: string;
164
201
  } = {},
165
- ): (event: BeforeAgentStartEvent) => { systemPrompt: string } {
202
+ ): TelegramBeforeAgentStartHook {
166
203
  return (event) =>
167
204
  buildTelegramBridgeSystemPrompt({
168
205
  prompt: event.prompt,
@@ -176,23 +213,23 @@ export function createTelegramBeforeAgentStartHook(
176
213
  });
177
214
  }
178
215
 
179
- function stripTelegramToolMetadataFromSystemPrompt(
180
- systemPrompt: string,
181
- ): string {
182
- const telegramLines = new Set([
183
- `- telegram_attach: ${TELEGRAM_ATTACH_PROMPT_SNIPPET}`,
184
- `- telegram_message: ${TELEGRAM_MESSAGE_PROMPT_SNIPPET}`,
185
- ...TELEGRAM_ATTACH_PROMPT_GUIDELINES.map((line) => `- ${line}`),
186
- ...TELEGRAM_MESSAGE_PROMPT_GUIDELINES.map((line) => `- ${line}`),
187
- ]);
216
+ function stripTelegramToolMetadataFromString(systemPrompt: string): string {
188
217
  return systemPrompt
189
218
  .split("\n")
190
- .filter((line) => !telegramLines.has(line))
219
+ .filter((line) => TELEGRAM_TOOL_METADATA_LINES[line] !== true)
191
220
  .join("\n");
192
221
  }
193
222
 
223
+ function stripTelegramToolMetadataFromSystemPrompt(
224
+ systemPrompt: TelegramSystemPrompt,
225
+ ): TelegramSystemPrompt {
226
+ return Array.isArray(systemPrompt)
227
+ ? systemPrompt.map(stripTelegramToolMetadataFromString)
228
+ : stripTelegramToolMetadataFromString(systemPrompt);
229
+ }
230
+
194
231
  export interface TelegramProactivePromptHookDeps<TContext> {
195
- baseHook?: (event: BeforeAgentStartEvent) => { systemPrompt: string };
232
+ baseHook?: TelegramBeforeAgentStartHook;
196
233
  reconcileAvailability?: () => void;
197
234
  isAvailable: (ctx: TContext) => boolean;
198
235
  }
@@ -200,9 +237,9 @@ export interface TelegramProactivePromptHookDeps<TContext> {
200
237
  export function createTelegramProactiveBeforeAgentStartHook<TContext>(
201
238
  deps: TelegramProactivePromptHookDeps<TContext>,
202
239
  ): (
203
- event: BeforeAgentStartEvent,
240
+ event: TelegramBeforeAgentStartEvent,
204
241
  ctx: TContext,
205
- ) => Promise<{ systemPrompt: string }> {
242
+ ) => Promise<TelegramBeforeAgentStartResult> {
206
243
  const baseHook = deps.baseHook ?? createTelegramBeforeAgentStartHook();
207
244
  return async (event, ctx) => {
208
245
  deps.reconcileAvailability?.();
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.36.1",
3
+ "version": "0.36.7",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: generated-control-surface
3
- description: Proactively compiles current state, available capabilities, and user intent into contextual, evidence-backed, ephemeral prompt-button interfaces. Use on Telegram turns and other prompt-button transports whenever controls materially shorten likely feedback, without requiring an explicit user request; route reusable deterministic loops toward Generative Apps, omit decorative UI, and preserve domain ownership while fixed transport menus and callbacks remain with their runtime owners.
3
+ description: Proactively compiles current state, available capabilities, and user intent into contextual, evidence-backed, ephemeral prompt-button interfaces. Use without an explicit button request on Telegram-originated turns when controls materially shorten likely feedback, or when a user explicitly requests controls on another prompt-button transport; connection or proactive projection alone is not activation intent. Route reusable deterministic loops toward Generative Apps, omit decorative UI, and preserve domain ownership while fixed transport menus and callbacks remain with their runtime owners.
4
4
  ---
5
5
 
6
6
  # Generated Control Surface
@@ -29,7 +29,7 @@ This Skill and `generative-apps` share one logical button-matrix and `label + pr
29
29
 
30
30
  When a generated surface reveals repeated stable interaction with bounded state and deterministic transitions, load the complementary `generative-apps` Skill and consider compiling that loop. A Generative App may retain ordinary model-mediated prompt buttons beside deterministic bound methods, so only the stable actions need bypass inference. Conversely, keep one-off, interpretive, changing, and context-heavy interaction here when a reusable script would add no concrete latency, token, cost, reliability, or UX value.
31
31
 
32
- On Telegram turns, evaluate this Skill proactively rather than waiting for the user to ask for buttons. Load and apply it when a likely next decision, approval, navigation step, inspection, or bounded action can be made materially easier through controls; its correct output may still contain zero buttons when the admission test fails.
32
+ On Telegram-originated turns, evaluate this Skill proactively rather than waiting for the user to ask for buttons. A connected Telegram session, available button syntax, or proactive projection of local output is capability evidence, not activation intent: do not load this Skill for an unrelated local/TUI prompt merely because its answer may also appear in Telegram. Load and apply it when the current turn is Telegram-originated or the user explicitly requests a prompt-button surface and a likely next decision, approval, navigation step, inspection, or bounded action can be made materially easier through controls; its correct output may still contain zero buttons when the admission test fails.
33
33
 
34
34
  A control surface may expose:
35
35
 
@@ -7,6 +7,12 @@ description: Operates pi-telegram turns, replies, attachments, direct delivery,
7
7
 
8
8
  Use pi-telegram as a mobile companion surface for the current Pi session. Preserve the current Telegram target, ordinary reply ownership, durable queue semantics, and the boundary between agent intent and bridge transport.
9
9
 
10
+ ## Connection Awareness
11
+
12
+ `Telegram session connected.` means this Pi instance currently has authorized Telegram transport, so Telegram tools and proactive projection may be available. It does not mean the current prompt came from Telegram and does not grant intent to add Telegram actions to unrelated local/TUI work. Use Telegram-specific reply features proactively only on a turn carrying structured Telegram origin; from local/TUI prompts, use them only when the user explicitly requests Telegram delivery or the established workflow is explicitly Telegram-mediated.
13
+
14
+ `Telegram session disconnected.` revokes that availability. Do not attempt Telegram delivery, actions, buttons, or voice until a later connected context is present. Treat the newest connection-state context as authoritative.
15
+
10
16
  ## Turn Recognition
11
17
 
12
18
  Telegram-originated prompts carry structured context:
@@ -46,9 +52,9 @@ Use `telegram_attach` outside Telegram turns only when the user explicitly reque
46
52
 
47
53
  ## Assistant-Authored Actions
48
54
 
49
- Before compiling assistant-authored controls, inspect already-loaded capability guidance for an advertised maintained Generative App or view/controller adapter. When that owner-provided view exists and the current intent concerns repeated interaction, load and follow the bundled `generative-apps` Skill and prefer binding or invoking the existing app over synthesizing one-shot prompt buttons. This is a routing breadcrumb, not permission for the bridge to discover capability-specific apps, own their state, or hard-code their identities.
55
+ Before compiling assistant-authored controls, inspect already-loaded capability guidance for an advertised maintained Generative App or view/controller adapter. When that owner-provided view exists and the current intent concerns repeated interaction, load and follow the bundled `generative-apps` Skill and prefer binding or invoking the existing app over synthesizing one-shot prompt buttons. This routing guidance is not permission for the bridge to discover capability-specific apps, own their state, or hard-code their identities.
50
56
 
51
- Otherwise, on Telegram turns, proactively load `generated-control-surface` when a likely next decision or action may benefit from prompt buttons; do not wait for an explicit button request, and accept zero controls when its admission rules reject decorative or low-value UI.
57
+ Otherwise, only on Telegram-originated turns, proactively load `generated-control-surface` when a likely next decision or action may benefit from prompt buttons; do not activate it merely because Telegram is connected or local output may be projected by proactive push. Do not wait for an explicit button request on a Telegram-originated turn, and accept zero controls when its admission rules reject decorative or low-value UI.
52
58
 
53
59
  `telegram_button` and `telegram_voice` are hidden top-level HTML comments, not tools. Emit them at column zero, outside lists, quotes, code blocks, and indentation.
54
60
 
@@ -116,6 +122,8 @@ Prefer no-code command-template configuration in `telegram.json` before adding a
116
122
  - `outboundHandlers` transforms final replies.
117
123
  - Voice transcription handlers can match `type: "voice"` or `mime: "audio/*"`; stdout becomes `[outputs]`.
118
124
 
125
+ When asked to configure voice rather than merely operate it, follow the provider-neutral contracts in `docs/voice.md`, `docs/inbound.md`, `docs/outbound.md`, and `docs/command-templates.md` from the pi-telegram package or repository. Inspect the available Skill catalog and trusted local executables for STT, TTS, and media conversion capabilities; check only whether required environment variables exist, never reveal their values. Preserve unrelated `telegram.json` fields, order multiple matching inbound handlers as fallbacks, require OGG/Opus output for native voice delivery, and validate each stage before a live Telegram smoke test. Keep `voice.replyMode` at its existing value unless the user requests a policy change: the default `hidden` mode is fully functional because explicit top-level `telegram_voice` actions still use the configured synthesis pipeline.
126
+
119
127
  When configuration is insufficient, use documented `@llblab/pi-telegram/*` public API subpaths. Never import package-private `lib/*`, start another polling loop, or bypass bridge ownership with raw Bot API access.
120
128
 
121
129
  ## Safety
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-kit",
3
- "version": "0.1.0",
3
+ "version": "0.1.3",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -41,9 +41,9 @@
41
41
  ],
42
42
  "dependencies": {
43
43
  "@llblab/pi-actors": "0.49.1",
44
- "@llblab/pi-codex-usage": "0.9.2",
45
- "@llblab/pi-grow-loop": "0.7.1",
46
- "@llblab/pi-telegram": "0.36.1"
44
+ "@llblab/pi-codex-usage": "0.9.3",
45
+ "@llblab/pi-grow-loop": "0.7.2",
46
+ "@llblab/pi-telegram": "0.36.7"
47
47
  },
48
48
  "bundledDependencies": [
49
49
  "@llblab/pi-actors",
@@ -63,5 +63,11 @@
63
63
  "./node_modules/@llblab/pi-grow-loop/skills",
64
64
  "./node_modules/@llblab/pi-telegram/skills"
65
65
  ]
66
- }
66
+ },
67
+ "bundleDependencies": [
68
+ "@llblab/pi-actors",
69
+ "@llblab/pi-codex-usage",
70
+ "@llblab/pi-grow-loop",
71
+ "@llblab/pi-telegram"
72
+ ]
67
73
  }