Skip to content

[Frontier] [CHAN15-01] Talk to S15 Jit through a bound Telegram bot #368

Closed

Description

@chayapats

[Frontier] [CHAN15-01] Talk to S15 Jit through a bound Telegram bot

Parent

S15 — #316. Canonical spec: docs/specs/S15-conversational-autonomy.md.

Model tier

Frontier: owner channel binding is an authority boundary (who counts as the authenticated owner), ingestion needs crash-safe dedupe/offset ordering, and outbound delivery needs a durable outbox. The settings UI and message copy must be authored by Claude Opus 5.5 at effort high.

Gap found (verified on main c5f476e)

Telegram exists only in the legacy project runtime (src/telegram/**, ADR 0013/0018/0037). S14 and the S15 runtime have no Telegram channel: S15 owner ingress is the Web UI only, and S15 has no outbound notification path. The owner decided on 2026-11-03 that the new production install on jit-vm gets a NEW Telegram bot (created in BotFather by the owner as a final step; the Pi keeps its own bot until it is retired). Without this slice that bot has nothing to talk to.

Context to read

AGENTS.md; CONTEXT.md; docs/product/02-prd.md (bound owner channels, line "Telegram support is conditional on its actual transport evidence"); docs/adr/0013-telegram-owner-binding.md; docs/adr/0037-access-levels-approvals-and-connections.md (Telegram limits); docs/design/S15-conversational-experience.md (Q4 accepted default: approvals inside companion channels are out of scope; channels notify and link to the Web slip); src/telegram/protocol.ts, binding.ts, inbox.ts, secrets.ts (reuse the protocol parsing/classification ideas, not the legacy store); src/s15/web/handler.ts (/s15/forms/message submit path and stall replies); src/s15/conversation/; src/s15/work/ (responsibility projections, pause/stall reports); src/exec/connections.ts (ConnectionVault); src/security/secret-fingerprints.ts; src/s15/composition.ts; src/s15/main.ts.

Deliverable

The owner pastes the bot token once in protected Web settings and binds their own Telegram account with a one-time challenge. After that, private messages from the bound owner become ordinary S15 conversation requests (same authority and checks as the Web composer), and Jit sends final replies, concise progress, stall/pause reports and held-action notices back to that chat. Held actions link to the Web review page; they cannot be approved inside Telegram.

Implementation steps

  1. Token setup: a write-only protected settings form stores the token in ConnectionVault (never in config, logs, exports, prompts or the guest) and registers its fingerprint. Verify it with getMe and show the bot username. Replace and remove are owner-only; removal stops polling and leaves history intact.
  2. Binding (ADR 0013 semantics, S15 store): the owner starts binding in the Web UI and receives a one-time code; sending that code from a private chat binds the stable numeric user ID and private chat ID. Usernames are display-only. Groups, channels, forwards, bot senders, other users and stale codes never bind or reach the conversation. Revoke from the Web UI. Persist binding, poll offset and inbox in the protected S15 journal (new tables, versioned), not in the legacy app store.
  3. Ingestion: long polling with finite backoff. Persist each update's identity and classification before advancing getUpdates offset (separate commit), dedupe on update_id, and derive a stable conversation request ID from it so a crash/replay submits at most once. Downtime beyond Telegram's 24-hour retention is shown as a visible gap, never invented.
  4. Conversation: an authorized text message is submitted through the same conversation port and checks as the Web composer, in a stable conversation for that chat. Quoted/forwarded content, replies to bot messages and message entities carry no extra authority. Text only in this slice; attachments, voice and media get a short "not supported here yet" reply.
  5. Outbound (durable outbox): derive outbound messages from conversation and responsibility state changes, keyed by a stable event ID so each is sent at most once across restart (a send with an unknown outcome is not blindly repeated with a new ID). Send final assistant replies, sparse progress for long work, stall/pause reports with the recommendation, and held-action notices with a link to the exact Web slip. Respect Telegram limits (4096 chars, rate limits/retry_after), escape formatting, and refuse credential-shaped content via the existing secret fingerprint check.
  6. Stall replies: a reply to a stall report names that paused responsibility and resumes it only through the durable resume binding delivered by WORK15-02 ([Frontier] [WORK15-02] Keep owner stall replies that wait to resume a duty durable across restart #367) (same checks as the Web path).
  7. UI (Claude Opus 5.5, effort high): a Telegram section in S15 settings (token, bot name, binding code and status, revoke, last poll and gap state) and the th/en message copy for every outbound kind. th and en keys for every new string.
  8. Docs: an S15 Telegram section in the runbook and a short ADR (next free number) recording that Telegram is a bound owner conversation/notification channel for S15, approvals stay in the Web UI, and the ADR 0013 binding rules apply.

Acceptance criteria

  • Token entry, verification, binding, revoke and removal work from the Web UI only; the token never appears in config, logs, exports, prompts, the guest or test snapshots.
  • Only the bound numeric owner in the bound private chat reaches the conversation; every other sender/chat type is refused and recorded as unauthorized.
  • A crash between persistence and offset advance neither loses nor duplicates a request.
  • Owner messages start the same work as the Web composer would, under the same standing policy; nothing in Telegram can approve a held action, widen policy or install rules.
  • Final replies, progress, stall reports and held-action notices arrive once each across restarts; held-action notices link to the exact Web slip.
  • UI and copy authored by Claude Opus 5.5 at effort high; th and en keys exist.

Tests and evidence

  1. Protocol fixtures with a scripted Telegram transport (no live token in CI): private owner text → submitted; group/forward/bot/other user/stale code → refused.
  2. Binding: correct code from private chat → bound; reused or expired code → refused; revoke → later messages refused.
  3. Crash between inbox commit and offset advance → reopened service submits exactly once.
  4. Outbox: each conversation/duty event → one send; restart → no duplicate; retry_after honored; long text split within limits; credential-shaped text refused.
  5. Held action → notice with the Web slip link; a Telegram reply "approve" does not approve anything.
  6. Stall reply → resume through WORK15-02's binding; unrelated reply → ordinary message.
  7. Web rendering tests (th/en) for the settings states.
  8. Live (owner step, recorded on [Frontier] [CUT15-01] Install S15 fresh on jit-vm with owner acceptance and a recoverable install #341): the owner's new bot, bound on jit-vm, completes one ordinary conversation.

Requirements

FR-10/11/15/17/19/22/23; NFR-05/07/10. Acceptance: S15-AC01 (channel ingress), S15-AC07 (approval stays exact), S15-AC15 (durable responsibility notices).

Blocked by

None for implementation. Bundled in one PR with WORK15-02 (#367). The live check needs the owner's new bot token (owner step on #341).

Contracts used/delivered

Uses the S15 conversation submit/view ports, responsibility projections, ConnectionVault, secret fingerprints. Delivers an S15 Telegram channel module with exported TSDoc, protected settings routes, journal tables and an outbox. Do not rename existing exported APIs.

Out of scope

Approving held actions inside Telegram; group chats; attachments/voice/media; webhooks; migrating the Pi's legacy Telegram binding or bot; changing the legacy src/telegram behavior.

Do not

Do not treat a username, forward, quote or message text as owner authority; do not put the token anywhere except the vault; do not advance the offset before persistence; do not let Telegram install policy, approve actions or resume outside the WORK15-02 checks; do not add AI attribution; do not merge the PR yourself.

Definition of done

bun install --frozen-lockfile, bun run typecheck, bun run test, git diff --check pass on the final HEAD. PR body: Closes #368, Closes #367, Refs #341, tested SHA, not-run limits. No merge by the implementer.

Activity

  1. added
    enhancementNew feature or request
    ready-for-agentImplementation-ready; start only when required blockers and ownership checks pass.
    on Nov 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestready-for-agentImplementation-ready; start only when required blockers and ownership checks pass.

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions