Two real bugs surfaced after the original retry helper:
1. Healthy long-polls were tripping our 15s per-attempt timeout. The
getUpdates request asks Telegram for a 30s server-side long-poll, so
our internal timeout aborted every healthy connection and turned it
into ABORT_ERR -> retry -> exhausted -> "disconnected", with no
auto-reconnect. The fix: long-poll bypasses the retry helper and uses
a 60s per-attempt timeout, since the poll loop already retries by
re-entering after sleep().
2. Our own internal AbortController timeout produced a DOMException
AbortError indistinguishable from a caller-abort. The poll loop's
shouldStopTelegramPolling treated that as "user wants to stop" and
exited. Now fetchWithRetry normalizes its own timeout into a tagged
Error with code ATTEMPT_TIMEOUT, so only real caller-aborts surface
as AbortError upstream.
Also: per-attempt timeout default dropped 15s -> 5s, retry budget
dropped from [500, 2000] to [500] (so 2 attempts, not 3) for outbound
sends, since they serialize and a long retry tail makes the bridge feel
hung.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
/help now re-runs setMyCommands so late-registered pi commands (skills,
prompts, extensions that registered after /start) become discoverable
without restarting the bridge, and prints the live pi.getCommands() list
so the user can see what's actually wired up right now.
Addresses the symptom where the Telegram command menu sometimes shows
only the bridge-local commands - a registration-order race against the
initial setMyCommands at /start.
Wrap fetch calls in callTelegram, callTelegramMultipart, and
downloadTelegramFile with bounded retry (max 3 attempts, 500ms/2000ms
backoff with jitter) on transient network codes and a per-attempt 15s
AbortController timeout so a stuck connection can't wedge the bridge
indefinitely. HTTP 4xx/5xx still surface as before.
Add a process-level unhandledRejection handler so a stray rejection from
a fire-and-forget void f() (e.g. timer-driven preview flush) can't crash
the host under Node 22's default unhandledRejection=throw - which is how
a remote session got dropped. Also tighten the two timer-callback voids
that can fire after their turn's try/catch is gone.
- Add search/filtering to the `/model` command with multi-word matching
- Finalize partial stream previews (e.g. thinking blocks) on turn abort instead of clearing them
- Dynamically format low-cost `$ value` metrics up to 5 decimal places in status outputs
- Update queue tests to expect text-turn plans for aborted turns with partial text
Tool call blocks (and thinking) arrive in streaming updates before their
args are populated — emitting immediately gave `Tool call bash {}`.
Buffer non-text blocks during onMessageUpdate; flush them in
onMessageStart (previous message now complete, args fully populated)
and onAgentEnd (single-message turns that never trigger onMessageStart).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Each pi output block (thinking, tool_call, tool_result) emits a
separate Telegram message as it arrives; text content still streams
in a single edit-in-place preview message
- Replace traceVisible bool with displayMode: "full" | "compact" | "text"
(/trace cycles through all three; compact is default)
- Add renderBlockMessage() to lib/rendering.ts; delete old transcript
aggregation helpers (buildTelegramAssistantTranscriptMarkdown, etc.)
- registerTelegramBotCommands now pulls extension commands from
pi.getCommands() in addition to local bot commands
- Update rendering tests for new block rendering API
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The first-DM auto-pair behavior combined with ! shell passthrough meant
the first account to DM the bot gained arbitrary shell access. This
removes that footgun entirely.
- allowedUserId must be set before polling starts; missing config blocks
polling with a TUI warning rather than silently accepting any sender
- TELEGRAM_ALLOWED_USER_ID env var is read on session start and
overwrites the saved config value, so rotating the allowed user is a
restart away
- /telegram-setup now prompts for a numeric user ID after the bot token
if one is not already configured
- Denied senders receive an auth error reply; their numeric ID is also
logged to the pi TUI as a warning so operators can identify themselves
on a fresh install without needing @userinfobot
- Dropped the {kind: "pair"} authorization state entirely; undefined
allowedUserId now produces deny, not pair
- Removed pairTelegramUserIfNeeded, shouldPair, shouldNotifyPaired
Existing installs with allowedUserId already in telegram.json are
unaffected. Fresh installs require explicit configuration.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>