Skip to content

Telegram UX Plan (Single Chat, Fine-Grained Control)


title: "Telegram UX Plan (Single Chat, Fine-Grained Control)" created_at: "2026-02-15 09:05 CST" mode: "full-plan" inputs:

  • dev-docs/plans/20260215-1600-telegram-ux-design.md
  • Telegram Bot API docs (topics in private chats, inline keyboards, delete/protect)

Outcomes

  • Desired behavior:

  • Caesar controls a whole legion inside one Telegram chat with low noise and high clarity.

  • Caesar can use slash commands for deterministic control and can @mention centuriones (single or multiple) to dispatch work.

  • Unmentioned and unassigned centuriones receive nothing.

  • Every centurio response is clearly attributed with an "avatar prefix" so Caesar knows who is speaking and who is working.

  • Centuriones can request consult/debate with other centuriones, but all cross-centurio communication is mediated by Legatus and remains timeboxed and readable for Caesar.

  • For dangerous actions, Legatus requires a Google Authenticator OTP (TOTP) before execution.

  • Constraints:

  • One Telegram chat session. Prefer private chat with the Legatus bot.

  • Prompt over code by default. Code is justified for orchestration plumbing and security gates (OTP).

  • 100% test coverage gate remains mandatory.

  • Non-goals:

  • Multi-user access control beyond "only Caesar can talk to the bot".

  • Replacing Telegram with a custom UI.

  • Real per-message Telegram avatars. (Telegram identity is per bot user; attribution must be in-message.)

Constraints & Dependencies

  • Runtime/toolchain versions:

  • Python 3.11+.

  • python-telegram-bot (un-pinned).

  • Claude Agent SDK (centurio sessions and tool blocks).

  • OS/platform assumptions:

  • Single-operator deployment (Caesar).

  • External services:

  • Telegram Bot API.

  • Required environment variables / secrets:

  • TELEGRAM_BOT_TOKEN (already used).

  • LEGIO_TOTP_SECRET (new, base32) for TOTP verification.

  • Settings (legio.toml):

  • [telegram] topics_mode = "auto"|"on"|"off" (new; default auto).

  • [ux] mission_assignment_mode = "explicit"|"auto_propose"|"auto" (new; default explicit).

  • [security] totp_required_actions = [...] (new; default includes destructive actions).

  • [security] confirm_required_actions = [...] (new; default includes publish_edictum unless moved to TOTP list).

  • Optional env overrides (non-secret):

  • LEGIO_TELEGRAM_TOPICS_MODE=auto|on|off (new).

  • LEGIO_MISSION_ASSIGNMENT_MODE=explicit|auto_propose|auto (new).

  • Example legio.toml (new sections only):

toml
[telegram]
topics_mode = "auto"

[ux]
mission_assignment_mode = "explicit"

[security]
totp_required_actions = ["remove_centurio", "revoke_edictum"]
confirm_required_actions = ["publish_edictum"]
totp_ttl_seconds = 120
totp_max_attempts = 3
totp_drift_steps = 1

Current Behavior Inventory

  • Entry points:

  • legio/__main__.py creates TelegramBot and starts polling.

  • legio/telegram/bot.py handles /status, /list, /create, /help, and free-form text.

  • legio/legatus.py parses @mentions and dispatches to centuriones, otherwise responds as Legatus.

  • Data flow:

  • Telegram message -> TelegramBot._handle_message() -> Legatus.handle_message(text, telegram_user_id, on_status=...).

  • Mentions present: dispatch in parallel via CenturioSessionManager.dispatch_parallel.

  • No mentions: Legatus SDK client responds; may still call dispatch_to_centurio tool.

  • Status updates: legio/session.py:format_status() emits short status lines; Telegram status message is edited (debounced).

  • Persistence:

  • legio/praetorium.py SQLite table nuntii for message history.

  • legio/memoria/store.py filesystem-backed edicta, acta, and per-centurio commentarii.

  • Centurio definitions live under castra/centuriones/<name>/.

  • Known invariants:

  • Only Caesar's Telegram user id is accepted (TelegramBot._is_caesar()).

  • Mention extraction only dispatches to known centuriones (Legatus.extract_mentions()).

Target Rules

