Podemos agregar a la extensión de telegram un comando "resume" que nos deje cambiar de sesión a una anterior.?


**Inspecting the repository**

I need to inspect the repository, possibly looking at something called pi-config. I should clarify my next steps, maybe by executing a direct command? It might help to outline a plan, so I think about using a to-do list to keep things organized. Also, I need to find the telegram extension; that's important for my process. Let's figure this all out one step at a time!

todo

Add Telegram /resume command to switch to prior session

Find Telegram bridge/session rotation flow, implement a /resume command to pick and switch to an older session, then validate behavior.


Created #1: Add Telegram /resume command to switch to prior session (pending)


bash

cd /home/sebas && rg -n "pi-telegram-new-session|/new|telegram" pi-config -g '!**/node_modules/**'

cd /home/sebas/pi-config && find . -maxdepth 3 \( -iname '*telegram*' -o -path '*telegram*' \) | sort


pi-config/settings.json:20:    "/mnt/rpi/dev/pi-telegram-sebas"
pi-config/SERVICES-ARCHITECTURE.md:297:- one fixed tmux session: `pi-telegram`
pi-config/SERVICES-ARCHITECTURE.md:302:- Telegram ownership stays on that one instance; use `/new` there when you want a fresh conversation
pi-config/SERVICES-ARCHITECTURE.md:306:- launcher loop: `/home/sebas/pi-config/bin/pi-telegram-service`
pi-config/SERVICES-ARCHITECTURE.md:307:- user service: `~/.config/systemd/user/pi-telegram.service`
pi-config/SERVICES-ARCHITECTURE.md:308:- tmux session name: `pi-telegram`
pi-config/SERVICES-ARCHITECTURE.md:309:- Telegram extension source pinned locally via settings package path: `/mnt/rpi/dev/pi-telegram-sebas`
pi-config/SERVICES-ARCHITECTURE.md:313:- first-time setup still needs one manual `/telegram-connect` inside that dedicated `pi` session
pi-config/SERVICES-ARCHITECTURE.md:314:- after that, normal conversation and `/new` stay inside the same long-lived instance
pi-config/SERVICES-ARCHITECTURE.md:315:- `/new` is provided by the local pinned `pi-telegram-sebas` fork, not upstream npm package state
pi-config/locks.json:2:  "@llblab/pi-telegram": {
pi-config/docs/DONE.md:58:  - repo-scripted install paths captured for pi-telegram, wifi-tracker-api, nginx kiosk, and shell/tmux helpers
pi-config/AGENTS.md:128:~/.agents/skills/telegram-notify/telegram-notify "Message"
pi-config/AGENTS.md:130:- If a Telegram user sends literal `/new`, do not treat it as normal chat. Acknowledge briefly and run `/home/sebas/pi-config/bin/pi-telegram-new-session` to rotate the tmux-backed Telegram Pi session.
pi-config/docs/service-manifest.md:51:## pi-telegram
pi-config/docs/service-manifest.md:54:  - `bin/pi-telegram-service`
pi-config/docs/service-manifest.md:55:  - `scripts/pi-telegram.service`
pi-config/docs/service-manifest.md:56:  - `scripts/install-pi-telegram-service.sh`
pi-config/docs/service-manifest.md:57:- install path: `pi-telegram.service` user service via repo installer
pi-config/docs/service-manifest.md:59:- health check: bridge receives/sends messages; `/new` rotation works
pi-config/docs/service-manifest.md:190:  - Pi extensions, task-phase tooling, and `pi-telegram.service` remain active
pi-config/scripts/pi-telegram.service:8:ExecStart=/home/sebas/pi-config/bin/pi-telegram-service
pi-config/scripts/pi-telegram.service:11:Environment=PI_TELEGRAM_SESSION_NAME=pi-telegram
pi-config/docs/BACKLOG.md:69:  - nice-to-haves later: full-text extraction, search, saved highlights, send-to-reader/share target, email/newsletter ingestion
pi-config/docs/bootstrap.md:90:- telegram service if desired
pi-config/docs/bootstrap.md:95:- `scripts/install-pi-telegram-service.sh`
pi-config/scripts/bootstrap-services.sh:23:echo "installing pi-telegram user service"
pi-config/scripts/bootstrap-services.sh:24:"$ROOT/scripts/install-pi-telegram-service.sh"
pi-config/scripts/session-memory-sweeper.py:23:TELEGRAM_NOTIFY_BIN = Path(os.environ.get("TELEGRAM_NOTIFY_BIN", str(Path.home() / ".agents" / "skills" / "telegram-notify" / "telegram-notify")))
pi-config/scripts/session-memory-sweeper.py:197:def maybe_send_telegram(message: str) -> None:
pi-config/scripts/session-memory-sweeper.py:283:        maybe_send_telegram("\n\n".join(notifications[:6]))
pi-config/scripts/install-pi-telegram-service.sh:5:UNIT_SRC="$ROOT/scripts/pi-telegram.service"
pi-config/scripts/install-pi-telegram-service.sh:6:UNIT_DST="$HOME/.config/systemd/user/pi-telegram.service"
pi-config/scripts/install-pi-telegram-service.sh:11:systemctl --user enable --now pi-telegram.service
pi-config/scripts/install-pi-telegram-service.sh:12:systemctl --user status --no-pager pi-telegram.service | sed -n '1,20p'


---
name: telegram-notify
description: Send Telegram notifications via bot
---

# telegram-notify

## Workflow

1. Ensure environment variables are set (or stored in `~/.agents/skills/telegram-notify/.env`):
   - `TELEGRAM_BOT_TOKEN`
   - `TELEGRAM_CHAT_ID`
   - fallback: `TELEGRAM_USER_ID` also works as chat id
2. Execute:
   - `~/.agents/skills/telegram-notify/telegram-notify "<message>"`
3. Confirm that the message was sent.

## Rules

- never print the token or chat id in outputs
- keep messages concise and action-oriented


./bin/pi-telegram-new-session
./bin/pi-telegram-service
./scripts/install-pi-telegram-service.sh
./scripts/pi-telegram.service
./telegram.json
./tmp/telegram


cd /mnt/rpi/dev/pi-telegram-sebas && rg -n "new session|/new|session|history|resume|callback|telegram" . -g '!**/node_modules/**'


#!/usr/bin/env bash
set -euo pipefail

SESSION_NAME="${PI_TELEGRAM_SESSION_NAME:-pi-telegram}"
TMUX_BIN="$(command -v tmux)"

nohup bash -lc "sleep 2; '$TMUX_BIN' kill-session -t '$SESSION_NAME' 2>/dev/null || true" >/tmp/pi-telegram-new-session.log 2>&1 &

echo "queued new Telegram Pi session: $SESSION_NAME"


#!/usr/bin/env bash
set -euo pipefail

SESSION_NAME="${PI_TELEGRAM_SESSION_NAME:-pi-telegram}"
PI_BIN="${PI_TELEGRAM_PI_BIN:-$HOME/.npm-global/bin/pi}"
CWD="${PI_TELEGRAM_CWD:-$HOME}"
AGENT_DIR="${PI_TELEGRAM_AGENT_DIR:-$HOME/pi-config}"
CHECK_INTERVAL="${PI_TELEGRAM_CHECK_INTERVAL_SECONDS:-2}"
TMUX_BIN="$(command -v tmux)"

if [[ ! -x "$PI_BIN" ]]; then
  echo "pi binary not executable: $PI_BIN" >&2
  exit 1
fi

start_session() {
  "$TMUX_BIN" has-session -t "$SESSION_NAME" 2>/dev/null && return 0
  "$TMUX_BIN" new-session -d -s "$SESSION_NAME" \
    "cd '$CWD' && env TERM=tmux-256color COLORTERM=truecolor TERM_PROGRAM= PI_CODING_AGENT_DIR='$AGENT_DIR' '$PI_BIN'"
}

start_session
while true; do
  start_session
  sleep "$CHECK_INTERVAL"
done


## Dedicated Telegram `pi` service

For an always-reachable Telegram agent, run one dedicated `pi` instance under a user systemd service and keep tmux as the PTY container.

Desired shape:

- one fixed tmux session: `pi-telegram`
- one fixed working directory: `/home/sebas`
- one fixed config dir: `/home/sebas/pi-config`
- service restarts on boot via user systemd
- service loop recreates the tmux session if `pi` exits
- Telegram ownership stays on that one instance; use `/new` there when you want a fresh conversation

### Runtime pieces

- launcher loop: `/home/sebas/pi-config/bin/pi-telegram-service`
- user service: `~/.config/systemd/user/pi-telegram.service`
- tmux session name: `pi-telegram`
- Telegram extension source pinned locally via settings package path: `/mnt/rpi/dev/pi-telegram-sebas`

### Behavior

- first-time setup still needs one manual `/telegram-connect` inside that dedicated `pi` session
- after that, normal conversation and `/new` stay inside the same long-lived instance
- `/new` is provided by the local pinned `pi-telegram-sebas` fork, not upstream npm package state
- after reboot, the service relaunches the same tmux-backed `pi` process from the same `cwd`, which matches the Telegram bridge auto-resume model

## Wi‑Fi tracker service

A lightweight Wi‑Fi/LAN presence tracker now runs on this machine and publishes a small dashboard through nginx.

Scope of this service:

- collect device presence visible from this Pi on the local LAN
- keep an append-only JSONL activity log
- maintain per-device aliases set from the web UI
- publish a derived summary for dashboard rendering
- expose a tiny local API for alias CRUD

[153 more lines in file. Use offset=330 to continue.]


./tests/outbound-handlers.test.ts:640:        id: "callback-1",
./tests/outbound-handlers.test.ts:660:        id: "callback-1",
./tests/outbound-handlers.test.ts:667:    { answer: "callback-1", text: "Queued." },
./tests/outbound-handlers.test.ts:671:test("Button callback handler consumes expired button callbacks", async () => {
./tests/outbound-handlers.test.ts:675:      { id: "callback-1", data: "tgbtn:missing" },
./tests/outbound-handlers.test.ts:690:    { id: "callback-1", text: "Button action expired." },
./tests/outbound-handlers.test.ts:711:      content: [{ type: "text", text: "[telegram] Continue now" }],
./tests/outbound-handlers.test.ts:712:      historyText: "Continue now",
./tests/outbound-handlers.test.ts:717:    inline_keyboard: [[{ text: "Continue", callback_data: "btn:1" }]],
./tests/replies.test.ts:84:        inline_keyboard: [[{ text: "ok", callback_data: "noop" }]],
./tests/replies.test.ts:96:        inline_keyboard: [[{ text: "ok", callback_data: "noop" }]],
./tests/replies.test.ts:141:        inline_keyboard: [[{ text: "ok", callback_data: "noop" }]],
./tests/replies.test.ts:161:        inline_keyboard: [[{ text: "ok", callback_data: "noop" }]],
./docs/outbound-handlers.md:3:`pi-telegram` maps hidden assistant-authored HTML comments to Telegram-native outbound actions.
./docs/outbound-handlers.md:5:This is intentionally prompt-driven: the agent writes normal Markdown plus small hidden top-level blocks, and the bridge performs the transport work after `agent_end`. `telegram_voice` and `telegram_button` are not π tools. Outbound behavior is an emergent result of the assistant prompt, configured command-template handlers, generated artifacts, and reply delivery. That avoids extra agent-side tool calls, avoids fragile parameter plumbing inside the conversation, and minimizes latency because text, voice, and buttons are planned in one standard assistant reply.
./docs/outbound-handlers.md:16:| `telegram_voice`  | `voice`      | Generate OGG/Opus and call `sendVoice`             |
./docs/outbound-handlers.md:17:| `telegram_button` | Built-in     | Attach an inline keyboard button to the final text |
./docs/outbound-handlers.md:25:This is ideal for machine translation, tone normalization, redaction, glossary expansion, compliance footers, or any other final text rewrite that should be configured outside the agent prompt. Text handlers run before Markdown/HTML rendering, so a Markdown reply remains Markdown input to the handler. They also run when the bridge finalizes an already streamed rich preview; in that path Telegram can briefly show a pre-transform preview before the final edited message is replaced with the handler output. Inline buttons are built as reply markup: visible button labels pass through the same text handler, while callback data and callback prompts remain unchanged.
./docs/outbound-handlers.md:57:`telegram.json` may define `outboundHandlers`:
./docs/outbound-handlers.md:75:In this example, the first step receives the `telegram_voice` text on stdin and returns translated text; the second step reads that translated text from stdin and writes `{mp3}`; the final step converts `{mp3}` to Telegram-ready `{ogg}`. If you do not need voice translation, omit the first step and call a TTS command that accepts `{text}` directly. If a matching voice handler fails, the bridge tries the next matching `type: "voice"` handler.
./docs/outbound-handlers.md:84:<!-- telegram_voice lang=ru rate=+30%
./docs/outbound-handlers.md:88:<!-- telegram_voice lang=ru rate=+30% text="Short spoken companion summary." -->
./docs/outbound-handlers.md:90:<!-- telegram_voice: Short spoken companion summary. -->
./docs/outbound-handlers.md:93:The bridge strips the comment from Telegram text. On `agent_end`, it maps each `telegram_voice` block to `type: "voice"`, generates one file per block, and sends each file as an independent Telegram-native voice message. The opening `<!-- telegram_voice` marker must start at column zero on a top-level line outside fenced code, quotes, and lists; otherwise it is rendered as literal Markdown. Body-form comments leave the opening line unclosed until the body-ending `-->`; closed heads can use `text="..."` for explicit one-line spoken text.
./docs/outbound-handlers.md:104:| `{mp3}`     | Flat temp artifact path under `~/.pi/agent/tmp/telegram` |
./docs/outbound-handlers.md:105:| `{ogg}`     | Flat temp artifact path under `~/.pi/agent/tmp/telegram` |
./docs/outbound-handlers.md:124:<!-- telegram_button label=Continue prompt="Continue with the current plan." -->
./docs/outbound-handlers.md:126:<!-- telegram_button label="Show risks"
./docs/outbound-handlers.md:130:<!-- telegram_button: Done -->
./docs/outbound-handlers.md:135:- `telegram_button: Label` creates one independent label-only button row whose prompt equals the label.
./docs/outbound-handlers.md:136:- `telegram_button label="Label" prompt="Prompt"` creates one independent button row whose prompt is the `prompt` attribute.
./docs/outbound-handlers.md:137:- `telegram_button label="Label"` with a body creates one independent button row whose prompt is the block body.
./docs/outbound-handlers.md:138:- The opening `<!-- telegram_button` marker must start at column zero on a top-level line outside fenced code, quotes, and lists; otherwise it is rendered as literal Markdown.
./docs/outbound-handlers.md:139:- Keep the canonical body form as `<!-- telegram_button label="Label"` + body + `-->`; closed heads must use `prompt="..."` or the colon shorthand to create a button.
./docs/outbound-handlers.md:141:- Button actions are stored in memory with short `callback_data`; Telegram never sees the full prompt in the button payload.
./docs/outbound-handlers.md:143:Buttons are built in and do not need a command template because they are pure Telegram reply markup plus callback routing.
./docs/outbound-handlers.md:150:- Add `telegram_voice` when a Telegram-native voice message is useful; use body text, `text="..."`, or colon shorthand for the text to synthesize. A companion summary is optional, no specific summary format is required.
./docs/outbound-handlers.md:151:- Add `telegram_button: ...` when label equals prompt, `telegram_button label="..." prompt="..."` for one-line prompts, or `telegram_button label="..."` with a body for multiline prompts. If the reply contains only button/voice comment blocks, add a short visible marker (for example `Choose one:`) before them so Telegram always has a visible parent message for attachment.
./tests/runtime.test.ts:27:      join(tmpdir(), "pi-telegram-runtime-agent-"),
./tests/runtime.test.ts:80:  const configPath = join(agentDir, "telegram.json");
./tests/runtime.test.ts:173:    sessionManager: {
./tests/runtime.test.ts:303:    telegramTurnDispatchPending: true,
./tests/runtime.test.ts:533:  const telegramConfig = await createRuntimeTelegramConfigFixture();
./tests/runtime.test.ts:564:              text: "hello from telegram",
./tests/runtime.test.ts:580:    await telegramConfig.write({ botToken: "123:abc", lastUpdateId: 0 });
./tests/runtime.test.ts:583:    await handlers.get("session_start")?.({}, ctx);
./tests/runtime.test.ts:584:    await commands.get("telegram-connect")?.handler("", ctx);
./tests/runtime.test.ts:592:    assert.match(promptBlock.text ?? "", /^\[telegram\] hello from telegram$/);
./tests/runtime.test.ts:593:    await handlers.get("session_shutdown")?.({}, ctx);
./tests/runtime.test.ts:596:    await telegramConfig.restore();
./tests/runtime.test.ts:601:  const telegramConfig = await createRuntimeTelegramConfigFixture();
./tests/runtime.test.ts:662:    await telegramConfig.write({
./tests/runtime.test.ts:669:    await handlers.get("session_start")?.({}, ctx);
./tests/runtime.test.ts:670:    await commands.get("telegram-connect")?.handler("", ctx);
./tests/runtime.test.ts:703:    await handlers.get("session_shutdown")?.({}, ctx);
./tests/runtime.test.ts:707:    await telegramConfig.restore();
./tests/runtime.test.ts:712:  const telegramConfig = await createRuntimeTelegramConfigFixture();
./tests/runtime.test.ts:765:    await telegramConfig.write({
./tests/runtime.test.ts:779:    await handlers.get("session_start")?.({}, idleCtx);
./tests/runtime.test.ts:780:    await commands.get("telegram-connect")?.handler("", idleCtx);
./tests/runtime.test.ts:845:    assert.equal(promptText, "[telegram] new request");
./tests/runtime.test.ts:851:    await handlers.get("session_shutdown")?.({}, idleCtx);
./tests/runtime.test.ts:854:    await telegramConfig.restore();
./tests/runtime.test.ts:859:  const telegramConfig = await createRuntimeTelegramConfigFixture();
./tests/runtime.test.ts:908:    await telegramConfig.write({
./tests/runtime.test.ts:916:      sessionManager: {
./tests/runtime.test.ts:935:    await handlers.get("session_start")?.({}, idleCtx);
./tests/runtime.test.ts:936:    await commands.get("telegram-connect")?.handler("", idleCtx);
./tests/runtime.test.ts:981:    assert.equal(runtimeEvents[0], "dispatch:[telegram] first request");
./tests/runtime.test.ts:985:      "dispatch:[telegram] follow up after status",
./tests/runtime.test.ts:988:    if (shutdownCtx) await handlers.get("session_shutdown")?.({}, shutdownCtx);
./tests/runtime.test.ts:990:    await telegramConfig.restore();
./tests/runtime.test.ts:995:  const telegramConfig = await createRuntimeTelegramConfigFixture();
./tests/runtime.test.ts:1047:    await telegramConfig.write({
./tests/runtime.test.ts:1056:      sessionManager: {
./tests/runtime.test.ts:1074:    await handlers.get("session_start")?.({}, idleCtx);
./tests/runtime.test.ts:1075:    await commands.get("telegram-connect")?.handler("", idleCtx);
./tests/runtime.test.ts:1120:    assert.equal(runtimeEvents[0], "dispatch:[telegram] first request");
./tests/runtime.test.ts:1122:    assert.equal(runtimeEvents[2], "dispatch:[telegram] follow up after model");
./tests/runtime.test.ts:1123:    await handlers.get("session_shutdown")?.({}, idleCtx);
./tests/runtime.test.ts:1126:    await telegramConfig.restore();
./tests/runtime.test.ts:1131:  const telegramConfig = await createRuntimeTelegramConfigFixture();
./tests/runtime.test.ts:1182:    await telegramConfig.write({
./tests/runtime.test.ts:1197:    await handlers.get("session_start")?.({}, ctx);
./tests/runtime.test.ts:1198:    await commands.get("telegram-connect")?.handler("", ctx);
./tests/runtime.test.ts:1218:        (event) => event === "dispatch:[telegram] follow up after compaction",
./tests/runtime.test.ts:1224:      runtimeEvents.includes("dispatch:[telegram] follow up after compaction"),
./tests/runtime.test.ts:1229:    await handlers.get("session_shutdown")?.({}, ctx);
./tests/runtime.test.ts:1232:    await telegramConfig.restore();
./tests/runtime.test.ts:1237:  const telegramConfig = await createRuntimeTelegramConfigFixture();
./tests/runtime.test.ts:1283:    await telegramConfig.write({
./tests/runtime.test.ts:1290:    await handlers.get("session_start")?.({}, ctx);
./tests/runtime.test.ts:1291:    await commands.get("telegram-connect")?.handler("", ctx);
./tests/runtime.test.ts:1297:      "dispatch:[telegram] first caption\n\nsecond caption",
./tests/runtime.test.ts:1299:    await handlers.get("session_shutdown")?.({}, ctx);
./tests/runtime.test.ts:1302:    await telegramConfig.restore();
./tests/runtime.test.ts:1307:  const telegramConfig = await createRuntimeTelegramConfigFixture();
./tests/runtime.test.ts:1351:    await telegramConfig.write({
./tests/runtime.test.ts:1358:    await handlers.get("session_start")?.({}, ctx);
./tests/runtime.test.ts:1359:    await commands.get("telegram-connect")?.handler("", ctx);
./tests/runtime.test.ts:1365:      `dispatch:[telegram] ${"x".repeat(3600)}\n\ntail`,
./tests/runtime.test.ts:1367:    await handlers.get("session_shutdown")?.({}, ctx);
./tests/runtime.test.ts:1370:    await telegramConfig.restore();
./tests/runtime.test.ts:1375:  const telegramConfig = await createRuntimeTelegramConfigFixture();
./tests/runtime.test.ts:1409:    await telegramConfig.write({
./tests/runtime.test.ts:1416:    await handlers.get("session_start")?.({}, ctx);
./tests/runtime.test.ts:1417:    await commands.get("telegram-connect")?.handler("", ctx);
./tests/runtime.test.ts:1419:    await handlers.get("session_shutdown")?.({}, ctx);
./tests/runtime.test.ts:1424:    await telegramConfig.restore();
./tests/runtime.test.ts:1429:  const telegramConfig = await createRuntimeTelegramConfigFixture();
./tests/runtime.test.ts:1476:    await telegramConfig.write({
./tests/runtime.test.ts:1486:    await handlers.get("session_start")?.({}, idleCtx);
./tests/runtime.test.ts:1487:    await commands.get("telegram-connect")?.handler("", idleCtx);
./tests/runtime.test.ts:1567:    assert.equal(runtimeEvents[0], "dispatch:[telegram] first request");
./tests/runtime.test.ts:1568:    assert.equal(runtimeEvents[1], "dispatch:[telegram] newer waiting");
./tests/runtime.test.ts:1583:      "dispatch:[telegram] first request",
./tests/runtime.test.ts:1584:      "dispatch:[telegram] newer waiting",
./tests/runtime.test.ts:1586:    await handlers.get("session_shutdown")?.({}, idleCtx);
./tests/runtime.test.ts:1589:    await telegramConfig.restore();
./tests/runtime.test.ts:1594:  const telegramConfig = await createRuntimeTelegramConfigFixture();
./tests/runtime.test.ts:1616:  const callbackAnswers: string[] = [];
./tests/runtime.test.ts:1650:      callbackAnswers.push(String(body?.text ?? ""));
./tests/runtime.test.ts:1663:    await telegramConfig.write({
./tests/runtime.test.ts:1677:    await handlers.get("session_start")?.({}, ctx);
./tests/runtime.test.ts:1678:    await commands.get("telegram-connect")?.handler("", ctx);
./tests/runtime.test.ts:1688:          callback_query: {
./tests/runtime.test.ts:1703:    assert.equal(callbackAnswers.includes("Switched to claude-b"), true);
./tests/runtime.test.ts:1714:    if (shutdownCtx) await handlers.get("session_shutdown")?.({}, shutdownCtx);
./tests/runtime.test.ts:1717:    await telegramConfig.restore();
./tests/runtime.test.ts:1722:  const telegramConfig = await createRuntimeTelegramConfigFixture();
./tests/runtime.test.ts:1743:  const callbackAnswers: string[] = [];
./tests/runtime.test.ts:1779:      callbackAnswers.push(String(body?.text ?? ""));
./tests/runtime.test.ts:1788:    await telegramConfig.write({
./tests/runtime.test.ts:1802:    await handlers.get("session_start")?.({}, ctx);
./tests/runtime.test.ts:1803:    await commands.get("telegram-connect")?.handler("", ctx);
./tests/runtime.test.ts:1823:        (event) => event === "dispatch:[telegram] first request",
./tests/runtime.test.ts:1833:          callback_query: {
./tests/runtime.test.ts:1848:      callbackAnswers.includes("Switching to claude-b and continuing…"),
./tests/runtime.test.ts:1872:      runtimeEvents.includes("dispatch:[telegram] first request"),
./tests/runtime.test.ts:1878:          "dispatch:[telegram] Continue the interrupted previous Telegram request using the newly selected model (anthropic/claude-b)",
./tests/runtime.test.ts:1883:    await handlers.get("session_shutdown")?.({}, ctx);
./tests/runtime.test.ts:1886:    await telegramConfig.restore();
./tests/runtime.test.ts:1891:  const telegramConfig = await createRuntimeTelegramConfigFixture();
./tests/runtime.test.ts:1912:  const callbackAnswers: string[] = [];
./tests/runtime.test.ts:1948:      callbackAnswers.push(String(body?.text ?? ""));
./tests/runtime.test.ts:1957:    await telegramConfig.write({
./tests/runtime.test.ts:1971:    await handlers.get("session_start")?.({}, ctx);
./tests/runtime.test.ts:1972:    await commands.get("telegram-connect")?.handler("", ctx);
./tests/runtime.test.ts:1992:        (event) => event === "dispatch:[telegram] first request",
./tests/runtime.test.ts:2003:          callback_query: {
./tests/runtime.test.ts:2016:      callbackAnswers.includes(
./tests/runtime.test.ts:2040:          "dispatch:[telegram] Continue the interrupted previous Telegram request using the newly selected model (anthropic/claude-b)",
./tests/runtime.test.ts:2044:    await handlers.get("session_shutdown")?.({}, ctx);
./tests/runtime.test.ts:2047:    await telegramConfig.restore();
./tests/model.test.ts:64:    historyText: "active",
./tests/model.test.ts:316:    historyText: "previous",
./tests/model.test.ts:368:    telegramPrefix: "[telegram]",
./tests/model.test.ts:383:test("Model-switch continuation turn stays control-lane and resume-oriented", () => {
./tests/model.test.ts:398:    telegramPrefix: "[telegram]",
./tests/model.test.ts:410:    turn.historyText,
./tests/model.test.ts:440:    /^\[telegram\] Continue/,
./tests/model.test.ts:457:      historyText: "continue",
./tests/model.test.ts:472:test("Continuation prompt stays Telegram-scoped and resume-oriented", () => {
./tests/model.test.ts:474:    "[telegram]",
./tests/model.test.ts:478:  assert.match(text, /^\[telegram\]/);
./docs/external-handlers.md:3:`pi-telegram` owns a single `getUpdates` long-poll connection per bot. Other
./docs/external-handlers.md:9:(running in the same pi process) hook into `pi-telegram`'s polling loop and
./docs/external-handlers.md:10:react to inbound Telegram updates **before** `pi-telegram`'s default routing
./docs/external-handlers.md:14:[Callback Namespaces](./callback-namespaces.md): callback namespaces define
./docs/external-handlers.md:15:how to share `callback_data` cleanly; external handlers define how to
./docs/external-handlers.md:23:  the moment a Telegram callback arrives, rather than waiting for the next
./docs/external-handlers.md:25:- Suppress `pi-telegram`'s default routing for callbacks owned by the
./docs/external-handlers.md:26:  layered extension (so `pi-telegram` does not also forward them as
./docs/external-handlers.md:27:  `[callback] <data>` text).
./docs/external-handlers.md:31:If the layered extension only needs to read assistant-visible callbacks, the
./docs/external-handlers.md:32:existing `[callback] <data>` fallback documented in
./docs/external-handlers.md:33:[Callback Namespaces](./callback-namespaces.md) is enough.
./docs/external-handlers.md:45:  `@llblab/pi-telegram`.
./docs/external-handlers.md:51:- `"consume"` — `pi-telegram` skips its default routing for this update.
./docs/external-handlers.md:52:- `"pass"` (or `void` / `undefined`) — `pi-telegram` routes the update
./docs/external-handlers.md:63:### Typed import (recommended when you can depend on `@llblab/pi-telegram`)
./docs/external-handlers.md:66:import { onTelegramExternalUpdate } from "@llblab/pi-telegram/lib/external-handlers.ts";
./docs/external-handlers.md:69:  const cb = (update as { callback_query?: { id?: string; data?: string } })
./docs/external-handlers.md:70:    .callback_query;
./docs/external-handlers.md:82:When the layered extension prefers no `import` from `@llblab/pi-telegram` (so
./docs/external-handlers.md:85:just `version` and `add`. pi-telegram's polling runtime calls `dispatch` on
./docs/external-handlers.md:89:pi-telegram defensively re-creates the registry if the object on `globalThis`
./docs/external-handlers.md:106:  // Required: pi-telegram's polling loop calls this on every update.
./docs/external-handlers.md:155:versioned (`version: 1`) and stable across pi-telegram releases; future
./docs/external-handlers.md:160:`pi-telegram` invokes registered interceptors first, then routes the update
./docs/external-handlers.md:162:default prompt routing, callback namespace fallback). If any interceptor
./docs/external-handlers.md:163:returns `"consume"`, `pi-telegram` skips the rest of routing for that update.
./docs/external-handlers.md:167:- Extensions can claim callback namespaces that `pi-telegram` would
./docs/external-handlers.md:168:  otherwise forward as `[callback] <data>` text.
./docs/external-handlers.md:171:- Extensions must not consume updates that belong to `pi-telegram`'s own
./docs/external-handlers.md:179:locked polling runtime stops `pi-telegram`'s poller (for example, after
./docs/external-handlers.md:184:observe `pi-telegram` lifecycle events through the standard pi extension
./tests/queue.test.ts:84:    historyText: "prompt",
./tests/queue.test.ts:113:    historyText: "",
./tests/queue.test.ts:131:    historyText: "",
./tests/queue.test.ts:288:    historyText: "default",
./tests/queue.test.ts:296:    historyText: "priority",
./tests/queue.test.ts:316:    historyText: "prompt history",
./tests/queue.test.ts:337:    historyText: "prompt history",
./tests/queue.test.ts:486:    historyText: "prompt history",
./tests/queue.test.ts:521:    content: [{ type: "text", text: "[telegram] voice transcript" }],
./tests/queue.test.ts:522:    historyText: "voice transcript",
./tests/queue.test.ts:539:    historyText: "prompt history",
./tests/queue.test.ts:546:    historyText: "default history",
./tests/queue.test.ts:564:test("Queue enqueue planning preserves queued prompts as history when requested", () => {
./tests/queue.test.ts:568:    historyText: "prompt history",
./tests/queue.test.ts:574:    historyTurns: [],
./tests/queue.test.ts:578:  assert.deepEqual(plan.historyTurns, [promptItem]);
./tests/queue.test.ts:601:    historyText: "prompt history",
./tests/queue.test.ts:609:    result.historyTurns.map((item) => item.statusSummary),
./tests/queue.test.ts:626:    historyText: "prompt history",
./tests/queue.test.ts:648:    historyText: "prompt history",
./tests/queue.test.ts:666:    historyText: "prompt history",
./tests/queue.test.ts:684:    historyText: "prompt history",
./tests/queue.test.ts:714:    historyText: "prompt history",
./tests/queue.test.ts:731:test("Agent end dispatch policy resumes after success and error, but not preserved aborts", () => {
./tests/queue.test.ts:897:    inline_keyboard: [[{ text: "Continue", callback_data: "btn:1" }]],
./tests/queue.test.ts:903:      text: 'Answer\n\n<!-- telegram_button label="Continue"\nContinue\n-->',
./tests/queue.test.ts:955:        "<!-- telegram_voice lang=ru rate=+20%",
./tests/queue.test.ts:991:      voiceText: markdown.includes("telegram_voice")
./tests/queue.test.ts:1094:      historyText: "prompt",
./tests/queue.test.ts:1241:    historyText: "turn",
./tests/queue.test.ts:1422:    historyText: "prompt history",
./tests/queue.test.ts:1622:      events.push(`flags:${state.telegramTurnDispatchPending}`);
./tests/queue.test.ts:1631:    telegramTurnDispatchPending: false,
./tests/queue.test.ts:1642:    telegramTurnDispatchPending: true,
./tests/queue.test.ts:1748:      historyText: "",
./tests/queue.test.ts:1766:test("Prompt enqueue runtime preserves queued prompts as history", async () => {
./tests/queue.test.ts:1768:  const historyPrompt: PendingTelegramTurn = createQueueTestPromptTurn({
./tests/queue.test.ts:1772:    content: [{ type: "text" as const, text: "history" }],
./tests/queue.test.ts:1773:    historyText: "history",
./tests/queue.test.ts:1774:    statusSummary: "history",
./tests/queue.test.ts:1786:    ...historyPrompt,
./tests/queue.test.ts:1791:    historyText: "new",
./tests/queue.test.ts:1794:  let queuedItems: TelegramQueueItem[] = [historyPrompt, controlItem];
./tests/queue.test.ts:1807:    createTurn: async (_messages, historyTurns) => {
./tests/queue.test.ts:1809:        `history:${historyTurns.map((turn) => turn.historyText).join(",")}`,
./tests/queue.test.ts:1827:    "history:history",
./tests/queue.test.ts:1902:test("Deferred queue dispatch uses only the bound session context", () => {
./tests/queue.test.ts:1904:  const callbacks: Array<() => void> = [];
./tests/queue.test.ts:1907:    setTimer: (callback) => {
./tests/queue.test.ts:1908:      callbacks.push(callback);
./tests/queue.test.ts:1909:      return callbacks.length as unknown as ReturnType<typeof setTimeout>;
./tests/queue.test.ts:1918:  assert.equal(callbacks.length, 0);
./tests/queue.test.ts:1924:  callbacks[0]?.();
./tests/queue.test.ts:1929:  callbacks[1]?.();
./tests/queue.test.ts:2035:        historyText: "prompt",
./tests/queue.test.ts:2074:      historyText: "prompt",
./tests/queue.test.ts:2118:      historyText: "prompt",
./tests/queue.test.ts:2179:      historyText: "prompt",
./tests/queue.test.ts:2281:test("Session runtime helper resets session start state", () => {
./tests/queue.test.ts:2288:  assert.equal(state.telegramTurnDispatchPending, false);
./tests/queue.test.ts:2331:  assert.equal(state.telegramTurnDispatchPending, false);
./tests/locks.test.ts:20:  const dir = mkdtempSync(join(tmpdir(), "pi-telegram-locks-"));
./tests/locks.test.ts:137:    const blocked = await runtime.start({ cwd: "/new" });
./tests/locks.test.ts:145:    const moved = await runtime.start({ cwd: "/new" }, { force: true });
./tests/locks.test.ts:152:      cwd: "/new",
./tests/locks.test.ts:218:test("Locked polling runtime suspends session replacement without releasing ownership", async () => {
./tests/locks.test.ts:290:test("Locked polling runtime resumes stale same-cwd ownership after process restart", async () => {
./tests/locks.test.ts:328:test("Locked polling runtime does not claim stale ownership from another cwd during session initialization", async () => {
./tests/preview.test.ts:439:      text: "hello\n\n<!-- telegram_voice\nhidden streaming voice",
./docs/README.md:9:- [inbound-handlers.md](./inbound-handlers.md) — Local `pi-telegram` inbound text/media handler bus, legacy `attachmentHandlers` compatibility, placeholders, and fallbacks
./docs/README.md:10:- [outbound-handlers.md](./outbound-handlers.md) — Local `pi-telegram` outbound-handler config, text/voice/button behavior, artifact outputs, and callback routing
./docs/README.md:12:- [callback-namespaces.md](./callback-namespaces.md) — Shared Telegram `callback_data` namespace standard for layered extensions
./tests/api.test.ts:3: * Verifies direct helper behavior around missing tokens, callback-query failures, downloads, and runtime transport binding
./tests/api.test.ts:147:    assert.equal(String(url), "https://api.telegram.org/bot123:abc/getMe");
./tests/api.test.ts:160:  const tempDir = await mkdtemp(join(tmpdir(), "pi-telegram-cleanup-"));
./tests/api.test.ts:176:    join(tmpdir(), "pi-telegram-prepare-parent-"),
./tests/api.test.ts:178:  const tempDir = join(parentDir, "nested", "telegram");
./tests/api.test.ts:197:        join(tmpdir(), "pi-telegram-missing-token"),
./tests/api.test.ts:259:  const tempDir = await mkdtemp(join(tmpdir(), "pi-telegram-upload-"));
./tests/api.test.ts:297:  const tempDir = await mkdtemp(join(tmpdir(), "pi-telegram-download-"));
./tests/api.test.ts:320:  const tempDir = await mkdtemp(join(tmpdir(), "pi-telegram-download-limit-"));
./tests/api.test.ts:347:    join(tmpdir(), "pi-telegram-download-partial-"),
./tests/api.test.ts:389:      answerTelegramCallbackQuery("123:abc", "callback-id", "ok"),
./tests/api.test.ts:415:  const tempDir = await mkdtemp(join(tmpdir(), "telegram-runtime-temp-"));
./tests/api.test.ts:436:    tempDir: "/tmp/telegram",
./tests/api.test.ts:485:    { tempDir: "/tmp/telegram", maxFileSizeBytes: 123 },
./tests/api.test.ts:498:    tempDir: "/tmp/telegram",
./tests/api.test.ts:549:    tempDir: "/tmp/telegram",
./tests/lifecycle.test.ts:43:test("Lifecycle helpers compose session hooks in order", async () => {
./tests/lifecycle.test.ts:78:      events.push("session-start");
./tests/lifecycle.test.ts:81:      events.push("session-shutdown");
./tests/lifecycle.test.ts:112:      "session_start",
./tests/lifecycle.test.ts:113:      "session_shutdown",
./tests/lifecycle.test.ts:125:  await getRequiredLifecycleHandler(harness.handlers, "session_start")({}, ctx);
./tests/lifecycle.test.ts:126:  await getRequiredLifecycleHandler(harness.handlers, "session_shutdown")(
./tests/lifecycle.test.ts:152:    "session-start",
./tests/lifecycle.test.ts:153:    "session-shutdown",
./tests/index.test.ts:9:import telegramExtension from "../index.ts";
./tests/index.test.ts:65:  telegramExtension(harness.api);
./tests/index.test.ts:66:  assert.equal(harness.tool()?.name, "telegram_attach");
./tests/index.test.ts:70:      "telegram-setup",
./tests/index.test.ts:71:      "telegram-status",
./tests/index.test.ts:72:      "telegram-settings",
./tests/index.test.ts:73:      "telegram-connect",
./tests/index.test.ts:74:      "telegram-disconnect",
./tests/index.test.ts:80:      "session_start",
./tests/index.test.ts:81:      "session_shutdown",
./tests/index.test.ts:96:  telegramExtension(harness.api);
./tests/index.test.ts:102:  const telegramResult = await handler(
./tests/index.test.ts:103:    { systemPrompt: basePrompt, prompt: "[telegram] hello" },
./tests/index.test.ts:110:  assertSystemPromptResult(telegramResult);
./tests/index.test.ts:113:    telegramResult.systemPrompt,
./tests/index.test.ts:116:  assert.match(telegramResult.systemPrompt, /telegram_attach/);
./docs/locks.md:54:`pid` is the process that currently owns the singleton runtime. `cwd` should be stored when ownership is tied to a pi session directory.
./docs/locks.md:67:Extension initialization and session-start hooks may read `locks.json`, update local status, install ownership watchers, and resume local work when the existing lock already points at the current `pid`/`cwd`. After a full process restart, a session-start hook may replace a stale lock from the same `cwd` to restore explicitly requested ownership. They must not create ownership from an inactive lock, take over a live external owner, or replace a stale lock from another directory by themselves. Such locks should stay visible as state until the user runs the start/connect command. Session replacement should suspend local runtime work and ownership watchers without releasing the lock, so the next session in the same `pid`/`cwd` can resume from explicit ownership.
./docs/locks.md:101:Extensions may prefix those states with their own compact name, such as `wakeup off` or `telegram on`.
./docs/locks.md:119:to: /new
./docs/locks.md:124:The previous owner may use `fs.watch`, mtime polling, or an existing status/timer tick. Long-lived watchers should compare against a snapshotted `pid`/`cwd` identity rather than a live pi context object, because session replacement such as `/new` makes captured contexts stale. The important contract is graceful local shutdown after ownership mismatch.
./docs/callback-namespaces.md:3:Telegram `callback_data` is one bot-wide namespace. Any extension that creates inline buttons for a bot shared with `pi-telegram` must use namespaced callback data.
./docs/callback-namespaces.md:23:- Do not use `pi-telegram` owned prefixes: `tgbtn:`, `menu:`, `model:`, `thinking:`, `status:`, `queue:`. Current app navigation uses `menu:`; `status:` remains reserved for legacy/owned status callbacks but is not emitted by current UI.
./docs/callback-namespaces.md:24:- Keep the full `callback_data` within Telegram's 64-byte limit.
./docs/callback-namespaces.md:26:- Treat callbacks as untrusted input. Validate namespace, action, and payload before executing side effects.
./docs/callback-namespaces.md:28:## pi-telegram fallback
./docs/callback-namespaces.md:30:If `pi-telegram` receives callback data that is not owned by its built-in prefixes and no built-in handler consumes it, it forwards the click to π as:
./docs/callback-namespaces.md:33:[callback] <callback_data>
./docs/callback-namespaces.md:36:Layered extensions may intercept that message and handle their own namespace. If no extension handles it, the assistant may see the fallback message and should tell the user the callback was not handled and the environment may be misconfigured.
./tests/config.test.ts:29:  const agentDir = await mkdtemp(join(tmpdir(), "pi-telegram-missing-config-"));
./tests/config.test.ts:31:    await readTelegramConfig(join(agentDir, "telegram.json")),
./tests/config.test.ts:37:  const agentDir = await mkdtemp(join(tmpdir(), "pi-telegram-config-"));
./tests/config.test.ts:38:  const configPath = join(agentDir, "telegram.json");
./tests/config.test.ts:57:  const agentDir = await mkdtemp(join(tmpdir(), "pi-telegram-store-"));
./tests/config.test.ts:58:  const configPath = join(agentDir, "telegram.json");
./tests/menu.test.ts:3: * Covers inline model/status/thinking menu state, callbacks, and transport payloads
./tests/menu.test.ts:295:test("Menu helpers build model menu state and parse callback actions", () => {
./tests/menu.test.ts:382:  assert.equal(markup.inline_keyboard[0]?.[0]?.callback_data, "menu:back");
./tests/menu.test.ts:384:  assert.equal(markup.inline_keyboard[1]?.[1]?.callback_data, "model:pages");
./tests/menu.test.ts:402:    markup.inline_keyboard[0]?.[0]?.callback_data,
./tests/menu.test.ts:418:test("Menu helpers build model callback plans for paging, page menu, selection, and restart modes", () => {
./tests/menu.test.ts:508:      callbackText: "Model: gpt-5",
./tests/menu.test.ts:526:      callbackText:
./tests/menu.test.ts:579:      callbackText: "Switched to claude-3",
./tests/menu.test.ts:612:      callbackText: "Switched to claude-3",
./tests/menu.test.ts:682:      callbackText: "Model: gpt-5",
./tests/menu.test.ts:714:      callbackText: "Model: model-7",
./tests/menu.test.ts:827:test("Menu helpers route callback entry states before action handlers", async () => {
./tests/menu.test.ts:829:  await handleTelegramMenuCallbackEntry("callback-1", undefined, undefined, {
./tests/menu.test.ts:837:  await handleTelegramMenuCallbackEntry("callback-2", "menu:model", undefined, {
./tests/menu.test.ts:846:    "callback-3",
./tests/menu.test.ts:876:test("Menu helpers route stored callback queries through matching action handlers", async () => {
./tests/menu.test.ts:880:    { id: "callback-1", data: "menu:model", message: { message_id: 2 } },
./tests/menu.test.ts:904:    { id: "callback-2", data: "menu:model" },
./tests/menu.test.ts:926:test("Menu runtime routes stored callback queries through callback action ports", async () => {
./tests/menu.test.ts:937:    { id: "callback-1", data: "menu:thinking", message: { message_id: 2 } },
./tests/menu.test.ts:986:    { id: "callback-2", data: "thinking:set:high", message: { message_id: 2 } },
./tests/menu.test.ts:1022:    { id: "callback-3", data: "model:pick:0", message: { message_id: 2 } },
./tests/menu.test.ts:1085:test("Menu callback handler captures runtime ports", async () => {
./tests/menu.test.ts:1123:    { id: "callback", data: "menu:model", message: { message_id: 2 } },
./tests/menu.test.ts:1129:test("Menu callback adapter converts active tool count into runtime booleans", async () => {
./tests/menu.test.ts:1174:    { id: "callback", data: "model:pick:1", message: { message_id: 2 } },
./tests/menu.test.ts:1179:    { id: "callback", data: "model:pick:1", message: { message_id: 2 } },
./tests/menu.test.ts:1194:test("Menu helpers execute model callback actions across update, switch, and restart paths", async () => {
./tests/menu.test.ts:1204:      "callback-1",
./tests/menu.test.ts:1244:      "callback-2",
./tests/menu.test.ts:1284:      "callback-3",
./tests/menu.test.ts:1337:test("Menu helpers handle status and thinking callback actions", async () => {
./tests/menu.test.ts:1343:      "callback-1",
./tests/menu.test.ts:1362:      "callback-2",
./tests/menu.test.ts:1382:      "callback-3",
./tests/menu.test.ts:1618:        { type: "text", text: "[telegram] queued <prompt>\n\nfull body" },
./tests/menu.test.ts:1620:      historyText: "",
./tests/menu.test.ts:1626:    inline_keyboard: Array<Array<{ text: string; callback_data: string }>>;
./tests/menu.test.ts:1629:    telegramQueueStore: {
./tests/menu.test.ts:1683:      id: "callback",
./tests/menu.test.ts:1691:      id: "callback",
./tests/menu.test.ts:1699:      id: "callback",
./tests/menu.test.ts:1707:  assert.equal(markups[0]?.inline_keyboard[0]?.[0]?.callback_data, "menu:back");
./tests/menu.test.ts:1708:  assert.equal(markups[1]?.inline_keyboard[0]?.[0]?.callback_data, "menu:back");
./tests/menu.test.ts:1714:    { text: "🌀 Refresh", callback_data: "queue:refresh" },
./tests/menu.test.ts:1717:    { text: "🟡 Priority", callback_data: "queue:prio-set:1:10:priority" },
./tests/menu.test.ts:1718:    { text: "⚫ Normal", callback_data: "queue:prio-set:1:10:normal" },
./tests/menu.test.ts:1721:    { text: "🗑 Delete", callback_data: "queue:delete:1:10" },
./tests/menu.test.ts:1724:    { text: "⚫ Priority", callback_data: "queue:prio-set:1:10:priority" },
./tests/menu.test.ts:1725:    { text: "🟡 Normal", callback_data: "queue:prio-set:1:10:normal" },
./tests/menu.test.ts:1728:    [{ text: "⬆ Main menu", callback_data: "menu:back" }],
./tests/menu.test.ts:1729:    [{ text: "🌀 Refresh", callback_data: "queue:refresh" }],
./tests/menu.test.ts:1734:    "<b>1.</b> 🕊\n<pre>[telegram] queued <prompt>\n\nfull body</pre>",
./tests/menu.test.ts:1738:    "<b>1.</b>\n<pre>[telegram] queued <prompt>\n\nfull body</pre>",
./tests/menu.test.ts:1747:    `[telegram]\n[attachments] /home/user/.pi/agent/tmp/telegram\n` +
./tests/menu.test.ts:1748:    `- /home/user/.pi/agent/tmp/telegram/file.txt\n${"&<>".repeat(2000)}`;
./tests/menu.test.ts:1751:    telegramQueueStore: {
./tests/menu.test.ts:1764:          historyText: "",
./tests/menu.test.ts:1791:      id: "callback",
./tests/menu.test.ts:1797:  assert.match(texts[0] ?? "", /^<b>1\.<\/b>\n<pre>\[telegram\]/);
./tests/menu.test.ts:1798:  assert.match(texts[0] ?? "", /\/home\/user\/\.pi\/agent\/tmp\/telegram/);
./tests/menu.test.ts:1817:      content: [{ type: "text", text: "[telegram] delete me" }],
./tests/menu.test.ts:1818:      historyText: "",
./tests/menu.test.ts:1824:    inline_keyboard: Array<Array<{ text: string; callback_data: string }>>;
./tests/menu.test.ts:1827:    telegramQueueStore: {
./tests/menu.test.ts:1879:      { text: "🗑 Yes, delete", callback_data: "queue:confirm-delete:1:10" },
./tests/menu.test.ts:1880:      { text: "❌ No", callback_data: "queue:keep:1:10" },
./tests/menu.test.ts:1902:  assert.equal(texts[2], "<b>1.</b>\n<pre>[telegram] delete me</pre>");
./tests/menu.test.ts:1935:    { text: "🟢 Active", callback_data: "model:pick-selected" },
./tests/menu.test.ts:1938:    { text: "🟡 Scoped", callback_data: "model:scope-enable" },
./tests/menu.test.ts:1939:    { text: "⚫ All", callback_data: "model:scope-disable" },
./tests/menu.test.ts:1946:    { text: "☑ Activate", callback_data: "model:pick-selected" },
./tests/menu.test.ts:1948:  assert.equal(modelMarkup.inline_keyboard[0]?.[0]?.callback_data, "menu:back");
./tests/menu.test.ts:1950:    { text: "🟡 Scoped", callback_data: "model:scope:scoped" },
./tests/menu.test.ts:1951:    { text: "⚫ All", callback_data: "model:scope:all" },
./tests/menu.test.ts:1954:    modelMarkup.inline_keyboard[2]?.[0]?.callback_data,
./tests/menu.test.ts:1958:    modelMarkup.inline_keyboard[3]?.[0]?.callback_data,
./tests/menu.test.ts:1966:    thinkingMarkup.inline_keyboard[0]?.[0]?.callback_data,
./tests/menu.test.ts:1975:    row.map((button) => button.callback_data),
./tests/menu.test.ts:1999:    statusCallbackData.some((callbackData) =>
./tests/menu.test.ts:2000:      callbackData.startsWith("status:"),
./tests/menu.test.ts:2011:    [{ text: "🟢 Proactive push", callback_data: "settings:open:proactive" }],
./tests/menu.test.ts:2015:    [{ text: "⚫ Proactive push", callback_data: "settings:open:proactive" }],
./tests/menu.test.ts:2023:      { text: "🟢 On", callback_data: "settings:set:proactive:on" },
./tests/menu.test.ts:2024:      { text: "⚫ Off", callback_data: "settings:set:proactive:off" },
./tests/menu.test.ts:2030:      { text: "⚫ On", callback_data: "settings:set:proactive:on" },
./tests/menu.test.ts:2031:      { text: "🟡 Off", callback_data: "settings:set:proactive:off" },
./tests/updates.test.ts:135:test("Update routing extracts only private human callback queries", () => {
./tests/updates.test.ts:138:      callback_query: {
./tests/updates.test.ts:146:    callback_query: {
./tests/updates.test.ts:197:test("Update flow returns authorized callback, message, and edit actions", () => {
./tests/updates.test.ts:198:  const callbackAction = buildTelegramUpdateFlowAction(
./tests/updates.test.ts:200:      callback_query: {
./tests/updates.test.ts:207:  assert.equal(callbackAction.kind, "callback");
./tests/updates.test.ts:209:    callbackAction.kind === "callback"
./tests/updates.test.ts:210:      ? callbackAction.authorization
./tests/updates.test.ts:249:    callback_query: {
./tests/updates.test.ts:257:test("Update execution plan maps callback and message authorization to side-effect flags", () => {
./tests/updates.test.ts:258:  const callbackPlan = buildTelegramUpdateExecutionPlan({
./tests/updates.test.ts:259:    kind: "callback",
./tests/updates.test.ts:266:  assert.deepEqual(callbackPlan, {
./tests/updates.test.ts:267:    kind: "callback",
./tests/updates.test.ts:313:      callback_query: {
./tests/updates.test.ts:320:  assert.equal(plan.kind, "callback");
./tests/updates.test.ts:321:  assert.equal(plan.kind === "callback" ? plan.shouldDeny : false, true);
./tests/updates.test.ts:351:      events.push("callback");
./tests/updates.test.ts:641:test("Update runtime handles callback deny and message pair flows", async () => {
./tests/updates.test.ts:645:      kind: "callback",
./tests/updates.test.ts:667:        events.push("callback");
./tests/media.test.ts:3: * Covers inbound file-info collection, text extraction, media groups, id collection, and history formatting
./tests/media.test.ts:102:test("Media helpers extract text, ids, and history summaries", () => {
./tests/media.test.ts:191:  const callbacks: Array<() => void> = [];
./tests/media.test.ts:194:  const setTimer = (callback: () => void): TestTimer => {
./tests/media.test.ts:195:    callbacks.push(callback);
./tests/media.test.ts:223:  callbacks.at(-1)?.();
./tests/media.test.ts:230:  const callbacks: Array<() => void> = [];
./tests/media.test.ts:239:    setTimer: (callback) => {
./tests/media.test.ts:240:      callbacks.push(callback);
./tests/media.test.ts:263:  callbacks.at(-1)?.();
./tests/media.test.ts:275:  const callbacks: Array<() => void> = [];
./tests/media.test.ts:285:    setTimer: (callback) => {
./tests/media.test.ts:286:      callbacks.push(callback);
./tests/media.test.ts:287:      return createTestTimer(callbacks.length);
./tests/media.test.ts:309:  callbacks.at(-1)?.();
./docs/inbound-handlers.md:3:`pi-telegram` can run ordered inbound handlers before a Telegram turn enters the π queue. Inbound handlers are the provider-neutral Telegram → π transformation bus for raw text and downloaded media/files.
./docs/inbound-handlers.md:33:Legacy `telegram.json` files may still define `attachmentHandlers` for media/file preprocessing:
./tests/turns.test.ts:36:test("Turn helpers build prompt text with history and attachments", () => {
./tests/turns.test.ts:38:    telegramPrefix: "[telegram]",
./tests/turns.test.ts:41:    historyTurns: [{ historyText: "older message" }],
./tests/turns.test.ts:43:  assert.match(prompt, /^\[telegram\]/);
./tests/turns.test.ts:55:    formatTelegramTurnStatusSummary("hello there from telegram", []),
./tests/turns.test.ts:56:    "hello there from telegram",
./tests/turns.test.ts:87:    content: [{ type: "text" as const, text: "[telegram] old" }],
./tests/turns.test.ts:88:    historyText: "old",
./tests/turns.test.ts:93:    telegramPrefix: "[telegram]",
./tests/turns.test.ts:99:    "[telegram] new edited message",
./tests/turns.test.ts:101:  assert.equal(updated.historyText, "new edited message");
./tests/turns.test.ts:148:    turn.historyText,
./tests/turns.test.ts:153:    "[telegram] Not yet\n\n[reply] Have you seen the latest Claude update?",
./tests/turns.test.ts:188:    turn.historyText,
./tests/turns.test.ts:193:    "[telegram]\n\n[attachments] /tmp\n- /voice-12.ogg\n\n[outputs]\n- transcript from /work",
./tests/turns.test.ts:198:  const tempDir = await mkdtemp(join(tmpdir(), "pi-telegram-turn-runtime-"));
./tests/turns.test.ts:202:    telegramPrefix: "[telegram]",
./tests/turns.test.ts:233:    content: [{ type: "text" as const, text: "[telegram] old" }],
./tests/turns.test.ts:234:    historyText: "old",
./tests/turns.test.ts:251:    telegramPrefix: "[telegram]",
./tests/turns.test.ts:258:    "[telegram] edited",
./tests/turns.test.ts:263:    telegramPrefix: "[telegram]",
./tests/turns.test.ts:282:      content: [{ type: "text" as const, text: "[telegram] old" }],
./tests/turns.test.ts:283:      historyText: "old",
./tests/turns.test.ts:306:    "[telegram] edited",
./tests/turns.test.ts:322:      content: [{ type: "text" as const, text: "[telegram] old" }],
./tests/turns.test.ts:323:      historyText: "old",
./tests/turns.test.ts:351:    "[telegram] edited\n\n[reply] quoted",
./tests/turns.test.ts:353:  assert.equal(items[0]?.historyText, "edited\n\n[reply] quoted");
./tests/turns.test.ts:371:          "[telegram] old caption\n\n" +
./tests/turns.test.ts:378:    historyText:
./tests/turns.test.ts:384:    telegramPrefix: "[telegram]",
./tests/turns.test.ts:389:    "[telegram] new caption\n\n" +
./tests/turns.test.ts:396:    updated.historyText,
./tests/turns.test.ts:402:test("Turn helpers preserve abort-history prompt context when queued turns are edited", () => {
./tests/turns.test.ts:416:          "[telegram]\n\n" +
./tests/turns.test.ts:423:    historyText: "old current",
./tests/turns.test.ts:428:    telegramPrefix: "[telegram]",
./tests/turns.test.ts:433:    "[telegram]\n\n" +
./tests/turns.test.ts:439:  assert.equal(updated.historyText, "new current");
./tests/turns.test.ts:443:test("Turn helpers assemble prompt turns with text, ids, history, and image payloads", async () => {
./tests/turns.test.ts:445:    telegramPrefix: "[telegram]",
./tests/turns.test.ts:450:    historyTurns: [
./tests/turns.test.ts:461:        historyText: "older message",
./tests/turns.test.ts:489:    turn.historyText,
./tests/prompts.test.ts:29:      prompt: " [telegram] hello",
./tests/prompts.test.ts:31:      telegramPrefix: "[telegram]",
./tests/prompts.test.ts:43:      telegramPrefix: "[telegram]",
./tests/prompts.test.ts:52:    telegramPrefix: "[telegram]",
./tests/prompts.test.ts:56:    hook(createBeforeAgentStartEvent(" [telegram] hello", "base")),
./tests/prompts.test.ts:63:    createBeforeAgentStartEvent(" [telegram] hello", "base"),
./tests/prompts.test.ts:76:  assert.match(defaultSystemPrompt, /telegram_attach/);
./tests/prompts.test.ts:77:  assert.match(defaultSystemPrompt, /telegram_voice text="Short summary"/);
./tests/prompts.test.ts:78:  assert.match(defaultSystemPrompt, /telegram_button: OK/);
./tests/prompts.test.ts:79:  assert.match(defaultSystemPrompt, /telegram_button label=Continue prompt=/);
./tests/prompts.test.ts:90:      telegramPrefix: "[telegram]",
./tests/outbound-attachments.test.ts:86:  assert.equal(tool?.name, "telegram_attach");
./tests/outbound-attachments.test.ts:112:    join(tmpdir(), "pi-telegram-attachment-queue-"),
./tests/outbound-attachments.test.ts:245:  const tempDir = await mkdtemp(join(tmpdir(), "pi-telegram-attachment-"));
./tests/inbound-handlers.test.ts:15:  telegramInboundHandlerMatchesFile,
./tests/inbound-handlers.test.ts:26:    telegramInboundHandlerMatchesFile({ mime: "audio/*" }, voiceFile),
./tests/inbound-handlers.test.ts:30:    telegramInboundHandlerMatchesFile({ type: "voice" }, voiceFile),
./tests/inbound-handlers.test.ts:34:    telegramInboundHandlerMatchesFile(
./tests/inbound-handlers.test.ts:40:  assert.equal(telegramInboundHandlerMatchesFile({}, voiceFile), true);
./tests/inbound-handlers.test.ts:197:  const dir = await mkdtemp(join(tmpdir(), "pi-telegram-text-attachment-"));
./tests/inbound-handlers.test.ts:218:  const dir = await mkdtemp(join(tmpdir(), "pi-telegram-text-attachment-"));
./tests/commands.test.ts:91:    { command: "compact", description: "🗜 Compact current session" },
./tests/commands.test.ts:92:    { command: "new", description: "🆕 Start new clean session" },
./tests/commands.test.ts:154:  await getRequiredCommand(harness.commands, "telegram-setup").handler("", ctx);
./tests/commands.test.ts:155:  await getRequiredCommand(harness.commands, "telegram-status").handler(
./tests/commands.test.ts:191:  await getRequiredCommand(harness.commands, "telegram-settings").handler(
./tests/commands.test.ts:224:  await getRequiredCommand(harness.commands, "telegram-connect").handler(
./tests/commands.test.ts:229:  await getRequiredCommand(harness.commands, "telegram-connect").handler(
./tests/commands.test.ts:233:  await getRequiredCommand(harness.commands, "telegram-disconnect").handler(
./tests/commands.test.ts:279:  await getRequiredCommand(harness.commands, "telegram-connect").handler(
./tests/commands.test.ts:637:    compact: (callbacks) => {
./tests/commands.test.ts:639:      complete = callbacks.onComplete;
./tests/commands.test.ts:682:    compact: (callbacks) => {
./tests/commands.test.ts:684:      fail = callbacks.onError;
./tests/commands.test.ts:861:      callbacks: { onComplete: () => void },
./tests/commands.test.ts:864:      compactComplete = callbacks.onComplete;
./tests/status.test.ts:36:    "<accent>telegram</accent> <muted>not configured</muted>",
./tests/status.test.ts:47:    "<accent>telegram</accent> <warning>active</warning><success> +1</success>",
./tests/status.test.ts:59:    "<accent>telegram</accent> <warning>active</warning><success> +1</success>",
./tests/status.test.ts:71:    "<accent>telegram</accent> <warning>active</warning>",
./tests/status.test.ts:83:    "<accent>telegram</accent> <error>error</error> <muted>typing failed</muted>",
./tests/status.test.ts:126:    "telegram:<accent>telegram</accent> <error>error</error> <muted>demo error</muted>",
./tests/status.test.ts:209:    "telegram:<accent>telegram</accent> <warning>active</warning>",
./tests/status.test.ts:248:    "telegram:<accent>telegram</accent> <warning>active</warning><success> +1</success>",
./tests/status.test.ts:328:    sessionManager: { getEntries: () => [] },
./tests/status.test.ts:343:    sessionManager: { getEntries: () => [] },
./tests/text-groups.test.ts:41:      setTimer: (callback) => {
./tests/text-groups.test.ts:42:        timers.push(callback);
./tests/text-groups.test.ts:43:        return callback as unknown as ReturnType<typeof setTimeout>;
./tests/text-groups.test.ts:62:  const timers: Array<{ active: boolean; callback: () => void }> = [];
./tests/text-groups.test.ts:69:    setTimer: (callback) => {
./tests/text-groups.test.ts:70:      const timer = { active: true, callback };
./tests/text-groups.test.ts:91:    if (timer.active) timer.callback();
./tests/text-groups.test.ts:109:      setTimer: (callback) => {
./tests/text-groups.test.ts:110:        timers.push(callback);
./tests/text-groups.test.ts:111:        return callback as unknown as ReturnType<typeof setTimeout>;
./tests/text-groups.test.ts:131:    setTimer: (callback: () => void) => {
./tests/text-groups.test.ts:132:      timers.push(callback);
./tests/text-groups.test.ts:133:      return callback as unknown as ReturnType<typeof setTimeout>;
./tests/pi.test.ts:117:    compact: (callbacks: { onComplete: () => void }) => {
./tests/pi.test.ts:119:      callbacks.onComplete();

[Showing lines 577-1138 of 1138 (50.0KB limit). Full output: /tmp/pi-bash-83c9dee7471d1eda.log]


cd /mnt/rpi/dev/pi-telegram-sebas && rg -n "registerCommand|command: \"new\"|compact|sessionManager|getEntries|resume" src lib index.ts tests/commands.test.ts tests/runtime.test.ts


rg: src: No such file or directory (os error 2)
index.ts:79:  const compact = Pi.compactExtensionContext;
index.ts:349:    compact,
tests/commands.test.ts:48:    registerCommand: (name: string, definition: RegisteredBridgeCommand) => {
tests/commands.test.ts:91:    { command: "compact", description: "🗜 Compact current session" },
tests/commands.test.ts:92:    { command: "new", description: "🆕 Start new clean session" },
tests/commands.test.ts:472:  assert.deepEqual(buildTelegramCommandAction("compact"), {
tests/commands.test.ts:473:    kind: "compact",
tests/commands.test.ts:516:    ["compact", "immediate"],
tests/commands.test.ts:595:test("Command helpers guard and complete compact command flow", async () => {
tests/commands.test.ts:613:    compact: () => {
tests/commands.test.ts:614:      events.push("unexpected:compact");
tests/commands.test.ts:637:    compact: (callbacks) => {
tests/commands.test.ts:638:      events.push("compact");
tests/commands.test.ts:647:    "reply:Cannot compact while π or the Telegram queue is busy. Wait for queued turns to finish or send /abort first.",
tests/commands.test.ts:650:    "compact",
tests/commands.test.ts:659:test("Command helpers report compact errors", async () => {
tests/commands.test.ts:682:    compact: (callbacks) => {
tests/commands.test.ts:683:      events.push("compact");
tests/commands.test.ts:706:    compact: () => {
tests/commands.test.ts:717:    "compact",
tests/commands.test.ts:722:    "event:compact:boom",
tests/commands.test.ts:728:    "event:compact:sync boom",
tests/commands.test.ts:797:    compact: () => {},
tests/commands.test.ts:828:  let compactComplete: (() => void) | undefined;
tests/commands.test.ts:851:      events.push(`compact:${inProgress}`);
tests/commands.test.ts:859:    compact: (
tests/commands.test.ts:863:      events.push("compact:start");
tests/commands.test.ts:864:      compactComplete = callbacks.onComplete;
tests/commands.test.ts:917:  assert.equal(await handleCommand("compact", message, { idle: true }), true);
tests/commands.test.ts:918:  compactComplete?.();
tests/commands.test.ts:935:    "compact:true",
tests/commands.test.ts:937:    "compact:start",
tests/commands.test.ts:939:    "compact:false",
tests/commands.test.ts:991:      events.push("compact");
tests/runtime.test.ts:3: * Exercises polling, queue/lifecycle integration, previews, reactions, compaction, and in-flight model switching
tests/runtime.test.ts:173:    sessionManager: {
tests/runtime.test.ts:174:      getEntries: () => [],
tests/runtime.test.ts:229:    registerCommand: (name: string, definition: RuntimeHarnessCommand) => {
tests/runtime.test.ts:304:    compactionInProgress: true,
tests/runtime.test.ts:916:      sessionManager: {
tests/runtime.test.ts:917:        getEntries: () => [],
tests/runtime.test.ts:1056:      sessionManager: {
tests/runtime.test.ts:1057:        getEntries: () => [],
tests/runtime.test.ts:1130:test("Extension runtime keeps queued turns blocked until compaction completes", async () => {
tests/runtime.test.ts:1133:  let compactHooks:
tests/runtime.test.ts:1163:              text: "/compact",
tests/runtime.test.ts:1189:      compact: (hooks: {
tests/runtime.test.ts:1193:        compactHooks = hooks;
tests/runtime.test.ts:1194:        runtimeEvents.push("compact:start");
tests/runtime.test.ts:1199:    await waitForCondition(() => runtimeEvents.includes("compact:start"));
tests/runtime.test.ts:1210:            text: "follow up after compaction",
tests/runtime.test.ts:1218:        (event) => event === "dispatch:[telegram] follow up after compaction",
tests/runtime.test.ts:1222:    compactHooks?.onComplete();
tests/runtime.test.ts:1224:      runtimeEvents.includes("dispatch:[telegram] follow up after compaction"),
lib/commands.ts:35:  compact: "🗜",
lib/commands.ts:75:      command: "compact",
lib/commands.ts:77:        "compact",
lib/commands.ts:82:      command: "new",
lib/commands.ts:188:  pi.registerCommand("telegram-setup", {
lib/commands.ts:194:  pi.registerCommand("telegram-status", {
lib/commands.ts:200:  pi.registerCommand("telegram-settings", {
lib/commands.ts:230:  pi.registerCommand("telegram-connect", {
lib/commands.ts:257:  pi.registerCommand("telegram-disconnect", {
lib/commands.ts:274:  "compact",
lib/commands.ts:306:  | { kind: "compact"; executionMode: "immediate" }
lib/commands.ts:371:  compact: (callbacks: {
lib/commands.ts:602:  compact: (
lib/commands.ts:634:  `${formatTelegramCommandEmojiPrefix("compact")}/compact — Compact current session`,
lib/commands.ts:703:  compact: { kind: "compact", executionMode: "immediate" },
lib/commands.ts:830:      "Cannot compact while π or the Telegram queue is busy. Wait for queued turns to finish or send /abort first.",
lib/commands.ts:837:    deps.compact({
lib/commands.ts:848:        deps.recordRuntimeEvent?.("compact", error);
lib/commands.ts:856:    deps.recordRuntimeEvent?.("compact", error);
lib/commands.ts:910:    case "compact":
lib/commands.ts:996:    compact: deps.compact,
lib/commands.ts:1141:          compact: (callbacks) => deps.compact(commandCtx, callbacks),
lib/prompts.ts:21:- Telegram is often phone-width; keep tables, dense list items, and compact text blocks at or below 37 visible cells when possible.
lib/status.ts:43:  sessionManager: { getEntries(): TelegramStatusSessionEntry[] };
lib/status.ts:95:  compactionInProgress: boolean;
lib/status.ts:113:  compactionInProgress: boolean;
lib/status.ts:350:      const compactionInProgress = deps.isCompactionInProgress();
lib/status.ts:355:        compactionInProgress,
lib/status.ts:383:        compactionInProgress: deps.isCompactionInProgress(),
lib/status.ts:426:  if (state.compactionInProgress) {
lib/status.ts:427:    return `${label} ${theme.fg("accent", "compacting")}${queued}`;
lib/status.ts:465:    `- compaction: ${state.compactionInProgress ? "running" : "idle"}`,
lib/status.ts:500:  for (const entry of ctx.sessionManager.getEntries()) {
lib/status.ts:553:  if (ctx.isCompactionInProgress?.()) return "compacting";
lib/pi.ts:109:export function compactExtensionContext(
lib/pi.ts:111:  callbacks: Parameters<ExtensionContext["compact"]>[0],
lib/pi.ts:112:): ReturnType<ExtensionContext["compact"]> {
lib/pi.ts:113:  return ctx.compact(callbacks);
lib/outbound-attachments.ts:94:  // Pi's compact tool rows need an empty first line to visually separate header and result
lib/queue.ts:120:  compactionInProgress: boolean;
lib/queue.ts:375:    !state.compactionInProgress &&
lib/queue.ts:396:      compactionInProgress: deps.isCompactionInProgress(),
lib/queue.ts:1067:  compactionInProgress: boolean;
lib/queue.ts:1079:  compactionInProgress: boolean;
lib/queue.ts:1092:  compactionInProgress?: boolean;
lib/queue.ts:1262:    compactionInProgress: false,
lib/queue.ts:1278:    compactionInProgress: false,
lib/routing.ts:103:  compact: (
lib/routing.ts:293:    compact: deps.compact,
lib/runtime.ts:18:  compactionInProgress: boolean;
lib/runtime.ts:86:    compactionInProgress: false,
lib/runtime.ts:195:  if (flags.compactionInProgress !== undefined) {
lib/runtime.ts:196:    state.compactionInProgress = flags.compactionInProgress;
lib/runtime.ts:247:  return state.compactionInProgress;
lib/runtime.ts:254:  state.compactionInProgress = inProgress;

Command exited with code 2


/**
 * Regression tests for Telegram command helpers
 * Covers slash-command normalization, bot suffix stripping, arguments, and non-command input
 */

import assert from "node:assert/strict";
import test from "node:test";

import {
  buildTelegramAppMenuHtml,
  buildTelegramCommandAction,
  isTelegramReservedCommandName,
  formatTelegramCommandEmojiPrefix,
  createTelegramAppMenuHtmlBuilder,
  createTelegramBotCommandRegistrar,
  createTelegramCommandControlEnqueueAdapter,
  createTelegramCommandControlQueueRuntime,
  createTelegramCommandHandler,
  createTelegramCommandHandlerTargetRuntime,
  createTelegramCommandOrPromptRuntime,
  createTelegramCommandTargetQueueRuntime,
  createTelegramCommandTargetRuntime,
  executeTelegramCommandAction,
  getTelegramCommandExecutionMode,
  getTelegramCommandMessageTarget,
  handleTelegramCompactCommand,
  handleTelegramModelCommand,
  handleTelegramStatusCommand,
  handleTelegramStopCommand,
  parseTelegramCommand,
  registerTelegramBotCommands,
  registerTelegramBridgeCommands,
  TELEGRAM_APP_MENU_INTRO_HTML,
  TELEGRAM_BOT_COMMANDS,
  TELEGRAM_COMMAND_ACTIONS,
  TELEGRAM_COMMAND_EMOJI,
  TELEGRAM_RESERVED_COMMAND_NAMES,
} from "../lib/commands.ts";
import type { ExtensionAPI, ExtensionCommandContext } from "../lib/pi.ts";

type RegisteredBridgeCommand = {
  handler: (args: string, ctx: ExtensionCommandContext) => Promise<void> | void;
};

function createCommandRegistrationApiHarness() {
  const commands = new Map<string, RegisteredBridgeCommand>();
  const api = {
    registerCommand: (name: string, definition: RegisteredBridgeCommand) => {
      commands.set(name, definition);
    },
  } as unknown as ExtensionAPI;
  return { api, commands };
}

function getRequiredCommand(
  commands: Map<string, RegisteredBridgeCommand>,
  name: string,
): RegisteredBridgeCommand {
  const command = commands.get(name);
  assert.ok(command, `Expected command ${name}`);
  return command;
}

function createBridgeCommandContext(
  notify: (message: string) => void = () => {},
  confirm: () => Promise<boolean> | boolean = () => false,
  select?: (title: string, items: string[]) => Promise<string | undefined>,
): ExtensionCommandContext {
  return {
    cwd: "/repo",
    ui: {
      notify,
      confirm,
      select,
      theme: {
        fg: (_color: string, value: string) => value,
      },
    },
  } as unknown as ExtensionCommandContext;
}

test("Command helpers expose Telegram bot command definitions", () => {
  assert.deepEqual(TELEGRAM_COMMAND_EMOJI.model, "🤖");
  assert.deepEqual(TELEGRAM_COMMAND_EMOJI.thinking, "🧠");
  assert.equal(formatTelegramCommandEmojiPrefix("model"), "🤖 ");
  const expectedBuiltins = [
    {
      command: "start",
      description: "🟢 Open menu / Pair bridge",
    },
    { command: "compact", description: "🗜 Compact current session" },
    { command: "new", description: "🆕 Start new clean session" },
    {
      command: "next",
      description: "⏩ Force next turn",
    },
    {
      command: "continue",
      description: "▶ Queue continue prompt",
    },
    {
      command: "abort",
      description: "⏹ Abort π",
    },
    {
      command: "stop",
      description: "🟥 Abort π & Clear queue",
    },
  ];
  assert.deepEqual(TELEGRAM_BOT_COMMANDS, expectedBuiltins);
});

test("Command helpers register Telegram bot commands through deps", async () => {
  const calls: unknown[] = [];
  await registerTelegramBotCommands({
    setMyCommands: async (commands) => {
      calls.push(commands);
    },
  });
  await createTelegramBotCommandRegistrar({
    setMyCommands: async (commands) => {
      calls.push(commands);
    },
  })();
  assert.deepEqual(calls, [TELEGRAM_BOT_COMMANDS, TELEGRAM_BOT_COMMANDS]);
});

test("Command helpers register pi setup and status commands", async () => {
  const harness = createCommandRegistrationApiHarness();
  const events: string[] = [];
  registerTelegramBridgeCommands(harness.api, {
    promptForConfig: async () => {
      events.push("setup");
    },
    getStatusLines: () => ["bot: @demo", "polling: stopped"],
    reloadConfig: async () => {
      events.push("reload");
    },
    hasBotToken: () => false,
    startPolling: async () => {
      events.push("start");
    },
    stopPolling: async () => {
      events.push("stop");
    },
    updateStatus: () => {
      events.push("update-status");
    },
  });
  const notifications: string[] = [];
  const ctx = createBridgeCommandContext((message) => {
    notifications.push(message);
  });
  await getRequiredCommand(harness.commands, "telegram-setup").handler("", ctx);
  await getRequiredCommand(harness.commands, "telegram-status").handler(
    "",
    ctx,
  );
  assert.deepEqual(events, ["setup"]);
  assert.deepEqual(notifications, ["bot: @demo\npolling: stopped"]);
});

test("Command helpers register terminal settings command", async () => {
  const harness = createCommandRegistrationApiHarness();
  const events: string[] = [];
  let proactive = false;
  registerTelegramBridgeCommands(harness.api, {
    promptForConfig: async () => {},
    getStatusLines: () => [],
    reloadConfig: async () => {
      events.push("reload");
    },
    hasBotToken: () => true,
    startPolling: async () => {},
    stopPolling: async () => {},
    updateStatus: () => {
      events.push("status");
    },
    isProactivePushEnabled: () => proactive,
    setProactivePushEnabled: async (enabled) => {
      proactive = enabled;
      events.push(`proactive:${enabled}`);
    },
  });
  const notifications: string[] = [];
  const ctx = createBridgeCommandContext(
    (message) => notifications.push(message),
    () => false,
    async (_title, items) => items[0],
  );
  await getRequiredCommand(harness.commands, "telegram-settings").handler(
    "",
    ctx,
  );
  assert.equal(proactive, true);
  assert.deepEqual(events, ["reload", "proactive:true", "status"]);
  assert.deepEqual(notifications, ["Proactive push enabled."]);
});

test("Command helpers register pi connect and disconnect commands", async () => {
  const harness = createCommandRegistrationApiHarness();
  const events: string[] = [];
  let hasToken = false;
  registerTelegramBridgeCommands(harness.api, {
    promptForConfig: async () => {
      events.push("setup");
    },
    getStatusLines: () => [],
    reloadConfig: async () => {
      events.push("reload");
    },
    hasBotToken: () => hasToken,
    startPolling: async () => {
      events.push("start");
    },
    stopPolling: async () => {
      events.push("stop");
    },
    updateStatus: () => {
      events.push("update-status");

[830 more lines in file. Use offset=221 to continue.]


/**
 * Telegram bridge extension entrypoint and orchestration layer
 * Zones: telegram, pi agent, orchestration
 * Keeps the runtime wiring in one place while delegating reusable domain logic to /lib modules
 */

import * as Api from "./lib/api.ts";
import * as CommandTemplates from "./lib/command-templates.ts";
import * as Commands from "./lib/commands.ts";
import * as Config from "./lib/config.ts";
import { createTelegramExternalHandleUpdate } from "./lib/external-handlers.ts";
import * as InboundHandlers from "./lib/inbound-handlers.ts";
import * as Keyboard from "./lib/keyboard.ts";
import * as Lifecycle from "./lib/lifecycle.ts";
import * as Locks from "./lib/locks.ts";
import * as Media from "./lib/media.ts";
import * as MenuQueue from "./lib/menu-queue.ts";
import * as MenuSettings from "./lib/menu-settings.ts";
import * as Menu from "./lib/menu.ts";
import * as Model from "./lib/model.ts";
import * as OutboundAttachments from "./lib/outbound-attachments.ts";
import * as OutboundHandlers from "./lib/outbound-handlers.ts";
import * as Pi from "./lib/pi.ts";
import * as Polling from "./lib/polling.ts";
import * as Preview from "./lib/preview.ts";
import * as PromptTemplates from "./lib/prompt-templates.ts";
import * as Prompts from "./lib/prompts.ts";
import * as Queue from "./lib/queue.ts";
import * as Replies from "./lib/replies.ts";
import * as Routing from "./lib/routing.ts";
import * as Runtime from "./lib/runtime.ts";
import * as Setup from "./lib/setup.ts";
import * as Status from "./lib/status.ts";
import * as TextGroups from "./lib/text-groups.ts";

type ActivePiModel = NonNullable<Pi.ExtensionContext["model"]>;
type RuntimeTelegramQueueItem = Queue.TelegramQueueItem<Pi.ExtensionContext>;

// --- Extension Runtime ---

export default function (pi: Pi.ExtensionAPI) {
  const piRuntime = Pi.createExtensionApiRuntimePorts(pi);
  const {
    getCommands,
    getThinkingLevel,
    sendUserMessage,
    setModel,
    setThinkingLevel,
  } = piRuntime;
  const bridgeRuntime = Runtime.createTelegramBridgeRuntime();
  const { abort, lifecycle, queue, setup, typing } = bridgeRuntime;
  const configStore = Config.createTelegramConfigStore();
  const isProactivePushEnabled =
    Config.createTelegramProactivePushChecker(configStore);
  const setProactivePushEnabled =
    Config.createTelegramProactivePushSetter(configStore);
  const lockRuntime = Locks.createTelegramLockRuntime<Pi.ExtensionContext>();
  const lockOwnershipGuard =
    Locks.createTelegramLockOwnershipGuard(lockRuntime);
  const activeTurnRuntime = Queue.createTelegramActiveTurnStore();
  const proactivePushChatIdGetter =
    Config.createTelegramProactivePushChatIdGetter({
      getActiveTurnChatId: activeTurnRuntime.getChatId,
      getAllowedUserId: configStore.getAllowedUserId,
    });
  const buttonActionStore = OutboundHandlers.createTelegramButtonActionStore();
  const pendingModelSwitchStore =
    Model.createPendingModelSwitchStore<
      Model.ScopedTelegramModel<ActivePiModel>
    >();
  const modelMenuRuntime = Menu.createTelegramModelMenuRuntime<ActivePiModel>();
  const runtimeEvents = Status.createTelegramRuntimeEventRecorder({
    getBotToken: configStore.getBotToken,
  });
  const recordRuntimeEvent = runtimeEvents.record;
  const getContextModel = Pi.getExtensionContextModel;
  const isIdle = Pi.isExtensionContextIdle;
  const hasPendingMessages = Pi.hasExtensionContextPendingMessages;
  const compact = Pi.compactExtensionContext;
  const mediaGroupRuntime = Media.createTelegramMediaGroupController<
    Api.TelegramMessage,
    Pi.ExtensionContext
  >();
  const textGroupRuntime = TextGroups.createTelegramTextGroupController<
    Api.TelegramMessage,
    Pi.ExtensionContext
  >();
  const telegramQueueStore =
    Queue.createTelegramQueueStore<Pi.ExtensionContext>();
  const deferredQueueDispatchRuntime =
    Queue.createTelegramDeferredQueueDispatchRuntime<Pi.ExtensionContext>({
      recordRuntimeEvent,
    });
  const pollingControllerState = Polling.createTelegramPollingControllerState();
  const { getStatusLines, updateStatus } =
    Status.createTelegramBridgeStatusRuntime<
      Pi.ExtensionContext,
      RuntimeTelegramQueueItem
    >({
      getConfig: configStore.get,
      isPollingActive: Polling.createTelegramPollingActivityReader(
        pollingControllerState,
      ),
      getActiveSourceMessageIds: activeTurnRuntime.getSourceMessageIds,
      hasActiveTurn: activeTurnRuntime.has,
      hasDispatchPending: lifecycle.hasDispatchPending,
      isCompactionInProgress: lifecycle.isCompactionInProgress,
      getActiveToolExecutions: lifecycle.getActiveToolExecutions,
      hasPendingModelSwitch: pendingModelSwitchStore.has,
      getQueuedItems: telegramQueueStore.getQueuedItems,
      formatQueuedStatus: Queue.formatQueuedTelegramItemsStatus,
      getRecentRuntimeEvents: runtimeEvents.getEvents,
      getRuntimeLockState: lockRuntime.getStatusLabel,
    });
  const currentModelRuntime = Model.createCurrentModelRuntime<
    Pi.ExtensionContext,
    ActivePiModel
  >({
    getContextModel,
    updateStatus,
  });
  const queueMutationRuntime =
    Queue.createTelegramQueueMutationController<Pi.ExtensionContext>({
      ...telegramQueueStore,
      getNextPriorityReactionOrder: queue.getNextPriorityReactionOrder,
      incrementNextPriorityReactionOrder:
        queue.incrementNextPriorityReactionOrder,
      updateStatus,
    });
  const inboundHandlerRuntime =
    InboundHandlers.createTelegramInboundHandlerRuntime<Pi.ExtensionContext>({
      getHandlers: configStore.getInboundHandlers,
      execCommand: CommandTemplates.execCommandTemplate,
      getCwd: Pi.getExtensionContextCwd,
      recordRuntimeEvent,
    });

  // --- Telegram API ---

  const {
    callMultipart,
    deleteWebhook,
    getUpdates,
    setMyCommands,
    sendTypingAction,
    sendMessageDraft,
    sendMessage,
    downloadFile: downloadTelegramBridgeFile,
    editMessageText: editTelegramMessageText,
    answerCallbackQuery,
    prepareTempDir,
  } = Api.createDefaultTelegramBridgeApiRuntime({
    getBotToken: configStore.getBotToken,
    recordRuntimeEvent,
  });

  // --- Message Delivery & Preview ---

  const promptDispatchRuntime =
    Runtime.createTelegramPromptDispatchRuntime<Pi.ExtensionContext>({
      lifecycle,
      typing,
      getDefaultChatId: activeTurnRuntime.getChatId,
      sendTypingAction,
      updateStatus,
      recordRuntimeEvent,
    });

  // --- Reply Runtime Wiring ---

  const replyRuntime =
    Replies.createTelegramRenderedMessageDeliveryRuntime<Keyboard.TelegramInlineKeyboardMarkup>(
      {
        sendMessage,
        editMessage: editTelegramMessageText,
      },
    );
  const { replyTransport, editInteractiveMessage, sendInteractiveMessage } =
    replyRuntime;
  const { sendTextReply, sendMarkdownReply } =
    OutboundHandlers.createTelegramOutboundTextReplyRuntime({
      sendTextReply: replyRuntime.sendTextReply,
      sendMarkdownReply: replyRuntime.sendMarkdownReply,
      execCommand: CommandTemplates.execCommandTemplate,
      getHandlers: configStore.getOutboundHandlers,
      recordRuntimeEvent,
    });
  const dispatchNextQueuedTelegramTurn =
    Queue.createTelegramQueueDispatchRuntime<Pi.ExtensionContext>({
      ...telegramQueueStore,
      isCompactionInProgress: lifecycle.isCompactionInProgress,
      hasActiveTurn: activeTurnRuntime.has,
      hasDispatchPending: lifecycle.hasDispatchPending,
      isIdle,
      hasPendingMessages,
      hasDispatchContext: deferredQueueDispatchRuntime.isBound,
      updateStatus,
      sendTextReply,
      recordRuntimeEvent,
      ...promptDispatchRuntime,
      sendUserMessage,
    }).dispatchNext;
  const previewRuntime = Preview.createTelegramAssistantPreviewRuntime<
    unknown,
    Keyboard.TelegramInlineKeyboardMarkup
  >({
    getActiveTurn: activeTurnRuntime.get,
    isAssistantMessage: Replies.isAssistantAgentMessage,
    getMessageText: Replies.getAgentMessageText,
    getDefaultReplyToMessageId: activeTurnRuntime.getReplyToMessageId,
    sendDraft: sendMessageDraft,
    sendMessage,
    editMessageText: editTelegramMessageText,
    canSend: lockOwnershipGuard.ownsCurrentProcess,
    ...replyTransport,
  });
  const { finalizeMarkdownPreview } =
    OutboundHandlers.createTelegramOutboundTextPreviewRuntime({
      finalizeMarkdownPreview: previewRuntime.finalizeMarkdown,
      execCommand: CommandTemplates.execCommandTemplate,

[304 more lines in file. Use offset=221 to continue.]


/**
 * Telegram command routing helpers
 * Zones: telegram controls, pi agent commands, queue controls
 * Owns Telegram slash-command normalization, bot command metadata, and pi-side command registration behind runtime ports
 */

import { pairTelegramUserIfNeeded } from "./config.ts";
import type { ExtensionAPI, ExtensionCommandContext } from "./pi.ts";
import {
  createTelegramControlItemBuilder,
  createTelegramControlQueueController,
  type PendingTelegramControlItem,
} from "./queue.ts";

export interface ParsedTelegramCommand {
  name: string;
  args: string;
}

export interface TelegramBotCommandDefinition {
  command: string;
  description: string;
}

export interface TelegramPromptTemplateMenuCommand {
  command: string;
  description?: string;
}

export const TELEGRAM_COMMAND_EMOJI = {
  start: "🟢",
  status: "📊",
  model: "🤖",
  thinking: "🧠",
  compact: "🗜",
  new: "🆕",
  queue: "🔢",
  next: "⏩",
  continue: "▶",
  abort: "⏹",
  stop: "🟥",
} as const;

export type TelegramCommandEmojiName = keyof typeof TELEGRAM_COMMAND_EMOJI;

export function getTelegramCommandEmoji(
  command: TelegramCommandEmojiName,
): string {
  return TELEGRAM_COMMAND_EMOJI[command];
}

export function formatTelegramCommandEmojiPrefix(
  command: TelegramCommandEmojiName,
): string {
  return `${getTelegramCommandEmoji(command)} `;
}

function formatTelegramBotCommandDescription(
  command: TelegramCommandEmojiName,
  description: string,
): string {
  return `${formatTelegramCommandEmojiPrefix(command)}${description}`;
}

export const TELEGRAM_BUILTIN_BOT_COMMANDS: readonly TelegramBotCommandDefinition[] =
  [
    {
      command: "start",
      description: formatTelegramBotCommandDescription(
        "start",
        "Open menu / Pair bridge",
      ),
    },
    {
      command: "compact",
      description: formatTelegramBotCommandDescription(
        "compact",
        "Compact current session",
      ),
    },
    {
      command: "new",
      description: formatTelegramBotCommandDescription(
        "new",
        "Start new clean session",
      ),
    },
    {
      command: "next",
      description: formatTelegramBotCommandDescription(
        "next",
        "Force next turn",
      ),
    },
    {
      command: "continue",
      description: formatTelegramBotCommandDescription(
        "continue",
        "Queue continue prompt",
      ),
    },
    {
      command: "abort",
      description: formatTelegramBotCommandDescription("abort", "Abort π"),
    },
    {
      command: "stop",
      description: formatTelegramBotCommandDescription(
        "stop",
        "Abort π & Clear queue",
      ),
    },
  ];

export const TELEGRAM_BOT_COMMANDS = TELEGRAM_BUILTIN_BOT_COMMANDS;

export interface TelegramBotCommandRegistrationDeps {
  setMyCommands: (
    commands: readonly TelegramBotCommandDefinition[],
  ) => Promise<unknown>;
}

export async function registerTelegramBotCommands(
  deps: TelegramBotCommandRegistrationDeps,
): Promise<void> {
  await deps.setMyCommands(TELEGRAM_BOT_COMMANDS);
}

export function createTelegramBotCommandRegistrar(
  deps: TelegramBotCommandRegistrationDeps,
): () => Promise<void> {
  return () => registerTelegramBotCommands(deps);
}

export interface TelegramBridgeCommandStartPollingOptions {
  force?: boolean;
}

export interface TelegramBridgeCommandStartPollingResult {
  ok: boolean;
  message?: string;
  canTakeover?: boolean;
  owner?: string;
}

interface TelegramBridgeSettingsSelectUi {
  select?: (title: string, items: string[]) => Promise<string | undefined>;
}

export interface TelegramBridgeCommandRegistrationDeps {
  promptForConfig: (ctx: ExtensionCommandContext) => Promise<void>;
  getStatusLines: () => string[];
  reloadConfig: () => Promise<void>;
  hasBotToken: () => boolean;
  startPolling: (
    ctx: ExtensionCommandContext,
    options?: TelegramBridgeCommandStartPollingOptions,
  ) =>
    | void
    | Promise<void | TelegramBridgeCommandStartPollingResult>
    | TelegramBridgeCommandStartPollingResult;
  stopPolling: () => Promise<void | string>;
  updateStatus: (ctx: ExtensionCommandContext) => void;
  isProactivePushEnabled?: () => boolean;
  setProactivePushEnabled?: (enabled: boolean) => Promise<void>;
}

function formatTelegramTakeoverTitle(ctx: ExtensionCommandContext): string {
  return ctx.ui.theme.fg("accent", "pi-telegram");
}

function formatTelegramTakeoverPrompt(
  ctx: ExtensionCommandContext,
  owner?: string,
): string {
  const theme = ctx.ui.theme;
  const action = theme.fg("warning", "move singleton lock here?");
  const from = theme.fg("muted", "from:");
  const to = theme.fg("muted", "to:");
  const source = owner ?? "another π instance";
  return `${action}\n\n${from} ${source}\n${to} ${ctx.cwd}`;
}

export function registerTelegramBridgeCommands(
  pi: ExtensionAPI,
  deps: TelegramBridgeCommandRegistrationDeps,
): void {
  pi.registerCommand("telegram-setup", {
    description: "Configure Telegram bot token",
    handler: async (_args, ctx) => {
      await deps.promptForConfig(ctx);
    },
  });
  pi.registerCommand("telegram-status", {
    description: "Show Telegram bridge status",
    handler: async (_args, ctx) => {
      ctx.ui.notify(deps.getStatusLines().join("\n"), "info");
    },
  });
  pi.registerCommand("telegram-settings", {
    description: "Open Telegram bridge settings",
    handler: async (_args, ctx) => {
      if (!deps.isProactivePushEnabled || !deps.setProactivePushEnabled) {
        ctx.ui.notify("Telegram settings are unavailable.", "warning");
        return;
      }
      await deps.reloadConfig();
      const enabled = deps.isProactivePushEnabled();
      const nextEnabled = !enabled;
      const label = `${enabled ? "🟢" : "⚫"} Proactive push`;
      const action = `${nextEnabled ? "Enable" : "Disable"} proactive push`;
      const select = (ctx.ui as TelegramBridgeSettingsSelectUi).select;
      if (!select) {
        ctx.ui.notify(
          `${label}\n${action}: /telegram-settings requires interactive mode.`,
          "info",
        );
        return;
      }
      const selected = await select("Telegram settings", [label, "Cancel"]);
      if (selected !== label) return;
      await deps.setProactivePushEnabled(nextEnabled);
      deps.updateStatus(ctx);
      ctx.ui.notify(
        `Proactive push ${nextEnabled ? "enabled" : "disabled"}.`,
        "info",
      );
    },
  });
  pi.registerCommand("telegram-connect", {
    description: "Start the Telegram bridge in this π session",
    handler: async (_args, ctx) => {
      await deps.reloadConfig();
      if (!deps.hasBotToken()) {
        await deps.promptForConfig(ctx);
        return;
      }
      let result = await deps.startPolling(ctx);
      if (result && !result.ok && result.canTakeover) {
        const confirmed = await ctx.ui.confirm(
          formatTelegramTakeoverTitle(ctx),
          formatTelegramTakeoverPrompt(ctx, result.owner),
        );
        if (!confirmed) {
          ctx.ui.notify("Telegram bridge takeover cancelled.", "info");
          deps.updateStatus(ctx);
          return;
        }
        result = await deps.startPolling(ctx, { force: true });
      }
      if (result?.message) {
        ctx.ui.notify(result.message, result.ok ? "info" : "warning");
      }
      deps.updateStatus(ctx);
    },
  });
  pi.registerCommand("telegram-disconnect", {
    description: "Stop the Telegram bridge in this π session",
    handler: async (_args, ctx) => {
      const message = await deps.stopPolling();

[940 more lines in file. Use offset=261 to continue.]


      if (message) ctx.ui.notify(message, "info");
      deps.updateStatus(ctx);
    },
  });
}

export const TELEGRAM_RESERVED_COMMAND_NAMES = [
  "stop",
  "abort",
  "next",
  "continue",
  "status",
  "queue",
  "compact",
  "new",
  "model",
  "thinking",
  "settings",
  "help",
  "start",
] as const;

export type TelegramReservedCommandName =
  (typeof TELEGRAM_RESERVED_COMMAND_NAMES)[number];

const TELEGRAM_RESERVED_COMMAND_NAME_SET = new Set<string>(
  TELEGRAM_RESERVED_COMMAND_NAMES,
);

export function isTelegramReservedCommandName(
  commandName: string | undefined,
): commandName is TelegramReservedCommandName {
  return (
    commandName !== undefined &&
    TELEGRAM_RESERVED_COMMAND_NAME_SET.has(commandName)
  );
}

export type TelegramCommandAction =
  | { kind: "ignore"; executionMode: "ignored" }
  | { kind: "stop"; executionMode: "immediate" }
  | { kind: "abort"; executionMode: "immediate" }
  | { kind: "next"; executionMode: "immediate" }
  | { kind: "continue"; executionMode: "immediate" }
  | { kind: "queue"; executionMode: "immediate" }
  | { kind: "compact"; executionMode: "immediate" }
  | { kind: "new"; executionMode: "immediate" }
  | { kind: "status"; executionMode: "immediate" }
  | { kind: "model"; executionMode: "immediate" }
  | { kind: "thinking"; executionMode: "immediate" }
  | { kind: "settings"; executionMode: "immediate" }
  | {
      kind: "help";
      commandName: "help" | "start";
      executionMode: "immediate";
    };

export type TelegramCommandExecutionMode = "ignored" | "immediate";

export interface TelegramCommandActionDeps<TMessage, TContext> {
  handleStop: (message: TMessage, ctx: TContext) => Promise<void>;
  handleAbort: (message: TMessage, ctx: TContext) => Promise<void>;
  handleNext: (message: TMessage, ctx: TContext) => Promise<void>;
  handleContinue: (message: TMessage, ctx: TContext) => Promise<void>;
  handleQueue: (message: TMessage, ctx: TContext) => Promise<void>;
  handleCompact: (message: TMessage, ctx: TContext) => Promise<void>;
  handleNew: (message: TMessage, ctx: TContext) => Promise<void>;
  handleStatus: (message: TMessage, ctx: TContext) => Promise<void>;
  handleModel: (message: TMessage, ctx: TContext) => Promise<void>;
  handleThinking: (message: TMessage, ctx: TContext) => Promise<void>;
  handleSettings?: (message: TMessage, ctx: TContext) => Promise<void>;
  handleHelp: (
    message: TMessage,
    commandName: "help" | "start",
    ctx: TContext,
  ) => Promise<void>;
}

export interface TelegramStopCommandDeps {
  hasAbortHandler: () => boolean;
  clearPendingModelSwitch: () => void;
  clearQueuedTelegramItems: () => number;
  setPreserveQueuedTurnsAsHistory: (preserve: boolean) => void;
  abortCurrentTurn: () => void;
  updateStatus: () => void;
  sendTextReply: (text: string) => Promise<void>;
}

export interface TelegramRuntimeEventRecorderPort {
  recordRuntimeEvent?: (
    category: string,
    error: unknown,
    details?: Record<string, unknown>,
  ) => void;
  sendUserMessage?: (
    message: string,
    options?: { deliverAs?: "steer" | "followUp" },
  ) => void;
}

export interface TelegramCompactCommandDeps extends TelegramRuntimeEventRecorderPort {
  isIdle: () => boolean;
  hasPendingMessages: () => boolean;
  hasActiveTelegramTurn: () => boolean;
  hasDispatchPending: () => boolean;
  hasQueuedTelegramItems: () => boolean;
  isCompactionInProgress: () => boolean;
  setCompactionInProgress: (inProgress: boolean) => void;
  updateStatus: () => void;
  dispatchNextQueuedTelegramTurn: () => void;
  compact: (callbacks: {
    onComplete: () => void;
    onError: (error: unknown) => void;
  }) => void;
  sendTextReply: (text: string) => Promise<void>;
}

export type TelegramControlCommandType =
  PendingTelegramControlItem<unknown>["controlType"];

export interface TelegramCommandRuntimeMessage {
  chat: { id: number };
  message_id: number;
  from?: { id?: number };
}

export interface TelegramCommandMessageTarget {
  chatId: number;
  replyToMessageId: number;
}

export interface TelegramCommandTargetRuntimeDeps<TContext> {
  enqueueControlItem: (
    target: TelegramCommandMessageTarget,
    ctx: TContext,
    controlType: TelegramControlCommandType,
    statusSummary: string,
    execute: (ctx: TContext) => Promise<void>,
  ) => void;
  showStatus: (
    chatId: number,
    replyToMessageId: number,
    ctx: TContext,
  ) => Promise<void>;
  openModelMenu: (
    chatId: number,
    replyToMessageId: number,
    ctx: TContext,
  ) => Promise<void>;
  openSettingsMenu?: (
    chatId: number,
    replyToMessageId: number,
    ctx: TContext,
  ) => Promise<void>;
  sendTextReply: (
    chatId: number,
    replyToMessageId: number,
    text: string,
  ) => Promise<unknown>;
}

export interface TelegramCommandTargetRuntime<
  TMessage extends TelegramCommandRuntimeMessage,
  TContext,
> {
  enqueueControlItem: (
    message: TMessage,
    ctx: TContext,
    controlType: TelegramControlCommandType,
    statusSummary: string,
    execute: (ctx: TContext) => Promise<void>,
  ) => void;
  showStatus: (message: TMessage, ctx: TContext) => Promise<void>;
  openModelMenu: (message: TMessage, ctx: TContext) => Promise<void>;
  openSettingsMenu: (message: TMessage, ctx: TContext) => Promise<void>;
  sendTextReply: (message: TMessage, text: string) => Promise<void>;
}

export function getTelegramCommandMessageTarget(
  message: TelegramCommandRuntimeMessage,
): TelegramCommandMessageTarget {
  return {
    chatId: message.chat.id,
    replyToMessageId: message.message_id,
  };
}

export interface TelegramCommandControlQueueRuntimeDeps<TContext> {
  createControlItem: (options: {
    chatId: number;
    replyToMessageId: number;
    controlType: TelegramControlCommandType;
    statusSummary: string;
    execute: (ctx: TContext) => Promise<void>;
  }) => PendingTelegramControlItem<TContext>;
  appendControlItem: (
    item: PendingTelegramControlItem<TContext>,
    ctx: TContext,
  ) => void;
  dispatchNextQueuedTelegramTurn: (ctx: TContext) => void;
}

export function createTelegramCommandControlQueueRuntime<TContext>(
  deps: TelegramCommandControlQueueRuntimeDeps<TContext>,
): TelegramCommandTargetRuntimeDeps<TContext>["enqueueControlItem"] {
  const controlQueueController = createTelegramControlQueueController({
    appendControlItem: deps.appendControlItem,
    dispatchNextQueuedTelegramTurn: deps.dispatchNextQueuedTelegramTurn,
  });
  return createTelegramCommandControlEnqueueAdapter({
    createControlItem: deps.createControlItem,
    enqueueControlItem: controlQueueController.enqueue,
  });
}

export function createTelegramCommandControlEnqueueAdapter<TContext>(deps: {
  createControlItem: (options: {
    chatId: number;
    replyToMessageId: number;
    controlType: TelegramControlCommandType;
    statusSummary: string;
    execute: (ctx: TContext) => Promise<void>;
  }) => PendingTelegramControlItem<TContext>;
  enqueueControlItem: (
    item: PendingTelegramControlItem<TContext>,
    ctx: TContext,
  ) => void;
}): TelegramCommandTargetRuntimeDeps<TContext>["enqueueControlItem"] {
  return (target, ctx, controlType, statusSummary, execute) => {
    deps.enqueueControlItem(
      deps.createControlItem({
        ...target,
        controlType,
        statusSummary,
        execute,
      }),
      ctx,
    );
  };
}

export type TelegramCommandTargetQueueRuntimeDeps<TContext> =
  TelegramCommandControlQueueRuntimeDeps<TContext> &
    Omit<TelegramCommandTargetRuntimeDeps<TContext>, "enqueueControlItem">;

export function createTelegramCommandTargetQueueRuntime<
  TMessage extends TelegramCommandRuntimeMessage,
  TContext,
>(
  deps: TelegramCommandTargetQueueRuntimeDeps<TContext>,
): TelegramCommandTargetRuntime<TMessage, TContext> {
  return createTelegramCommandTargetRuntime({
    enqueueControlItem: createTelegramCommandControlQueueRuntime({
      createControlItem: deps.createControlItem,
      appendControlItem: deps.appendControlItem,
      dispatchNextQueuedTelegramTurn: deps.dispatchNextQueuedTelegramTurn,
    }),
    showStatus: deps.showStatus,
    openModelMenu: deps.openModelMenu,
    openSettingsMenu: deps.openSettingsMenu,

[680 more lines in file. Use offset=521 to continue.]