feat: add Telegram chat ingress (telegrambot)#1081
Draft
murderteeth wants to merge 14 commits into
Draft
Conversation
Register a new services/telegrambot Bun/TypeScript workspace package mirroring discordbot's layout (package.json scripts, tsconfig, slim bun Dockerfile, README, AGENTS.md), plus the shared types contract and ported utils. Wire the service into pnpm-workspace.yaml, the Justfile build/tag/deploy targets, and a Telegrambot typecheck-and-tests CI job with its own change-detection filter. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Bot API client embeds the token in every URL path, so errors are rebuilt from the method name plus the API description and never carry a request URL. Message acceptance is fail-closed (empty chat/user allowlists render the bot inert; DMs are per-user allowlisted since Telegram has no workspace boundary), with replies to the bot and addressed /ask commands as the privacy-mode group trigger contract. Thread identity encodes the chat kind in typed durable keys (telegram:chat:/telegram:private:) with forum-topic preservation and strict private-chat identity validation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Port discordbot's session-api module: create/append/execute with stable client_message_id idempotency keys, SSE replay from after_event_id with retryable-error classification, steering event detection for active-execution follow-ups, and attachment ingest via getFile with conservative count/size caps. Create-session metadata carries platform/thread routing context and telegram_conversation_name on every create since api-rs re-upserts the principal each time. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The renderer translates harness Markdown into Telegram-supported HTML and splits it into independently valid chunks whose parsed text stays within the 4096-char limit, keeping pre/code entities balanced across chunk boundaries with surrogate-safe cuts, plus an escaped plain-text fallback for parse rejections. The rate limiter wraps the Bot API client with per-chat FIFO pacing (~1 msg/s per chat, 20/min group budget), method-specific 429 retry_after handling, stale-edit coalescing, and an urgent path so terminal answers outrank progress edits. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Port discordbot's append-only narrator: best-effort reactions on the triggering message (Telegram replaces the reaction set per call, so settling is a single setMessageReaction), throttled thinking blurbs as italic HTML posts capped under the message limit, and a typing keepalive for noticeably slow executions. Reaction and typing failures are classified and suppressed so cosmetic surfaces never fail delivery. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
telegram:chat:{id}[:{topic}] maps to a telegram-chat-{id} principal and
telegram:private:{id}[:{topic}] to telegram-user-{id} — the key's
discriminator, never metadata, decides the kind. Topic sessions
collapse onto the chat/user principal with the topic kept as a label,
ids validate as optionally negative integers (verbatim in the foreign
id, since slugify would collide -N with N), and no team scoping is
applied because Telegram chat ids are globally unique. Adds
telegram_conversation_name to the session principal display-name
fallback chain.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Deployment + Service gated on telegrambot.enabled: single replica with Recreate (getUpdates allows one consumer per token; the in-service fenced lease is the correctness guard), startup/liveness probes on /live and readiness on /ready so Telegram outages fail readiness without probe-induced restarts. Adds the telegrambot values block, network policy entries (api-rs/Postgres plus outbound 443 — FQDN scoping to api.telegram.org needs an egress proxy or CNI extension), and TELEGRAM_BOT_TOKEN / TELEGRAMBOT_API_KEY seeding in the secrets bootstrap. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The correctness core of the ingress: purpose-built telegram_poll_state and telegram_update_inbox tables (never the generic chat-state store), created by an advisory-lock-serialized startup migration runner with a version gate readiness can fail closed on. Receipt is separate from processing — each getUpdates batch upserts and advances receive_offset in one fenced transaction with a GREATEST guard, so processing can never gate or regress the cursor. Ownership is a lease row (holder, monotonic generation, database-time expiry); every cursor update, stage transition, and prune proves the lease in the same statement, so a paused old owner cannot mutate state after a takeover. Stage transitions are compare-and-set and terminal dispositions require a durable reason. Tests run against real Postgres via TELEGRAMBOT_TEST_DATABASE_URL and skip cleanly without it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
getUpdates controller mirroring discordbot's gateway crash-to-restart pattern: startup orders migrations -> getMe -> ownership -> deleteWebhook(preserve pending) -> poll; only the committed receive_offset feeds the next poll, with first start polling without an offset so pending updates are never dropped. Fatal 401/404/409 exits for a k8s restart; transient failures back off and surface through /ready (poll freshness, ownership, schema) while /live tracks only a local loop heartbeat. Ownership loss stops polling immediately. Includes a reusable scriptable fake Bot API server for tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
createTelegrambot composes the poller, durable dispatcher, and rendering into the running service: recovery-first dispatch through the claim scan (restarts resume oldest-first per thread before new work), fenced stage transitions received -> message_appended -> execution_accepted -> render_obligation_persisted -> terminal, 409 execute-conflicts resolving as steering with execution-terminal fallback, and streamed answer delivery that persists every message_id and delivered chunk before further edits (at-least-once terminal delivery; recovery never re-executes to redeliver). /live and /ready map the poller's health surface. server.ts mirrors discordbot's entry style. Reconciles the client-message-id derivation into telegram-threading and adds inbox.claimThreadBacklog so live steering is not starved by the one-row-per-thread claim scan. Includes the orchestration unit suite and a 7-scenario end-to-end emulate suite against a fake Bot API server, fake api-rs, and real Postgres. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Confirmed adversarial-review findings: a steering wait registered on an already-settled execution now resolves as terminal instead of wedging the follow-up and hanging shutdown; a transiently blocked older row defers the whole thread to the retry sweep instead of letting newer siblings execute first (same-thread FIFO); the steering fallback backs off exponentially and yields its worker slot rather than spinning execute conflicts once a second; answers collapse honestly at a max-messages cap (TELEGRAMBOT_ANSWER_MAX_MESSAGES, default 8) with a truncation notice; answer replies set allow_sending_without_reply so a deleted trigger message degrades to a reply-less answer instead of an infinite render retry; settle reactions use the Bot API's whitelisted emoji set (thumbs up/down — check/cross are rejected with 400); the typing keepalive refreshes under the indicator's ~5s lifetime; getFile/downloadFile honor 429 retry_after; group messages with sender_chat or is_automatic_forward (linked-channel identities) are rejected fail-closed; per-row in-memory state is dropped on terminal dispositions. Adds the spec's steering recovery suites, FIFO and settled-wait regressions, the truncation test, and the insufficient-DDL-privileges migration test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A telegram:-prefixed thread key that fails validation (e.g. telegram:chat:C123ABC) previously fell through to the Slack segment parser, which could mint a real Slack-shaped principal from the segments. Malformed telegram keys now short-circuit to the generic thread- fallback with empty labels, with regression coverage pinning that telegram:chat:D0420 can never collide with the real Slack DM principal slack-channel-d0420. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The durability core (inbox receipt, fenced ownership, migrations, poller recovery, end-to-end emulation) is gated on TELEGRAMBOT_TEST_DATABASE_URL and silently skipped without Postgres, so the telegrambot job now provisions a postgres service container (mirroring the rust-api job) and sets the URL. Adds packages/ to the telegrambot change filter and registers telegrambot in the publish-images build and manifest matrices so the ghcr image the chart references actually gets published. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds the telegrambot block to contrib/chart/values.dev.yaml (local k3s dev flow parity with the sibling bots) and lists the service in the root AGENTS.md ownership-by-tree section. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
services/telegrambot, a Telegram chat frontend with the same experience as the Slack/Discord/Teams bots: mention-driven durable agent sessions with streamed progress and answers. The service follows the chat-ingress contract inservices/AGENTS.mdand mirrors discordbot's pull model — long-pollinggetUpdates(single replica,Recreate, no public ingress) instead of webhooks.Key design points:
telegram_poll_state/telegram_update_inboxtables (startup migrations, advisory-lock serialized). EachgetUpdatesbatch upserts and advancesreceive_offsetin one fenced transaction; processing never gates receipt. Accepted updates progress idempotently throughreceived → message_appended → execution_accepted → render_obligation_persisted → terminal.replicas: 1is rollout hygiene, not the correctness mechanism.sender_chat, auto-forwards) are rejected. Group triggers are replies-to-bot and/ask@botname; plain@botnamementions are deliberately unsupported until the manual acceptance test below records a pass.<pre>/<code>across chunks), per-chat rate control honoring429 retry_after, throttled in-place edits, honest truncation at a max-messages cap, at-least-once terminal delivery via persisted render obligations (recovery may duplicate a final answer, never re-executes the agent).telegram:chat:{id}/telegram:private:{id}thread keys derivetelegram-chat-*/telegram-user-*principals (topic ids collapse to labels; malformed keys fall to the generic fallback, never another platform's parser).telegrambot.enabled,/livevs/readyprobe split (Telegram outages fail readiness only), network policy entries, secrets bootstrap, CI job with a Postgres service container, publish-images matrix entry.How to review
Suggested order:
services/telegrambot/AGENTS.md(invariants) →src/inbox.ts+src/ownership.ts(correctness core) →src/poller.ts→src/index.ts(stage machine, steering, answer delivery) →src/telegram-render.ts/rate-limit.ts/telegram-narrator.ts→ api-rsprincipal.rsdiff → chart.src/session-api.tsis a deliberate port of discordbot's client — reviewing the delta against that file is faster than reading it cold. Tests mirror module names undertest/.Test plan
pnpm install --frozen-lockfile, then fromservices/telegrambot:bun run check:typesandTELEGRAMBOT_TEST_DATABASE_URL=postgres://… bun test test(260 tests incl. receipt-cursor atomicity, split-brain fencing, steering recovery, FIFO regressions, render fault injection, 7-scenario e2e against a fake Bot API + fake api-rs + real Postgres; without the env var the DB suites skip cleanly — 216 pass/52 skip).cargo test -p centaur-iron-control(68),cargo fmt --check,clippy -D warnings,helm lint contrib/chart.services/telegrambot: setTELEGRAM_BOT_TOKEN,TELEGRAMBOT_USER_ALLOWLIST=<your telegram user id>,DATABASE_URL,CENTAUR_API_URL, and runbun src/server.ts.GET :3001/ready→ 200 after startup (/liveis immediate).TELEGRAMBOT_CHAT_ALLOWLIST, and verify/ask@<botname> pinganswers while plain chatter is ignored; a non-allowlisted chat gets zero responses.services/telegrambot/AGENTS.mdand record the outcome there.Risk / impact
Disabled by default (
telegrambot.enabled: false); no existing service's behavior changes. The api-rs change adds a principal branch plus a fallback guard (covered by regression tests, incl. that malformed telegram keys can't mint Slack principals). The bot token rides in every Bot API URL path — client code redacts it from all errors/logs; reviewers should keep an eye on that in any follow-ups.🤖 Generated with Claude Code