R0: Single Chat, Mission-Scoped Work

  • Default mode is one private chat with Legatus.
  • Work is organized by "mission context" to keep a single chat usable.
  • Preferred: Telegram topics in private chats.
  • Fallback: reply-to chaining to simulate threads if topics are unavailable.
  • Topics auto-detection:
  • On startup, call getMe and cache whether private-chat topics are enabled for the bot (has_topics_enabled).
  • If topics are disabled or unsupported, do not call forum-topic methods.

R1: Command Precedence

  • If a message is a slash command, it is handled as control-plane first.
  • If a message contains @mentions, it is treated as a dispatch instruction.
  • If a message has no mentions and no active mission assignment context, Legatus responds directly (and may propose routing).

R2: Who Sees What

  • A centurio receives work only if:
  • Caesar explicitly @mentions them, or
  • The current mission context has them assigned, or
  • Legatus explicitly consults them and narrates that choice to Caesar.

R2.1: Mission Assignment Mode (Configurable)

  • Config: ux.mission_assignment_mode
  • explicit:
  • Messages without @mentions dispatch only to mission-assigned centuriones.
  • If no centuriones are assigned, Legatus responds and proposes an /assign action.
  • auto_propose:
  • If no centuriones are assigned, Legatus proposes assignees and requires Caesar confirmation before assignment.
  • auto:
  • If no centuriones are assigned, Legatus assigns based on routing heuristics and narrates the decision.

R3: Attribution and "Avatar Prefix"

  • Every centurio response must include an attribution header that is unambiguous and stable.
  • Header format is consistent across the system and is applied centrally (not left to LLM discretion).
  • Avatar prefix is a short, visual token per centurio. Options:
  • Emoji (preferred on Telegram clients).
  • ASCII callsign (fallback), e.g. [VORENUS].

R4: Status and "Who Is Working Now"

  • Caesar can always answer:
  • Which missions are active.
  • Which centuriones are currently working and on what mission.
  • Legatus maintains a compact "panel" message that is edited in place.
  • Status edits are rate-limited to avoid Telegram edit limits.

R5: Mediated Consult and Debate

  • Centuriones do not message each other directly.
  • Consult is one-shot and scoped. Debate is timeboxed and structured.
  • Legatus is the mediator:
  • It creates the protocol, routes only required context, and produces the final synthesis.
  • Caesar sees the synthesis by default. Raw transcripts are opt-in (spoilered or on-demand).

R6: Dangerous Actions Require TOTP

  • Policy is configurable by action id (not by natural language intent).
  • Actions listed in security.totp_required_actions require a verified TOTP.
  • Actions listed in security.confirm_required_actions require Caesar to reply Confirmed (no OTP).
  • Default policy:
  • remove_centurio, revoke_edictum require TOTP.
  • publish_edictum requires Confirmed unless moved to TOTP list or removed from confirmation.
  • Legatus must obtain a valid TOTP (Google Authenticator compatible) tied to a specific authorization request.
  • The OTP must be bound to a request id and must expire quickly.
  • OTP messages should be deleted after verification (best-effort; subject to Telegram API limits).
  • Config knobs:
  • security.totp_required_actions, security.confirm_required_actions.
  • security.totp_ttl_seconds, security.totp_max_attempts, security.totp_drift_steps.

Decision Log

  • D1:

  • Options:

  • Single bot with in-message attribution (current).

  • One bot per centurio in a shared group (native avatars).

  • Decision:

  • Keep single bot, implement strong in-message attribution and mission scoping.

  • Rationale:

  • Operational simplicity, consistent mediation, easier state management, no cross-bot visibility constraints.

  • Rejected alternatives:

  • Multi-bot group complicates mediation and state, and increases operational surface area.

  • D2:

  • Options:

  • Flat private chat with reply-to chains only.

  • Topics (forum topics) in private chats where supported, with reply-to fallback.

  • Decision:

  • Prefer topics when available; fallback to reply-to chains.

  • Rationale:

  • Topics provide the "fine-grained" UX without leaving a single chat surface.

  • D3:

  • Options:

  • Prompt-only confirmation ("Confirmed").

  • Code-level TOTP gate for configured actions.

  • Hybrid.

  • Decision:

  • Code-level TOTP for configured actions. Prompt-only confirmation for configured actions.

  • Rationale:

  • Security guarantee is worth code for irreversible operations.

  • D4:

  • Options:

  • Let centuriones debate freely in chat.

  • Only Caesar triggers debate.

  • Allow centuriones to request debate, but Legatus mediates and Caesar approves.

  • Decision:

  • Centuriones may request; Legatus proposes; Caesar approves start.

  • Rationale:

  • Prevents debate spam while preserving useful internal conflict.

