2 Commits
Author SHA1 Message Date
evandro c972bf0811 docs(readme): redesenha o README com hero em SVG [skip ci] 2026-10-24 04:57:57 -03:00
evandro e24c5c6bba docs: redesign do README (padrão TwitterBR-MOD) — dados atualizados
Validate / validate (push) Failing after 5m21s
2026-10-13 21:47:35 -03:00
7 changed files with 130 additions and 1203 deletions

No files matched your search

+98 -287
View File
@@ -1,339 +1,150 @@
# oh-my-pi-telegram <div align="center">
![pi-telegram screenshot](screenshot.png) <img src="assets/readme/hero.svg" width="100%" alt="oh-my-pi-telegram: drive a live omp session from a private Telegram chat, with /start, the /compact, /next and /stop menu and prompts sent from the phone">
**Telegram companion hub for live [Oh My Pi](https://github.com/can1357/oh-my-pi) (omp) sessions.** ![Node >=22.19.0](https://img.shields.io/badge/Node-%E2%89%A522.19.0-5FA04E?style=flat-square&logo=nodedotjs&logoColor=white)
![omp >=16.5.2](https://img.shields.io/badge/omp-%E2%89%A516.5.2-informational?style=flat-square)
![Version 0.22.0-evandro.1](https://img.shields.io/badge/version-0.22.0--evandro.1-informational?style=flat-square)
![License MIT](https://img.shields.io/badge/license-MIT-blue?style=flat-square)
> **This is the Evandro Fonseca Junior fork** of [`@llblab/[email protected]`](https://github.com/llblab/pi-telegram/tree/v0.22.0) (SHA `afe09c5`), repackaged and validated as a drop-in extension for [`can1357/oh-my-pi`](https://github.com/can1357/oh-my-pi) — the AI coding agent for the terminal. [Issues](https://git.evandro.dev.br/evandro/oh-my-pi-telegram/issues) · [Docs](docs/README.md) · [Changelog](CHANGELOG.md) · [Backlog](BACKLOG.md)
>
> Upstream lineage: `badlogic/pi-telegram` → `llblab/pi-telegram` (0.22.0) → **`evandrodevbr/oh-my-pi-telegram`** (this fork).
## Oh My Pi (omp) Compatibility </div>
This fork is built and tested against: ![oh-my-pi-telegram running: an omp terminal session on the left and two Telegram chats on the right, one with the /start menu and session status, one with the model picker](screenshot.png)
| Component | Version | Status | ## What it is
| --- | --- | --- |
| `can1357/oh-my-pi` (CLI: `omp`) | `>=16.5.2` (`@oh-my-pi/pi-coding-agent`) | ✅ verified |
| `@earendil-works/pi-{ai,agent-core,coding-agent}` | `>=0.80.6` | ✅ verified (peerDeps preserved from upstream) |
| `node` | `>=22.19.0` | ✅ verified on Node 24.15.0 |
| Telegram Bot API | current | ✅ unchanged from upstream |
### Install into omp oh-my-pi-telegram is an [Oh My Pi](https://github.com/can1357/oh-my-pi) (`omp`) extension that turns a private chat with your own Telegram bot into a companion for the session running on your machine. From the phone you send prompts, files and voice notes, queue and steer turns, switch the model or thinking level, and receive streamed previews and the final answer.
It is a fork of [`@llblab/[email protected]`](https://github.com/llblab/pi-telegram/tree/v0.22.0) (SHA `afe09c5`), repackaged as `@evandrodevbr/oh-my-pi-telegram` and declared as an `omp` extension.
| Surface | What you can do |
|---|---|
| Prompt intake | Send text, replies, edits, images, files, albums and voice notes into the session. |
| Queue control | Messages sent while the agent is busy become queued turns: inspect, delete, promote, continue, abort, stop or force the next one. |
| Operator menu | `/start` opens status, prompt templates, model, thinking level, settings, queue and extension sections. |
| Rich replies | Rich Markdown (default) or legacy Markdown-to-HTML for final answers; optional live draft previews; native `...typing` status while the agent works. |
| Files and voice | Inbound files land in `<agent dir>/tmp/telegram` (50 MiB default limit); `telegram_attach` returns artifacts as real files; inbound voice goes through command templates or STT handlers; outbound voice through `telegram_voice` and TTS providers. |
| Buttons | Hidden `telegram_button` comments become inline buttons whose callbacks re-enter the session. |
| Threaded Mode | One leader plus visible follower `omp` instances, each with its own Telegram thread, through one bot. |
| Named profiles | Independent bots from one agent directory, with isolated polling, diagnostics and bus transport. |
## Why use it
It is a runtime adapter, not a remote terminal. A Telegram prompt is a normal model turn in the session that is active right now and inherits its post-compaction context, so token cost is not proportional only to the new mobile message.
Why this fork and not upstream: `@llblab/[email protected]` targets the Pi extension platform (`@earendil-works/pi-*`), and `omp` consumes the same platform republished as `@oh-my-pi/pi-coding-agent`. This fork:
1. Renames the package to `@evandrodevbr/oh-my-pi-telegram` so it is unambiguous in the `omp` extension registry.
2. Keeps the upstream `peerDependencies` on `@earendil-works/pi-{ai,agent-core,coding-agent}` `>=0.80.6`, with no code changes or shims.
3. Declares the extension through an `omp` block in `package.json` (extensions, display name, `compatibleWith: >=16.5.2`); the old `pi` block was removed in `572967c`.
Do not use it if you need a real terminal from the phone: there are no hidden `omp` processes, no PTY and no raw TTY input. Telegram can compact the current session but cannot create, resume, fork, browse or switch sessions, and arbitrary slash commands are never forwarded to the TUI. For plain Pi, use upstream `@llblab/pi-telegram`.
## How it works
- The active `omp` instance owns `getUpdates` polling for one bot. You start it with `/telegram-connect`; Telegram never launches hidden `omp` processes.
- Each Telegram message becomes a turn: it goes to the queue or is dispatched to the active session, then streams back as a preview with the native typing status and ends as a final Rich Markdown reply, optionally with files, voice, buttons or callback actions.
- In Threaded Mode, follower instances talk to the leader over a local bus (Unix socket or named pipe) and get their own Telegram thread.
- Code layout: `index.ts` is the composition root, `lib/*.ts` are flat domain modules (queue, polling, locks, bus, menus, rendering, delivery), `api/*.ts` are the public entry points for companion extensions, and `docs/` holds the architecture and contract documents.
## Quickstart
Requires `omp` >= 16.5.2 (`package.json` → `omp.compatibleWith`) and Node.js >= 22.19.0 (`engines.node`).
```bash ```bash
# From npm (after this fork is published): # From git (this repository)
omp install npm:@evandrodevbr/oh-my-pi-telegram omp plugin install https://git.evandro.dev.br/evandro/oh-my-pi-telegram.git
# From git (always works): # From a local checkout (development)
omp install git:github.com/evandrodevbr/oh-my-pi-telegram omp plugin link /path/to/oh-my-pi-telegram
# From a local checkout (development):
omp install --link /path/to/oh-my-pi-telegram
``` ```
Then inside `omp`: The npm package `@evandrodevbr/oh-my-pi-telegram` is not published yet. Restart (or reload) `omp` so the extension loads, then:
```bash 1. Create a bot with [@BotFather](https://t.me/BotFather) (`/newbot`) and copy the token.
/telegram-setup # paste bot token 2. In `omp`, run `/telegram-setup` and paste the token.
/telegram-connect # activate polling for this omp instance 3. Run `/telegram-connect` to start polling for this instance.
``` 4. In the bot DM, send `/start`.
In your Telegram bot DM: > [!WARNING]
> The first Telegram user to message the bot becomes the allowed owner; everyone else is ignored. Pair right after `/telegram-connect`, and never put the bot token in chat, source, committed config or logs. Setup can prefill the token from `TELEGRAM_BOT_TOKEN`, `TELEGRAM_BOT_KEY`, `TELEGRAM_TOKEN` or `TELEGRAM_KEY`.
```text The saved config is `<agent dir>/telegram.json`. The agent directory is `PI_CODING_AGENT_DIR` when set, `~/.omp/agent` when the process is `omp`, and `~/.pi/agent` otherwise (`lib/paths.ts`).
/start
```
The first user becomes the allowed owner. ## Usage
### Why this fork exists <details>
<summary>Commands</summary>
`llblab/[email protected]` targets the **Pi extension platform** (`@earendil-works/pi-*`). `omp` ships the same platform but republishes it under `@oh-my-pi/pi-coding-agent` with the additional harness tooling (LSP, DAP, hashline, subagents, advisor, TTSR, …). This fork: In the bot DM:
1. **Renames the npm package** to `@evandrodevbr/oh-my-pi-telegram` so it is unambiguous in the `omp` extension registry.
2. **Keeps the upstream `peerDependencies`** on `@earendil-works/*` (these are the canonical runtime packages that both Pi and omp consume).
3. **Adds an `omp` field** in `package.json` with explicit compatibility metadata for downstream extension discovery.
4. **Re-runs every upstream gate** (`typecheck` → `test` → `audit` → `pack:check`) on the exact `v0.22.0` source — 1229 tests pass, 0 vulnerabilities, pack clean.
5. **Adds this README section** so the fork is discoverable as the Telegram bridge for omp.
## What `pi-telegram` (and this fork) does
`pi-telegram` turns a private Telegram DM into a mobile operator surface for Pi/omp. It accepts prompts, queues work, streams readable previews, delivers final replies and files, exposes safe controls, and lets companion extensions add Telegram-native capabilities without owning a second bot loop.
It is a **runtime adapter**, not a remote terminal. Start or supervise work in the `omp` TUI, then continue from Telegram while away from the keyboard. Each Telegram destination follows a running `omp` 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.
This fork is derived from [`llblab/[email protected]`](https://github.com/llblab/pi-telegram/tree/v0.22.0) (SHA `afe09c5`), which itself is an actively maintained fork of [`badlogic/pi-telegram`](https://github.com/badlogic/pi-telegram).
## Install (npm/git, same as upstream)
From npm:
```bash
pi install npm:@evandrodevbr/oh-my-pi-telegram
```
From git:
```bash
pi install git:github.com/evandrodevbr/oh-my-pi-telegram
```
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.
## Quick Start
### 1. Create a Telegram bot
1. Open [@BotFather](https://t.me/BotFather).
2. Run `/newbot`.
3. Pick a name and username.
4. Copy the bot token.
### 2. Configure Pi/omp
Run this inside Pi/omp:
```bash
/telegram-setup
```
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`. Named profiles are optional; the ordinary `/telegram-setup` and `/telegram-connect` flow keeps using the default profile. 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.
### 3. Connect this Pi/omp instance and its active session
```bash
/telegram-connect
```
The connected instance owns Telegram polling. Use `/telegram-connect <name>` to activate a named profile. Each profile is a parallel bot runtime with isolated polling, diagnostics, Threaded Mode state, and local bus transport; the unnamed default profile keeps legacy paths. In classic mode each profile uses a singleton lock. When Telegram private-chat Threaded Mode is available, one live instance becomes the profile's leader and later visible Pi instances register as followers.
### 4. Pair your Telegram account
Open the bot DM and send:
```text
/start
```
The first Telegram user to message the bot becomes the allowed owner. Other users are ignored.
## What It Feels Like
- Start a task in the terminal, walk away, and keep supervising it from your phone.
- Send another prompt while Pi is busy; it becomes a queued Telegram turn instead of interrupting the active run.
- Open `/start` to inspect status, model, thinking, settings, prompt templates, and queue controls.
- Send voice, images, files, replies, edits, or media groups; the bridge turns them into Pi context.
- Ask for an artifact; `telegram_attach` returns it through the active reply or direct Telegram delivery.
- In Threaded Mode, run multiple visible Pi instances through one bot, each with its own Telegram thread.
- Configure named profiles to run independent Telegram bots from the same Pi agent directory without sharing transport or routing state.
## Product Model
| Lens | What `pi-telegram` owns |
| --- | --- |
| Operator companion | A phone-width control surface for the active session of a running Pi instance |
| Runtime adapter | Telegram targets mapped to Pi instances, then into each instance's current session lifecycle, queueing, previews, final replies, and artifacts |
| Telegram UI harness | Menus, settings, callbacks, Rich Markdown, drafts, active status, buttons, voice, and files |
| Multi-instance organism | One leader plus explicit visible followers routed through Telegram private-chat threads |
| Extension platform | Commands, sections, status rows, update handlers, inbound/outbound handlers, and voice providers |
| Safety boundary | No hidden Pi processes, no fake terminal, no PTY tricks, no arbitrary TUI slash-command forwarding |
## Feature Showcase
`pi-telegram` is intentionally broad: it is a Telegram-shaped runtime surface, not only a message relay. This catalogue keeps the practical feature surface visible while detailed contracts stay in `/docs`.
| Surface | What you can do | Why it matters |
| --- | --- | --- |
| Prompt intake | Send text, replies, edits, images, files, albums, voice notes, and handler output into Pi. | Telegram becomes a real mobile input surface with file/context references, not just a text tunnel. |
| Queue control | Inspect waiting turns, delete stale work, promote important prompts, continue, abort, stop, or force the next queued item. | Long Pi tasks keep running while new mobile prompts stay visible and controllable instead of interrupting or disappearing. |
| Operator menu | Use `/start` for status, prompt templates, model, thinking, settings, queue, extension sections, and diagnostics. | The bot is an operator panel, not a command cheat sheet. |
| Prompt templates | Run Pi prompt templates as Telegram-safe commands such as `/fix_tests`. | Reusable local workflows become phone-accessible without exposing arbitrary terminal commands. |
| Model and thinking | Switch model or reasoning level from Telegram through safe continuation flows. | Mobile control can adjust execution strategy without tearing down the current session. |
| Compaction | Confirm `/compact`, show native active status during compaction, and preserve Telegram-owned turn semantics. | Context maintenance is visible and safe from the phone. |
| Draft previews | Show Telegram's native `…typing` indicator whenever the connected instance is doing agent work, or enable Rich Draft previews for streamed answer text. | Local prompts, Telegram turns, and autonomous continuations remain visibly active while draft visibility stays independent from final rendering. |
| Assistant rendering | Choose Native Rich Markdown or legacy Markdown-to-HTML for final assistant replies. | Renderer compatibility is explicit instead of being conflated with draft previews. |
| Bridge UI rendering | Render tool rows, reasoning/technical steps, menus, queue controls, status, settings, diagnostics, and sections through explicit Telegram HTML/plain UI. | Harness-owned surfaces remain operationally predictable and visually distinct from model-authored answers. |
| Inbound files | Download inbound files to the Pi agent temp directory with size limits. | Screenshots, PDFs, datasets, and artifacts enter Pi as inspectable local files. |
| Outbound artifacts | Return generated files through `telegram_attach` during active turns or explicit direct delivery. | Agents send real artifacts as files, not pasted blobs. |
| Voice input | Route audio through configured command-template handlers, programmatic handlers, or STT providers. | Voice notes become usable prompt context. |
| Voice output | Use `telegram_voice`, reply modes, configured voice handlers, and TTS providers. | Replies can become Telegram voice messages when the workflow calls for it. |
| Buttons | Turn top-level `telegram_button` comments into inline buttons. | Assistant-authored choices become native Telegram interactions. |
| Callback routing | Route known callbacks to the owner extension and unknown callbacks back into Pi. | Companion extensions can build UI without polling Telegram themselves. |
| Threaded Mode | Run one leader plus visible follower Pi instances through named private-chat threads. | One bot can host a local multi-instance Pi organism without hidden process spawning. |
| Reroute and restore | Preserve unknown threads and offer explicit target choices. | Telegram client state can be repaired without silently deleting or hijacking prompts. |
| Extension sections | Add menu sections, commands, status rows, settings, callbacks, and delivery helpers from companion extensions. | `pi-telegram` becomes a platform surface for other Pi extensions. |
| Runtime diagnostics | Use `/telegram-status` and recent runtime events for connection, role, queue, transport, and failure evidence. | Debugging lives in the operator surface instead of hidden logs only. |
| Safety and ownership | Pair one owner, lock transport, scope targets, and reject fake terminal behavior. | Remote access remains explicit, bounded, and understandable. |
## Core Loop
```text
Telegram message
-> Telegram turn
-> queue or active dispatch
-> Pi agent lifecycle
-> streaming preview / native active status
-> final Rich Markdown reply
-> optional files, voice, buttons, or callback actions
```
The bridge keeps Telegram responsive without stealing Pi's runtime model. Queueing, model changes, compaction, aborts, final delivery, and direct artifact sends all stay scoped to the Pi instance that accepted the work.
## Telegram Controls
Use these in the bot DM.
| Command | Purpose | | Command | Purpose |
| --- | --- | |---|---|
| `/start` | Pair when needed and open the main operator menu | | `/start` | Pair when needed and open the operator menu |
| `/compact` | Confirm and run session compaction when safe | | `/compact` | Confirm and run session compaction when safe |
| `/next` | Dispatch the next queued turn, aborting first if needed | | `/next` | Dispatch the next queued turn, aborting first if needed |
| `/continue` | Enqueue a priority continuation prompt | | `/continue` | Enqueue a priority continuation prompt |
| `/abort` | Abort the active run while preserving the queue | | `/abort` | Abort the active run, keeping the queue |
| `/stop` | Abort the active run and clear waiting Telegram turns | | `/stop` | Abort the active run and clear waiting Telegram turns |
Hidden compatibility shortcuts: `/help`, `/status`, `/model`, `/thinking`, `/queue`, and `/settings` jump into the same menu system. `/help`, `/status`, `/model`, `/thinking`, `/queue` and `/settings` are hidden shortcuts into the same menus.
## Pi Commands Inside `omp`:
Run these inside Pi.
| Command | Purpose | | Command | Purpose |
| --- | --- | |---|---|
| `/telegram-setup` | Save or update the default bot token | | `/telegram-setup [profile]` | Save or update the default (or a named-profile) bot token |
| `/telegram-setup <profile>` | Save or update a named-profile bot token | | `/telegram-connect [profile]` | Activate a profile and acquire its transport ownership |
| `/telegram-connect` | Activate the default profile and acquire its transport ownership |
| `/telegram-connect <profile>` | Activate a named profile and acquire its transport ownership |
| `/telegram-disconnect` | Stop polling and release ownership | | `/telegram-disconnect` | Stop polling and release ownership |
| `/telegram-status` | Inspect connection, mode, queue, transport, and recent diagnostics | | `/telegram-status` | Inspect connection, mode, queue, transport and diagnostics |
Named profile identifiers contain only lowercase ASCII letters and digits (maximum 32 characters); `default`, `main`, and `active` remain reserved. Profile names use lowercase ASCII letters and digits (max 32 characters); `default`, `main` and `active` are reserved.
## Main Surfaces </details>
### Operator Menu <details>
<summary>Environment variables</summary>
`/start` opens the Telegram-native control panel: status, prompt-template commands, model selection, thinking level, settings, queue controls, and extension sections. It is the primary Telegram UI; reaction shortcuts are secondary queue affordances.
### Queue Runtime
Messages sent while Pi is busy become queued turns. Priority lanes support control actions and model-switch continuations. Queue controls let you inspect, delete, promote, and dispatch work from Telegram without touching the terminal.
### Native Rich Markdown
Rich Markdown is the default model-answer membrane. Complete assistant and guest model replies use Telegram's native Rich Message APIs, while tool-call rows, reasoning/thinking blocks, menus, status rows, queue controls, settings, diagnostics, and other harness-owned surfaces use explicit Telegram HTML/plain rendering. This keeps meaningful model-authored answers visually distinct from bridge-owned operational UI. Two Settings controls keep the layers separate: `Draft previews` toggles live `sendRichMessageDraft` frames, while `Assistant rendering` chooses final-answer delivery (`rich` Native Rich Markdown or `html` legacy Markdown-to-HTML).
### Files And Artifacts
Inbound files land under `<agent-dir>/tmp/telegram` and default to a 50 MiB limit. `telegram_attach` is the canonical outbound file path. During Telegram-originated turns it attaches to the active reply; during explicit local/TUI delivery it can send to the paired/default chat or routed Threaded Mode target.
### Voice And Media
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.
### Buttons And Callbacks
Assistant replies can include top-level hidden `telegram_button` comments. The bridge strips the comments from visible text, renders inline buttons, and routes callbacks back into Pi as queued prompts or extension-owned callback actions.
### Threaded Mode And Multi-Instance Bus
Classic private DM mode is the base product mode. When Telegram private-chat Threaded Mode is available, the bridge enables a local leader/follower bus automatically:
- One live leader owns `getUpdates`.
- Followers are visible Pi processes started by the operator.
- Each connected instance gets a Telegram thread target.
- Follower session replacement automatically reconnects the new session context to the same thread instead of requiring another manual connect.
- Unknown threads are preserved and offered explicit reroute/restore choices.
- Telegram never launches hidden Pi processes.
| Mode | Best for | Runtime shape |
| --- | --- | --- |
| Classic DM | One running Pi instance and its active session controlled from one private bot chat | One polling owner, one queue/runtime surface |
| Threaded Mode | Several visible Pi instances sharing one bot | One leader owns transport; each named private-chat thread follows its assigned instance and current session |
## Environment Configuration
Most controls live in Pi commands or the Telegram menu. Environment variables remain for bootstrap and transport boundaries:
| Area | Variables | | Area | Variables |
| --- | --- | |---|---|
| Bot token bootstrap | `TELEGRAM_BOT_TOKEN`, `TELEGRAM_BOT_KEY`, `TELEGRAM_TOKEN`, `TELEGRAM_KEY` | | Bot token bootstrap | `TELEGRAM_BOT_TOKEN`, `TELEGRAM_BOT_KEY`, `TELEGRAM_TOKEN`, `TELEGRAM_KEY` |
| HTTP proxy | `HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY`, plus `NODE_USE_ENV_PROXY=1` or Node `--use-env-proxy` | | Network family | `PI_TELEGRAM_NETWORK_FAMILY` = `auto`, `ipv4`, `ipv6` or `ipv4-fallback` (default) |
| Telegram network family | `PI_TELEGRAM_NETWORK_FAMILY=auto`, `ipv4`, `ipv6`, or `ipv4-fallback` |
| Agent data root | `PI_CODING_AGENT_DIR` | | Agent data root | `PI_CODING_AGENT_DIR` |
| Inbound file limit | `PI_TELEGRAM_INBOUND_FILE_MAX_BYTES`, `TELEGRAM_MAX_FILE_SIZE_BYTES` | | Inbound file limit | `PI_TELEGRAM_INBOUND_FILE_MAX_BYTES`, `TELEGRAM_MAX_FILE_SIZE_BYTES` |
| Outbound attachment limit | `PI_TELEGRAM_OUTBOUND_ATTACHMENT_MAX_BYTES`, `TELEGRAM_MAX_ATTACHMENT_SIZE_BYTES` | | Outbound attachment limit | `PI_TELEGRAM_OUTBOUND_ATTACHMENT_MAX_BYTES`, `TELEGRAM_MAX_ATTACHMENT_SIZE_BYTES` |
Defaults are chosen for ordinary private-bot use: saved config in `~/.pi/agent`, inbound temp files in `~/.pi/agent/tmp/telegram`, `assistant: { rendering: "rich", draftPreviews: false }` for assistant answer output, and native Telegram active status for long-running turns. Defaults: inbound temp files in `<agent dir>/tmp/telegram`, and `assistant: { rendering: "rich", draftPreviews: false }`.
## Extension Platform </details>
Companion extensions can integrate with Telegram without owning polling or transport: Full reference: [architecture](docs/architecture.md), [public API](docs/public-api.md), [delivery](docs/delivery.md), [inbound](docs/inbound.md), [outbound](docs/outbound.md), [voice](docs/voice.md), [multi-instance bus](docs/multi-instance-bus.md) and the rest in the [docs index](docs/README.md).
- Register Telegram slash commands. ## Project status
- Add menu sections and settings surfaces.
- Add compact status rows.
- Deliver target-aware operational views and chat actions from companion code.
- Observe normalized assistant, reasoning, tool, compaction, and settlement activity without blocking Pi.
- Handle update/callback namespaces.
- Provide inbound preprocessing handlers.
- Provide outbound voice synthesis.
- Use direct delivery helpers for explicit local/TUI sends.
Stable public entrypoints are documented in [Public API](./docs/public-api.md), [Telegram Delivery API](./docs/delivery.md), [Telegram Activity API](./docs/activity.md), [Extension Sections](./docs/sections.md), [Inbound Handlers](./docs/inbound.md), [Outbound Handlers](./docs/outbound.md), [Updates](./docs/updates.md), and [Voice Integration](./docs/voice.md). Fork release `0.22.0-evandro.1`. Per [`CHANGELOG.md`](CHANGELOG.md), the fork re-ran the upstream gates: typecheck, 1,229 of 1,230 tests passing with one platform-only skip, `npm audit` with 0 vulnerabilities and a clean `npm pack --dry-run`. CI (`.github/workflows/validate.yml`) runs `npm ci` and `npm run validate` on Ubuntu with Node 24 for every push and pull request.
## Safety Boundaries - A design spec for an `omp` status-bar segment ([`docs/superpowers/specs/`](docs/superpowers/specs/2026-07-15-telegram-omp-status-design.md)) needs a new `omp` core API and is not implemented here.
- In Threaded Mode, threadless messages sent from the `All` tab are not routed as prompts once bound threads exist; use a bound thread tab.
- `release.yml` needs a `## <version>` heading in `CHANGELOG.md`; the fork entry sits under `## Unreleased` as a `###` heading, so a tag fails that check until the section is promoted.
- Open work (promoted-follower reload evidence, native Windows Threaded Mode follow-ups, Telegram client and Rich Markdown quirks) is tracked in [`BACKLOG.md`](BACKLOG.md).
`pi-telegram` intentionally does not: Development:
- Spawn hidden Pi follower processes.
- Pretend Telegram is a terminal or PTY.
- Forward arbitrary Telegram slash commands into the Pi TUI.
- Inject raw TTY input or terminal-control sequences.
- Replace Pi session lifecycle without an official Pi API.
- Let non-owner Telegram users control the bridge.
Telegram is a companion surface around a live Pi runtime, not a second runtime. It can compact the current session, but it cannot create, resume, fork, browse, or switch sessions until Pi exposes safe public extension APIs for those operations.
A Telegram prompt is a normal model turn in the active Pi session and therefore inherits that session's active post-compaction context; the bridge does not make token cost proportional only to the new mobile message. Current releases keep per-turn guidance small and transient, with detailed bridge instructions available on demand through `telegram_help` instead of persisted in every user turn. Pi session JSONL contains model history; profile-scoped pi-telegram `logs*.jsonl` contains redacted operational events and is never model context.
## Documentation Map
- [Architecture](./docs/architecture.md) — runtime, domains, queue, transport, and Threaded Mode overview.
- [Public API](./docs/public-api.md) — package entrypoints and stable companion-extension contracts.
- [Telegram Delivery API](./docs/delivery.md) — target-aware operational views, logical message handles, and lifecycle-safe transport.
- [Telegram Activity API](./docs/activity.md) — normalized lifecycle events, source identity, non-blocking delivery contexts, and consumer policy examples.
- [Inbound Handlers](./docs/inbound.md) — Telegram-to-Pi preprocessing pipelines.
- [Outbound Handlers](./docs/outbound.md) — final text/voice/file transformation and delivery.
- [Voice Integration](./docs/voice.md) — STT/TTS provider model and reply policies.
- [Extension Sections](./docs/sections.md) — Telegram-native companion UI surfaces.
- [Updates](./docs/updates.md) — update handler registry and callback interop.
- [Multi-Instance Bus](./docs/multi-instance-bus.md) — leader/follower routing in Threaded Mode.
- [Locks](./docs/locks.md) — singleton ownership and shared lock conventions.
- [UI Style](./docs/ui-style.md) — menu, emoji, labels, dialogs, and inline keyboard standards.
- [Callback Namespaces](./docs/callback-namespaces.md) — callback ownership and routing.
- [Command Templates](./docs/command-templates.md) — handler command-template conventions.
The docs index lives at [docs/README.md](./docs/README.md).
## Development
```bash ```bash
npm run typecheck npm ci
npm test npm run validate # typecheck + test + audit + pack:check
npm run audit
npm run pack:check
``` ```
Full validation: Engineering conventions are in [`AGENTS.md`](AGENTS.md).
```bash ## License
npm run validate
```
Project context: MIT, see [`LICENSE`](LICENSE): copyright Evandro Fonseca Junior (evandrodevbr), llblab and Mario Zechner (badlogic). Built on [llblab/pi-telegram](https://github.com/llblab/pi-telegram), itself a fork of [badlogic/pi-telegram](https://github.com/badlogic/pi-telegram); runs inside [Oh My Pi](https://github.com/can1357/oh-my-pi) and talks to the Telegram Bot API. Unofficial project, not affiliated with Telegram or the Oh My Pi maintainers.
- [AGENTS.md](./AGENTS.md) — engineering and runtime conventions. ---
- [BACKLOG.md](./BACKLOG.md) — release-relevant open work.
- [CHANGELOG.md](./CHANGELOG.md) — completed delivery history. <div align="center">
<sub>Made by <a href="https://evandro.dev.br">Evandro Fonseca Junior</a> · <a href="https://git.evandro.dev.br/evandro">git.evandro.dev.br</a></sub>
</div>
+32
View File
@@ -0,0 +1,32 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="420" viewBox="0 0 1200 420" role="img" aria-labelledby="title desc">
<title id="title">oh-my-pi-telegram</title>
<desc id="desc">oh-my-pi-telegram: drive a live omp session from a private Telegram chat. On the right, a simplified Telegram chat shows the /start command, the bot menu with /compact, /next and /stop, and a prompt sent from the phone.</desc>
<rect width="1200" height="420" rx="24" fill="#17212b"/>
<g id="title-block" font-family="system-ui, -apple-system, &quot;Segoe UI&quot;, Roboto, Helvetica, Arial, sans-serif">
<text x="64" y="156" font-size="64" font-weight="700" fill="#f5f5f5">oh-my-pi-telegram</text>
<text x="64" y="214" font-size="28" fill="#f5f5f5">Drive a live omp session from</text>
<text x="64" y="252" font-size="28" fill="#f5f5f5">a private Telegram chat.</text>
<text x="64" y="326" font-size="20" fill="#8a9bad" font-family="ui-monospace, &quot;SF Mono&quot;, Consolas, monospace">TypeScript · Node ≥22.19 · omp ≥16.5.2</text>
</g>
<g id="chat" font-family="system-ui, -apple-system, &quot;Segoe UI&quot;, Roboto, Helvetica, Arial, sans-serif" font-size="20">
<rect x="712" y="44" width="424" height="332" rx="20" fill="#1e2c3a"/>
<rect x="980" y="68" width="132" height="44" rx="14" fill="#2e557c"/>
<text x="1000" y="97" fill="#f5f5f5">/start</text>
<path d="M1074 92 L1079 97 L1088 87 M1082 97 L1091 87" fill="none" stroke="#70baf5" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
<rect x="736" y="132" width="376" height="144" rx="14" fill="#182533"/>
<text x="756" y="174" fill="#70baf5">/compact</text>
<text x="860" y="174" fill="#f5f5f5">Compact current session</text>
<text x="756" y="214" fill="#70baf5">/next</text>
<text x="860" y="214" fill="#f5f5f5">Force next turn</text>
<text x="756" y="254" fill="#70baf5">/stop</text>
<text x="860" y="254" fill="#f5f5f5">Abort Pi &amp; Clear queue</text>
<rect x="928" y="296" width="184" height="44" rx="14" fill="#2e557c"/>
<text x="948" y="325" fill="#f5f5f5">run the tests</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

@@ -1,404 +0,0 @@
# Telegram OMP 17 Status Indicator Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Show the Telegram bridge's connection state in OMP 17's upper status bar immediately after the cost segment, while documenting the correct OMP plugin workflow.
**Architecture:** OMP gains one narrow public extension UI method for a sanitized status segment fixed after `cost`. The Telegram plugin calls that method from its existing status update path, keeping connection-state derivation local and network-free. A checkout of OMP is modified and built; the Bun-global package is never patched in place.
**Tech Stack:** TypeScript, Bun, OMP 17 status-line component, OMP extension API, Node test runner, Telegram bridge extension.
---
## File structure
### OMP source checkout: `D:/Documents/Github/oh-my-pi`
- `packages/coding-agent/src/extensibility/extensions/types.ts` — public extension UI type.
- `packages/coding-agent/src/modes/controllers/extension-ui-controller.ts` — forwards the public API to the status-line component.
- `packages/coding-agent/src/modes/components/status-line/component.ts` — owns segment storage, sanitization, insertion after `cost`, and width eviction.
- `packages/coding-agent/src/modes/components/status-line/component.test.ts` — verifies public-status segment rendering.
### Telegram plugin: `D:/Documents/Github/oh-my-pi-telegram`
- `lib/pi.ts` — narrow structural adapter for OMP's optional public top-status API.
- `lib/status.ts` — derives the two Telegram status labels and publishes through injected ports.
- `index.ts` — wires the OMP status-segment port into the existing status runtime.
- `tests/status.test.ts` — verifies connection-state labels and publication calls.
- `tests/public-api.test.ts` — raises the declared OMP compatibility floor to 17.0.0.
- `package.json` — raises `omp.compatibleWith["@oh-my-pi/pi-coding-agent"]` to `>=17.0.0`.
- `README.md` — correct OMP plugin install/link/setup instructions.
## Task 1: Create a durable OMP 17 source checkout
**Files:**
- Create: `D:/Documents/Github/oh-my-pi`
- [ ] **Step 1: Clone and install the OMP 17 source tree**
```powershell
git clone https://github.com/can1357/oh-my-pi.git D:/Documents/Github/oh-my-pi
cd D:/Documents/Github/oh-my-pi
git switch --create feature/telegram-status-segment
bun install
```
- [ ] **Step 2: Confirm the checkout matches the installed OMP major version**
Run:
```powershell
bun --cwd packages/coding-agent -e "console.log(require('./package.json').version)"
```
Expected: a `17.x.x` version. If the upstream checkout is newer, switch to the `v17.0.0` tag before editing so the source API matches the installed OMP.
- [ ] **Step 3: Commit no code in this task**
The checkout is preparation only. The first code commit is made after the API and its test pass together.
## Task 2: Add OMP's public after-cost extension segment API
**Files:**
- Modify: `D:/Documents/Github/oh-my-pi/packages/coding-agent/src/extensibility/extensions/types.ts`
- Modify: `D:/Documents/Github/oh-my-pi/packages/coding-agent/src/modes/controllers/extension-ui-controller.ts`
- Modify: `D:/Documents/Github/oh-my-pi/packages/coding-agent/src/modes/components/status-line/component.ts`
- Test: `D:/Documents/Github/oh-my-pi/packages/coding-agent/src/modes/components/status-line/component.test.ts`
- [ ] **Step 1: Write failing status-line component tests**
Add tests that construct `StatusLineComponent`, register two extension segments, and inspect `getTopBorder(200).content` after stripping SGR codes. The assertions must prove:
```ts
expect(stripped.indexOf("$2.50")).toBeLessThan(stripped.indexOf("Telegram: conectado"));
expect(stripped.indexOf("Telegram: conectado")).toBeLessThan(stripped.indexOf("session-name"));
expect(stripped).not.toContain("\\x1b[31m");
```
Set the test session's usage statistics to `cost: 2.5` and its session name to `session-name`. Register a segment with `"Telegram: conectado"`; register another containing `"\x1b[31munsafe\x1b[0m"`; then clear the first segment and assert that only the cleared value disappears.
- [ ] **Step 2: Run the component test and observe the missing API failure**
Run:
```powershell
bun test packages/coding-agent/src/modes/components/status-line/component.test.ts
```
Expected: FAIL because `StatusLineComponent` has no extension-status-segment method.
- [ ] **Step 3: Define the public UI type**
In `ExtensionUIContext`, add the narrow method and option type:
```ts
export type ExtensionStatusSegmentPlacement = "after-cost";
export interface ExtensionStatusSegmentOptions {
placement: ExtensionStatusSegmentPlacement;
}
setStatusSegment(
key: string,
text: string | undefined,
options: ExtensionStatusSegmentOptions,
): void;
```
Only `"after-cost"` is accepted. Do not expose component references, arbitrary segment IDs, callbacks, or styling controls.
- [ ] **Step 4: Forward the API through the interactive extension UI controller**
Add to `uiContext` in `ExtensionUiController.initHooksAndCustomTools()`:
```ts
setStatusSegment: (key, text, options) => {
if (options.placement !== "after-cost") return;
this.ctx.statusLine.setExtensionStatusSegment(key, text);
this.ctx.ui.requestRender();
},
```
Add no-op implementations for non-interactive mock UI contexts in `extensibility/utils.ts`, `extensibility/extensions/runner.ts`, `modes/acp/acp-agent.ts`, and `session/agent-session.ts`, matching the existing `setStatus` convention.
- [ ] **Step 5: Store and render sanitized extension segments in the status-line component**
Add a private `Map<string, string>` and method:
```ts
setExtensionStatusSegment(key: string, text: string | undefined): void {
if (text === undefined) this.#extensionStatusSegments.delete(key);
else this.#extensionStatusSegments.set(key, sanitizeStatusText(text));
}
```
In `#buildStatusLine`, retain IDs for both rendered groups. After collecting built-in segments, insert the map values sorted by key immediately after the rendered `cost` entry in whichever group contains it. If `cost` is not visible, omit extension segments; their contract is explicitly after visible cost. Include synthetic IDs in the width bookkeeping so the normal right-to-left overflow policy can evict them without breaking `leftSegIds` alignment.
- [ ] **Step 6: Run the focused test to verify behavior**
Run:
```powershell
bun test packages/coding-agent/src/modes/components/status-line/component.test.ts
```
Expected: PASS. It proves deterministic ordering, sanitization, and clearing.
- [ ] **Step 7: Type-check and commit the OMP API**
Run:
```powershell
bun --cwd packages/coding-agent run check:types
git diff --check
git add packages/coding-agent/src/extensibility/extensions/types.ts packages/coding-agent/src/extensibility/utils.ts packages/coding-agent/src/extensibility/extensions/runner.ts packages/coding-agent/src/modes/acp/acp-agent.ts packages/coding-agent/src/modes/components/status-line/component.ts packages/coding-agent/src/modes/components/status-line/component.test.ts packages/coding-agent/src/modes/controllers/extension-ui-controller.ts packages/coding-agent/src/session/agent-session.ts
git commit -m "feat: expose after-cost extension status segments"
```
Expected: type-check exits 0 and the commit contains only the API, wiring, component, and test.
## Task 3: Publish Telegram bridge state through the new API
**Files:**
- Modify: `D:/Documents/Github/oh-my-pi-telegram/lib/pi.ts`
- Modify: `D:/Documents/Github/oh-my-pi-telegram/lib/status.ts`
- Modify: `D:/Documents/Github/oh-my-pi-telegram/index.ts`
- Test: `D:/Documents/Github/oh-my-pi-telegram/tests/status.test.ts`
- [ ] **Step 1: Write failing plugin status tests**
Add pure tests for a new helper with these observable contracts:
```ts
assert.equal(getTelegramTopStatusSegment({ pollingActive: true }), "Telegram: conectado");
assert.equal(getTelegramTopStatusSegment({ pollingActive: false }), "Telegram: desconectado");
```
Add an injected `setStatusSegment` spy and call `runtime.updateStatus(ctx)` with a context whose `ui.setStatus` is a no-op. Assert that the captured publication is:
```ts
["telegram", "Telegram: conectado", { placement: "after-cost" }]
```
when polling becomes active, and the disconnected text when it becomes inactive.
- [ ] **Step 2: Run the focused status test and observe the missing helper failure**
Run:
```powershell
node --experimental-strip-types --test tests/status.test.ts
```
Expected: FAIL because `getTelegramTopStatusSegment` is not exported.
- [ ] **Step 3: Add a narrow OMP context capability adapter**
In `lib/pi.ts`, define structural ports without importing OMP internal modules:
```ts
export interface PiTopStatusSegmentPorts {
setStatusSegment?: (
key: string,
text: string | undefined,
options: { placement: "after-cost" },
) => void;
}
export function setExtensionContextTopStatusSegment(
ctx: ExtensionContext,
key: string,
text: string,
): void {
const ui = ctx.ui as ExtensionContext["ui"] & PiTopStatusSegmentPorts;
ui.setStatusSegment?.(key, text, { placement: "after-cost" });
}
```
The optional capability keeps passive modes and stale local SDK declarations from throwing. The package compatibility floor still makes OMP 17 the supported runtime.
- [ ] **Step 4: Publish the two-state bridge label in `lib/status.ts`**
Add:
```ts
export function getTelegramTopStatusSegment(state: Pick<TelegramStatusBarState, "pollingActive">): string {
return state.pollingActive ? "Telegram: conectado" : "Telegram: desconectado";
}
```
Extend `TelegramStatusRuntimeDeps` with optional:
```ts
setStatusSegment?: (ctx: TContext, key: string, text: string) => void;
```
In `updateStatus`, compute the bridge state once, retain the existing footer `setStatus` call, and then call:
```ts
deps.setStatusSegment?.(ctx, "telegram", getTelegramTopStatusSegment(state));
```
The status runtime is already called from connect, disconnect, polling, lock, and session updates, so no additional event listeners are added.
- [ ] **Step 5: Wire the context adapter from `index.ts`**
Pass `Pi.setExtensionContextTopStatusSegment` as `setStatusSegment` into `createTelegramBridgeStatusRuntime`. Do not call the OMP API directly outside the `lib/pi.ts` adapter boundary.
- [ ] **Step 6: Run focused tests and type-check**
Run:
```powershell
node --experimental-strip-types --test tests/status.test.ts
npm run typecheck
```
Expected: all focused status tests pass and TypeScript exits 0.
- [ ] **Step 7: Commit the plugin integration**
```powershell
git diff --check
git add lib/pi.ts lib/status.ts index.ts tests/status.test.ts
git commit -m "feat: publish Telegram bridge status in OMP"
```
## Task 4: Correct OMP installation and secure setup documentation
**Files:**
- Modify: `D:/Documents/Github/oh-my-pi-telegram/README.md`
- Modify: `D:/Documents/Github/oh-my-pi-telegram/package.json`
- Modify: `D:/Documents/Github/oh-my-pi-telegram/tests/public-api.test.ts`
- [ ] **Step 1: Write a failing manifest test for the OMP 17 floor**
Change the existing test expectation to:
```ts
assert.equal(
packageJson.omp?.compatibleWith?.["@oh-my-pi/pi-coding-agent"],
">=17.0.0",
);
```
- [ ] **Step 2: Run the manifest test and observe the floor mismatch**
Run:
```powershell
node --experimental-strip-types --test tests/public-api.test.ts
```
Expected: FAIL because the manifest still declares `>=16.5.2`.
- [ ] **Step 3: Update the manifest and README**
Set the OMP compatibility floor in `package.json` to `>=17.0.0`.
Replace the incorrect OMP install block with:
```bash
omp plugin install npm:@evandrodevbr/oh-my-pi-telegram
omp plugin install git:github.com/evandrodevbr/oh-my-pi-telegram
omp plugin link /path/to/oh-my-pi-telegram
```
Document restarting OMP after installation/linking. Keep bot-token setup limited to the interactive commands:
```text
/telegram-setup
/telegram-connect
/telegram-disconnect
/telegram-status
```
Explicitly state that bot tokens must not be committed or pasted into chat.
- [ ] **Step 4: Run documentation-adjacent verification**
Run:
```powershell
node --experimental-strip-types --test tests/public-api.test.ts
npm run typecheck
npm run audit
npm run pack:check
```
Expected: all commands exit 0.
- [ ] **Step 5: Commit documentation and compatibility metadata**
```powershell
git diff --check
git add README.md package.json tests/public-api.test.ts
git commit -m "docs: document OMP 17 Telegram setup"
```
## Task 5: Build, install, configure, and smoke-test the integrated runtime
**Files:**
- Modify: no source files expected.
- [ ] **Step 1: Build and link the versioned OMP checkout**
From `D:/Documents/Github/oh-my-pi`:
```powershell
bun --cwd packages/coding-agent run check:types
bun --cwd packages/coding-agent run build
bun --cwd packages/coding-agent link
omp --version
```
Expected: the linked `omp` reports the built 17.x version. Do not edit or link files under the Bun global package directory directly.
- [ ] **Step 2: Link the updated Telegram plugin**
From `D:/Documents/Github/oh-my-pi-telegram`:
```powershell
omp plugin link .
omp plugin doctor
```
Expected: the doctor lists `@evandrodevbr/oh-my-pi-telegram` with zero errors.
- [ ] **Step 3: Configure the replacement token locally**
Start `omp` in the user's own terminal and run:
```text
/telegram-setup
```
Paste the new bot token only into OMP's local prompt. Then run:
```text
/telegram-connect
```
Expected: polling starts and the upper status bar contains `Telegram: conectado` after `$… (sub)`.
- [ ] **Step 4: Exercise disconnect and command behavior**
In the same OMP session, run:
```text
/telegram-status
/telegram-disconnect
```
Expected: `/telegram-status` returns bridge diagnostics and the upper status bar changes to `Telegram: desconectado` after cost.
- [ ] **Step 5: Commit and push both repositories**
From each repository after its focused tests pass:
```powershell
git status --short
git push origin HEAD
```
Expected: both commits are on their respective remote branches. Do not push a bot token, generated build output, Bun-global installation files, or `.env` files.
-104
View File
@@ -1,104 +0,0 @@
/**
* Telegram operator notices
* Zones: telegram ui, tui
* Owns the one-off host toasts (owner paired, connection recovered, reply not delivered) and their throttling
*/
import {
sanitizeTelegramStatusText,
summarizeTelegramStatusError,
} from "./status.ts";
export type TelegramNoticeLevel = "info" | "warning" | "error";
/** A failed delivery raises at most one notice per this window. */
export const TELEGRAM_DELIVERY_FAILED_NOTICE_COOLDOWN_MS = 30_000;
export interface TelegramNoticeRuntimeDeps<TContext> {
/** Current session context, when the bridge has one. */
getContext: () => TContext | undefined;
notify: (ctx: TContext, message: string, level: TelegramNoticeLevel) => void;
recordRuntimeEvent: (
category: string,
error: unknown,
details?: Record<string, unknown>,
) => void;
now?: () => number;
}
export interface TelegramNoticeRuntime<TContext> {
/** The first Telegram user was accepted as the bridge owner. */
paired: (userId: number, ctx: TContext) => void;
/** Polling works again after at least one failed attempt. */
recovered: (ctx: TContext) => void;
/** A message could not be delivered; throttled, uses the current context. */
deliveryFailed: (error: unknown) => void;
}
function describeTelegramDeliveryFailure(error: unknown): {
message: string;
level: TelegramNoticeLevel;
} {
const message = error instanceof Error ? error.message : String(error);
const summary = summarizeTelegramStatusError(message);
if (summary?.deliveryUnknown) {
return {
message: "Telegram: reply delivery unconfirmed (check the chat)",
level: "warning",
};
}
if (summary) {
return {
message: `Telegram: reply not delivered (${summary.label})`,
level: summary.kind === "fatal" ? "error" : "warning",
};
}
const raw = sanitizeTelegramStatusText(message);
const reason = raw.length > 80 ? `${raw.slice(0, 79)}…` : raw;
return { message: `Telegram: reply not delivered (${reason})`, level: "error" };
}
/**
* Toasts never propagate failures into the path that raised them: a stale
* session context only produces a recorded runtime event.
*/
export function createTelegramNoticeRuntime<TContext>(
deps: TelegramNoticeRuntimeDeps<TContext>,
): TelegramNoticeRuntime<TContext> {
const now = deps.now ?? Date.now;
let lastDeliveryNoticeAtMs: number | undefined;
const show = (
ctx: TContext,
message: string,
level: TelegramNoticeLevel,
): void => {
try {
deps.notify(ctx, message, level);
} catch (error) {
deps.recordRuntimeEvent("status", error, { phase: "notice" });
}
};
return {
paired(userId, ctx) {
show(ctx, `Telegram paired with user ${userId}`, "info");
},
recovered(ctx) {
show(ctx, "Telegram reconnected", "info");
},
deliveryFailed(error) {
const ctx = deps.getContext();
if (ctx === undefined) return;
const nowMs = now();
if (
lastDeliveryNoticeAtMs !== undefined &&
nowMs - lastDeliveryNoticeAtMs <
TELEGRAM_DELIVERY_FAILED_NOTICE_COOLDOWN_MS
) {
return;
}
lastDeliveryNoticeAtMs = nowMs;
const failure = describeTelegramDeliveryFailure(error);
show(ctx, failure.message, failure.level);
},
};
}
-134
View File
@@ -1,134 +0,0 @@
/**
* Telegram bridge traffic counters
* Zones: telegram transport, status
* Owns in-memory received/sent message counts and last-activity time for the status bar
*
* This domain is pure: it counts, it does not read config, call Telegram, or import other domains.
*/
export interface TelegramTrafficSnapshot {
/** Authorized messages received from the paired owner since the bridge loaded. */
received: number;
/** Messages the bridge delivered to Telegram since the bridge loaded. */
sent: number;
/** Messages that could not be delivered after the transport retries. */
failed: number;
/** Time of the latest received or sent message, when there was one. */
lastAtMs?: number;
}
export interface TelegramTrafficCounters {
recordReceived: () => void;
recordSent: () => void;
recordFailed: () => void;
snapshot: () => TelegramTrafficSnapshot;
}
/**
* Bot API methods that deliver a message the owner can read. Typing actions,
* drafts, edits, reactions and callback answers are not messages.
*/
const TELEGRAM_OUTBOUND_MESSAGE_METHODS: Readonly<Record<string, true>> = {
sendMessage: true,
sendRichMessage: true,
sendPhoto: true,
sendDocument: true,
sendAudio: true,
sendVideo: true,
sendVoice: true,
sendAnimation: true,
sendVideoNote: true,
sendMediaGroup: true,
sendSticker: true,
};
export function isTelegramOutboundMessageMethod(method: string): boolean {
return Object.hasOwn(TELEGRAM_OUTBOUND_MESSAGE_METHODS, method);
}
export function createTelegramTrafficCounters(
now: () => number = Date.now,
): TelegramTrafficCounters {
let received = 0;
let sent = 0;
let failed = 0;
let lastAtMs: number | undefined;
return {
recordReceived() {
received += 1;
lastAtMs = now();
},
recordSent() {
sent += 1;
lastAtMs = now();
},
recordFailed() {
failed += 1;
},
snapshot: () => ({
received,
sent,
failed,
...(lastAtMs === undefined ? {} : { lastAtMs }),
}),
};
}
export interface TelegramTrafficStatusBridgeDeps<TContext> {
counters: TelegramTrafficCounters;
/** Current session context, when the bridge has one. */
getContext: () => TContext | undefined;
updateStatus: (ctx: TContext) => void;
recordRuntimeEvent: (
category: string,
error: unknown,
details?: Record<string, unknown>,
) => void;
/** Told about every message delivery that failed, e.g. to raise a notice. */
onDeliveryFailed?: (error: unknown) => void;
}
export interface TelegramTrafficStatusBridge<TContext> {
/** Observer for successful Bot API calls; counts delivered messages. */
onCallSucceeded: (method: string) => void;
/** Observer for failed Bot API calls; counts undelivered messages. */
onCallFailed: (method: string, error: unknown) => void;
/** Observer for messages accepted from the paired owner. */
onAuthorizedMessage: (ctx: TContext) => void;
}
/**
* Counts traffic and refreshes the status bar so the counters stay current. A
* status refresh failure (for example a stale session context) is recorded and
* never propagates into the send or receive path that triggered it.
*/
export function createTelegramTrafficStatusBridge<TContext>(
deps: TelegramTrafficStatusBridgeDeps<TContext>,
): TelegramTrafficStatusBridge<TContext> {
const refreshStatus = (ctx: TContext): void => {
try {
deps.updateStatus(ctx);
} catch (error) {
deps.recordRuntimeEvent("status", error, { phase: "traffic-refresh" });
}
};
return {
onCallSucceeded(method) {
if (!isTelegramOutboundMessageMethod(method)) return;
deps.counters.recordSent();
const ctx = deps.getContext();
if (ctx !== undefined) refreshStatus(ctx);
},
onCallFailed(method, error) {
if (!isTelegramOutboundMessageMethod(method)) return;
deps.counters.recordFailed();
deps.onDeliveryFailed?.(error);
const ctx = deps.getContext();
if (ctx !== undefined) refreshStatus(ctx);
},
onAuthorizedMessage(ctx) {
deps.counters.recordReceived();
refreshStatus(ctx);
},
};
}
-111
View File
@@ -1,111 +0,0 @@
/**
* Regression tests for the Telegram operator notices
* Covers the toast wording, severity, throttling and failure isolation.
*/
import assert from "node:assert/strict";
import test from "node:test";
import {
createTelegramNoticeRuntime,
TELEGRAM_DELIVERY_FAILED_NOTICE_COOLDOWN_MS,
} from "../lib/notices.ts";
function createNoticeHarness(options: { notifyThrows?: boolean } = {}) {
const notices: string[] = [];
const recorded: string[] = [];
let nowMs = 1_000_000;
let context: string | undefined = "session";
const runtime = createTelegramNoticeRuntime({
getContext: () => context,
notify: (ctx: string, message, level) => {
if (options.notifyThrows) throw new Error("stale ctx");
notices.push(`${level}|${ctx}|${message}`);
},
recordRuntimeEvent: (category, error, details) => {
recorded.push(`${category}:${(error as Error).message}:${details?.phase}`);
},
now: () => nowMs,
});
return {
runtime,
notices,
recorded,
advance: (ms: number) => {
nowMs += ms;
},
dropContext: () => {
context = undefined;
},
};
}
test("Notices announce pairing and recovery as info toasts", () => {
const { runtime, notices } = createNoticeHarness();
runtime.paired(777, "ctx-a");
runtime.recovered("ctx-b");
assert.deepEqual(notices, [
"info|ctx-a|Telegram paired with user 777",
"info|ctx-b|Telegram reconnected",
]);
});
test("Delivery failures explain the reason and pick a severity from it", () => {
const harness = createNoticeHarness();
harness.runtime.deliveryFailed(
new Error("Telegram API sendMessage failed: HTTP 429: Too Many Requests: retry after 12"),
);
harness.advance(TELEGRAM_DELIVERY_FAILED_NOTICE_COOLDOWN_MS);
harness.runtime.deliveryFailed(new Error("Unable to connect. Is the computer able to access the url?"));
harness.advance(TELEGRAM_DELIVERY_FAILED_NOTICE_COOLDOWN_MS);
harness.runtime.deliveryFailed(new Error("Telegram API sendMessage failed: HTTP 401: Unauthorized"));
harness.advance(TELEGRAM_DELIVERY_FAILED_NOTICE_COOLDOWN_MS);
harness.runtime.deliveryFailed(new Error("message is too long"));
harness.advance(TELEGRAM_DELIVERY_FAILED_NOTICE_COOLDOWN_MS);
harness.runtime.deliveryFailed(
new Error("Telegram API sendRichMessage may have committed before transport failed."),
);
assert.deepEqual(harness.notices, [
"warning|session|Telegram: reply not delivered (rate limited 12s)",
"warning|session|Telegram: reply not delivered (offline)",
"error|session|Telegram: reply not delivered (invalid token)",
"error|session|Telegram: reply not delivered (message is too long)",
"warning|session|Telegram: reply delivery unconfirmed (check the chat)",
]);
});
test("Delivery failure notices are throttled and need a live context", () => {
const harness = createNoticeHarness();
harness.runtime.deliveryFailed(new Error("fetch failed"));
harness.advance(TELEGRAM_DELIVERY_FAILED_NOTICE_COOLDOWN_MS - 1);
harness.runtime.deliveryFailed(new Error("fetch failed"));
assert.equal(harness.notices.length, 1);
harness.advance(1);
harness.runtime.deliveryFailed(new Error("fetch failed"));
assert.equal(harness.notices.length, 2);
harness.advance(TELEGRAM_DELIVERY_FAILED_NOTICE_COOLDOWN_MS);
harness.dropContext();
harness.runtime.deliveryFailed(new Error("fetch failed"));
assert.equal(harness.notices.length, 2);
});
test("A failing toast is recorded and never reaches the caller", () => {
const harness = createNoticeHarness({ notifyThrows: true });
assert.doesNotThrow(() => {
harness.runtime.paired(777, "ctx");
harness.runtime.recovered("ctx");
harness.runtime.deliveryFailed(new Error("fetch failed"));
});
assert.deepEqual(harness.recorded, [
"status:stale ctx:notice",
"status:stale ctx:notice",
"status:stale ctx:notice",
]);
});
test("Delivery failure notices carry no control sequences from raw error text", () => {
const harness = createNoticeHarness();
harness.runtime.deliveryFailed(new Error("bad\u001b]0;pwned\u0007\u001b[31m text"));
assert.equal(harness.notices.length, 1);
assert.doesNotMatch(harness.notices[0], /[\u0000-\u001f\u007f-\u009f]/);
});
-163
View File
@@ -1,163 +0,0 @@
/**
* Regression tests for the Telegram traffic counters
* Covers what counts as a message, last-activity tracking, failed deliveries, and the /telegram-status line.
*/
import assert from "node:assert/strict";
import test from "node:test";
import { buildTelegramBridgeStatusLines } from "../lib/status.ts";
import {
createTelegramTrafficCounters,
createTelegramTrafficStatusBridge,
isTelegramOutboundMessageMethod,
} from "../lib/traffic.ts";
test("Traffic counters count every direction and remember the latest message", () => {
let nowMs = 1_000;
const counters = createTelegramTrafficCounters(() => nowMs);
assert.deepEqual(counters.snapshot(), { received: 0, sent: 0, failed: 0 });
counters.recordReceived();
nowMs = 2_000;
counters.recordSent();
counters.recordSent();
nowMs = 3_000;
counters.recordFailed();
assert.deepEqual(counters.snapshot(), {
received: 1,
sent: 2,
failed: 1,
lastAtMs: 2_000,
});
});
test("Only calls that deliver a readable message count as sent", () => {
for (const method of ["sendMessage", "sendRichMessage", "sendPhoto", "sendDocument", "sendVoice"]) {
assert.equal(isTelegramOutboundMessageMethod(method), true, method);
}
for (const method of [
"sendChatAction",
"sendMessageDraft",
"editMessageText",
"setMessageReaction",
"answerCallbackQuery",
"getUpdates",
"toString",
]) {
assert.equal(isTelegramOutboundMessageMethod(method), false, method);
}
});
test("Status lines report the message counters and the time of the last one", () => {
const baseState = {
botUsername: "demo_bot",
allowedUserId: 7,
pollingActive: true,
pendingDispatch: false,
compactionInProgress: false,
activeToolExecutions: 0,
pendingModelSwitch: false,
queuedItems: [],
recentRuntimeEvents: [],
};
const lastAtMs = new Date(2026, 9, 1, 14, 32).getTime();
const clean = buildTelegramBridgeStatusLines({
...baseState,
traffic: { received: 12, sent: 11, failed: 0, lastAtMs },
});
assert.ok(
clean.includes("- messages: 12 received, 11 sent, last 14:32"),
clean.join("\n"),
);
const withFailures = buildTelegramBridgeStatusLines({
...baseState,
traffic: { received: 12, sent: 11, failed: 2, lastAtMs },
});
assert.ok(
withFailures.includes("- messages: 12 received, 11 sent, 2 failed, last 14:32"),
withFailures.join("\n"),
);
const withoutTraffic = buildTelegramBridgeStatusLines(baseState);
assert.equal(withoutTraffic.some((line) => line.startsWith("- messages:")), false);
});
test("Traffic bridge counts delivered messages and refreshes the status with the current context", () => {
const counters = createTelegramTrafficCounters();
const refreshed: string[] = [];
let context: string | undefined = "session-a";
const bridge = createTelegramTrafficStatusBridge({
counters,
getContext: () => context,
updateStatus: (ctx: string) => {
refreshed.push(ctx);
},
recordRuntimeEvent: () => {},
});
bridge.onCallSucceeded("sendChatAction");
bridge.onCallSucceeded("editMessageText");
assert.deepEqual(counters.snapshot(), { received: 0, sent: 0, failed: 0 });
assert.deepEqual(refreshed, []);
bridge.onCallSucceeded("sendRichMessage");
assert.equal(counters.snapshot().sent, 1);
assert.deepEqual(refreshed, ["session-a"]);
context = undefined;
bridge.onCallSucceeded("sendMessage");
assert.equal(counters.snapshot().sent, 2);
assert.deepEqual(refreshed, ["session-a"]);
bridge.onAuthorizedMessage("session-b");
assert.equal(counters.snapshot().received, 1);
assert.deepEqual(refreshed, ["session-a", "session-b"]);
});
test("Traffic bridge counts undelivered messages, reports them and refreshes the status", () => {
const counters = createTelegramTrafficCounters();
const refreshed: string[] = [];
const failures: string[] = [];
const bridge = createTelegramTrafficStatusBridge({
counters,
getContext: () => "session",
updateStatus: (ctx: string) => {
refreshed.push(ctx);
},
recordRuntimeEvent: () => {},
onDeliveryFailed: (error) => {
failures.push((error as Error).message);
},
});
bridge.onCallFailed("getUpdates", new Error("poll failed"));
bridge.onCallFailed("sendChatAction", new Error("typing failed"));
assert.equal(counters.snapshot().failed, 0);
assert.deepEqual(failures, []);
assert.deepEqual(refreshed, []);
bridge.onCallFailed("sendMessage", new Error("HTTP 500"));
assert.equal(counters.snapshot().failed, 1);
assert.equal(counters.snapshot().sent, 0);
assert.deepEqual(failures, ["HTTP 500"]);
assert.deepEqual(refreshed, ["session"]);
});
test("Traffic bridge records a failed status refresh instead of breaking the message path", () => {
const counters = createTelegramTrafficCounters();
const recorded: string[] = [];
const bridge = createTelegramTrafficStatusBridge({
counters,
getContext: () => "session",
updateStatus: () => {
throw new Error("stale ctx");
},
recordRuntimeEvent: (category, error, details) => {
recorded.push(`${category}:${(error as Error).message}:${details?.phase}`);
},
});
bridge.onCallSucceeded("sendMessage");
bridge.onAuthorizedMessage("session");
bridge.onCallFailed("sendMessage", new Error("HTTP 500"));
assert.equal(counters.snapshot().received, 1);
assert.equal(counters.snapshot().sent, 1);
assert.equal(counters.snapshot().failed, 1);
assert.deepEqual(recorded, [
"status:stale ctx:traffic-refresh",
"status:stale ctx:traffic-refresh",
"status:stale ctx:traffic-refresh",
]);
});