Open Questions

  • Q1:
  • Why it matters:
  • Do we want to support a forum supergroup "HQ" mode in addition to private chat?
  • Who decides:
  • Caesar.
  • Default if unresolved:
  • Private chat only for now.

Data Model (applicable)

  • New concepts:

  • missio (mission) metadata and Telegram topic linkage.

  • auctoritas (authorization request) state for TOTP-gated actions.

  • Proposed schema additions in legio/praetorium.py (new tables, additive migration via CREATE TABLE IF NOT EXISTS):

  • missiones:

  • Columns: id (TEXT PK), title (TEXT), status (TEXT), assignees_json (TEXT), chat_id (INTEGER), thread_id (INTEGER NULL), created_at (TEXT), updated_at (TEXT).

  • auctoritates:

  • Columns: id (TEXT PK), action (TEXT), payload_json (TEXT), status (TEXT), chat_id (INTEGER), message_id (INTEGER), expires_at (TEXT), attempts (INTEGER).

  • Compatibility:

  • Existing deployments should start without migrations beyond table creation.

  • If tables are absent, features using them should degrade gracefully (missions unavailable).

API / Contract Changes (applicable)

  • New Telegram commands (flat, no subcommands):

  • /missio <title>: create/select mission context (topic if available).

  • /missions: list active missions.

  • /assign @centurio ...: assign centuriones to current mission.

  • /unassign @centurio ...: remove assignment.

  • /consult @centurio <question>: one-shot consult.

  • /debate @a @b <question>: mediated debate protocol.

  • /remove <centurio>: destructive; authorization per security.totp_required_actions (default: TOTP).

  • /revoke <edictum>: destructive; authorization per security.totp_required_actions (default: TOTP).

  • /edict <name> <text>: publish standing order; authorization per policy (default: Confirmed).

  • /history [n]: show last N nuntii (read-only).

  • Signature expansions (likely required):

  • Legatus.handle_message(...) needs mission context:

  • Add chat_id: int and thread_id: int | None so routing can bind to mission scope and topics.

Observability (applicable)

  • Logs:
  • Command invocations (without secrets).
  • Mission lifecycle (create/assign/close).
  • Authorization lifecycle (requested/approved/expired).
  • OTP verification attempts count (never log OTP values).

Work Items

WI-001: Mission Context and Topic Support

  • Goal:

  • Support mission-scoped work in a single chat using Telegram topics when available, with reply-to fallback.

  • Acceptance (measurable):

  • In a topic-enabled chat, replies and status updates land in the same topic as Caesar's request.

  • In a topic-enabled chat, typing indicators are emitted to the same topic (topic-aware sendChatAction).

  • If topics are disabled, system behaves as today (flat chat) and uses reply-to chaining for context.

  • Tests (first):

  • File(s): tests/test_telegram_bot.py

  • Intent: Given updates with message_thread_id, bot replies with message_thread_id set and does not leak messages to the base thread.

  • File(s): tests/test_telegram_bot.py

  • Intent: Given topics disabled, bot does not attempt topic methods and still replies successfully.

  • Touched areas:

  • File(s): legio/telegram/bot.py

  • Symbols: _handle_message(), _keep_typing(), _make_status_callback().

  • Dependencies:

  • Telegram client supports topics for the bot (or we detect and disable).

  • Risks + mitigations:

  • Risk: API mismatch across python-telegram-bot versions.

  • Mitigation: Feature-detect in runtime and provide fallback behavior.

  • Rollback:

  • Set [telegram] topics_mode = "off" (or env override LEGIO_TELEGRAM_TOPICS_MODE=off).

  • Estimate: M

WI-002: Mission Persistence and Assignment

  • Goal:

  • Persist missions (missiones) and per-mission assigned centuriones, keyed by chat/thread.

  • Acceptance (measurable):

  • /missio creates a mission record and selects it for the current topic.

  • /assign modifies mission assignees.

  • In a mission context, dispatch behavior follows ux.mission_assignment_mode.

  • Tests (first):

  • File(s): tests/test_praetorium.py

  • Intent: Schema upgrade creates new tables without impacting existing nuntii behavior.

  • File(s): tests/test_legatus.py

  • Intent: Dispatch rules respect assignment and do not dispatch to unassigned centuriones.

  • Touched areas:

  • File(s): legio/praetorium.py

  • File(s): legio/legatus.py

  • File(s): legio/telegram/bot.py

  • File(s): legio/config.py (load ux.mission_assignment_mode).

  • New file(s): legio/missio.py (data model + helpers).

  • Dependencies:

  • WI-001.

  • Risks + mitigations:

  • Risk: Complex coupling between Telegram thread ids and mission ids.

  • Mitigation: One mission per (chat_id, thread_id) with a stable mapping; keep it simple.

  • Rollback:

  • Feature flag: do not auto-dispatch without mentions unless mission tables exist.

  • Estimate: L

WI-003: Attribution Headers with Stable Avatars

  • Goal:

  • Make centurio identity unmistakable in a single-bot chat by using a stable header format and avatar prefix.

  • Acceptance (measurable):

  • Every centurio reply is formatted by code with the same header schema.

  • Avatar prefix is stable for a given centurio and does not collide within the roster.

  • Tests (first):

  • File(s): tests/test_legatus.py

  • Intent: Multi-mention responses include correct headers and preserve body content.

  • File(s): tests/test_rendering.py

  • Intent: Header rendering escapes unsafe content and remains Telegram-HTML safe.

  • Touched areas:

  • File(s): legio/legatus.py

  • File(s): legio/rendering.py

  • File(s): legio/centurio.py (if adding persisted avatar metadata).

  • Dependencies:

  • None.

  • Risks + mitigations:

  • Risk: Emoji rendering inconsistencies.

  • Mitigation: Provide ASCII fallback callsign and allow per-centurio override.

  • Rollback:

  • Keep current ⚔️ name header as fallback.

  • Estimate: S

WI-004: Legatus Panel and Noise Budget

  • Goal:

  • Keep Caesar oriented with a single edited "panel" message that shows active missions and WIP centuriones.

  • Acceptance (measurable):

  • /status remains a stable roster view.

  • Panel updates do not exceed edit rate limits and do not spam the chat.

  • Tests (first):

  • File(s): tests/test_telegram_bot.py

  • Intent: Status callback debounce ensures edits are rate-limited.

  • File(s): tests/test_session.py

  • Intent: format_status() outputs user-facing text (no raw tool identifiers).

  • Touched areas:

  • File(s): legio/session.py

  • File(s): legio/telegram/bot.py

  • Prompt: castra/legatus/prompt.md (to instruct narration and summarization).

  • Dependencies:

  • WI-001 (topic-aware panel if enabled).

  • Risks + mitigations:

  • Risk: Panel becomes a second source of truth.

  • Mitigation: Panel is derived and ephemeral; /missions and /status are authoritative.

  • Rollback:

  • Disable panel updates (keep existing per-message status only).

  • Estimate: M

WI-005: Consult and Debate Protocols (Mediated)

  • Goal:

  • Add predictable, timeboxed patterns for centurio-to-centurio consult and debate mediated by Legatus.

  • Acceptance (measurable):

  • /consult @x Q returns an attributed answer from @x and a short Legatus summary.

  • /debate @a @b Q produces a final synthesis that includes options, recommendation, and risks, within fixed rounds.

  • Tests (first):

  • File(s): tests/test_legatus.py

  • Intent: Debate orchestration dispatches only to listed centuriones and stops at the configured round limit.

  • File(s): tests/test_praetorium.py

  • Intent: Debate artifacts are posted with correct audience and reply_to linkage.

  • Touched areas:

  • File(s): legio/telegram/bot.py (command parsing).

  • File(s): legio/legatus.py (orchestration helpers).

  • Prompt: castra/legatus/prompt.md (protocol definitions).

  • Dependencies:

  • WI-002 (mission context helps keep debates scoped, but not strictly required).

  • Risks + mitigations:

  • Risk: Debate loops or prompt injection.

  • Mitigation: Hard round limits in code; legatus prompt requires structured outputs; no tool side-effects during debate.

  • Rollback:

  • Keep consult only; disable debate command.

  • Estimate: M

WI-006: TOTP Authorization Gate for HIGH Actions

  • Goal:

  • Require Google Authenticator OTP for destructive actions and ensure tools cannot bypass it.

  • Acceptance (measurable):

  • Any action listed in security.totp_required_actions requires a valid OTP before execution.

  • Default policy requires OTP for /remove <centurio> and /revoke <edictum>, and can be configured to include publish_edictum.

  • OTP is bound to a specific request id and expires (default 2 minutes).

  • OTP messages are deleted after verification (best-effort).

  • Authorization request messages are sent with protected-content enabled where supported.

  • Tests (first):

  • File(s): tests/test_telegram_bot.py

  • Intent: Authorization request is created and OTP reply advances state to approved.

  • File(s): tests/test_config.py

  • Intent: Missing LEGIO_TOTP_SECRET disables TOTP-required actions with a clear error to Caesar.

  • File(s): tests/test_config.py

  • Intent: security.totp_required_actions and ux.mission_assignment_mode are loaded from TOML (and env overrides if present).

  • File(s): tests/test_security_totp.py (new)

  • Intent: TOTP verifier accepts valid codes within a small drift window and rejects invalid/replayed codes.

  • Touched areas:

  • File(s): legio/config.py (new TOML sections + env overrides + LEGIO_TOTP_SECRET).

  • New file(s): legio/auctoritas.py (auth request state machine), legio/totp.py (verifier).

  • File(s): legio/telegram/bot.py (OTP UX and deletion).

  • File(s): legio/praetorium.py (persist pending authorizations, optional).

  • Dependencies:

  • WI-001 (topic-aware authorization cards are nicer, but not required).

  • Risks + mitigations:

  • Risk: Storing secrets unsafely.

  • Mitigation: Secret only from env; never persisted; never logged.

  • Risk: Telegram API deleteMessage limits prevent deletion sometimes.

  • Mitigation: Best-effort delete; do not block success on deletion.

  • Rollback:

  • Set security.totp_required_actions = [] to temporarily disable OTP and rely on confirm-only policy.

  • Estimate: L

WI-007: Inline Keyboards for Selection and Approvals

  • Goal:

  • Reduce typing and mistakes by using inline buttons for common actions and dangerous action confirmation steps.

  • Acceptance (measurable):

  • Mission cards contain buttons: Assign, Consult, Debate, Close.

  • Authorization cards contain buttons: Cancel, I will enter OTP.

  • Button payloads fit callback_data constraints (1-64 bytes), using short ids that expand server-side.

  • Tests (first):

  • File(s): tests/test_telegram_bot.py

  • Intent: Callback queries map to the correct internal actions and respect Caesar-only access.

  • Touched areas:

  • File(s): legio/telegram/bot.py

  • Dependencies:

  • WI-001, WI-006.

  • Risks + mitigations:

  • Risk: Callback payload length limits.

  • Mitigation: Use short ids and store full payload server-side.

  • Rollback:

  • Keep command-only UX.

  • Estimate: M

Testing Procedures

  • Fast checks:

  • pytest -q

  • ruff check .

  • Full gate:

  • pytest --cov --cov-fail-under=100

  • When to run each:

  • After each WI implementation and before merging any branch.

Manual Test Checklist

  • [ ] In a fresh chat, /help lists commands and mention syntax.
  • [ ] Create a mission, assign 2 centuriones, send a message without mentions, and confirm only those 2 are dispatched.
  • [ ] Send @a @b message and confirm parallel dispatch and attributed replies.
  • [ ] Run /consult and /debate and confirm summaries are readable and timeboxed.
  • [ ] Attempt /remove <centurio> without OTP and confirm it refuses.
  • [ ] Provide valid OTP and confirm the action executes and OTP message is deleted (best-effort).

Plan -> Verify Handoff

  • Evidence to collect per WI:
  • WI-001/002: unit tests showing correct thread_id routing and assignment dispatch rules.
  • WI-003/004: snapshot tests for header/panel formatting; manual Telegram sanity.
  • WI-005: tests demonstrating hard round limits and scoped dispatch.
  • WI-006: security tests for OTP verification, expiry, and Caesar-only binding.
  • WI-007: callback query tests for keyboard flows.

References

Built with Roman discipline.