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; defaultauto).[ux] mission_assignment_mode = "explicit"|"auto_propose"|"auto"(new; defaultexplicit).[security] totp_required_actions = [...](new; default includes destructive actions).[security] confirm_required_actions = [...](new; default includespublish_edictumunless 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):
[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 = 1Current Behavior Inventory
Entry points:
legio/__main__.pycreatesTelegramBotand starts polling.legio/telegram/bot.pyhandles/status,/list,/create,/help, and free-form text.legio/legatus.pyparses@mentionsand 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_centuriotool.Status updates:
legio/session.py:format_status()emits short status lines; Telegram status message is edited (debounced).Persistence:
legio/praetorium.pySQLite tablenuntiifor message history.legio/memoria/store.pyfilesystem-backededicta,acta, and per-centuriocommentarii.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
getMeand 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
/assignaction. 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_actionsrequire a verified TOTP. - Actions listed in
security.confirm_required_actionsrequire Caesar to replyConfirmed(no OTP). - Default policy:
remove_centurio,revoke_edictumrequire TOTP.publish_edictumrequiresConfirmedunless 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 viaCREATE 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 persecurity.totp_required_actions(default: TOTP)./revoke <edictum>: destructive; authorization persecurity.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: intandthread_id: int | Noneso 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.pyIntent: Given updates with
message_thread_id, bot replies withmessage_thread_idset and does not leak messages to the base thread.File(s):
tests/test_telegram_bot.pyIntent: Given topics disabled, bot does not attempt topic methods and still replies successfully.
Touched areas:
File(s):
legio/telegram/bot.pySymbols:
_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-botversions.Mitigation: Feature-detect in runtime and provide fallback behavior.
Rollback:
Set
[telegram] topics_mode = "off"(or env overrideLEGIO_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):
/missiocreates a mission record and selects it for the current topic./assignmodifies mission assignees.In a mission context, dispatch behavior follows
ux.mission_assignment_mode.Tests (first):
File(s):
tests/test_praetorium.pyIntent: Schema upgrade creates new tables without impacting existing
nuntiibehavior.File(s):
tests/test_legatus.pyIntent: Dispatch rules respect assignment and do not dispatch to unassigned centuriones.
Touched areas:
File(s):
legio/praetorium.pyFile(s):
legio/legatus.pyFile(s):
legio/telegram/bot.pyFile(s):
legio/config.py(loadux.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.pyIntent: Multi-mention responses include correct headers and preserve body content.
File(s):
tests/test_rendering.pyIntent: Header rendering escapes unsafe content and remains Telegram-HTML safe.
Touched areas:
File(s):
legio/legatus.pyFile(s):
legio/rendering.pyFile(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
⚔️ nameheader 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):
/statusremains 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.pyIntent: Status callback debounce ensures edits are rate-limited.
File(s):
tests/test_session.pyIntent:
format_status()outputs user-facing text (no raw tool identifiers).Touched areas:
File(s):
legio/session.pyFile(s):
legio/telegram/bot.pyPrompt:
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;
/missionsand/statusare 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 Qreturns an attributed answer from@xand a short Legatus summary./debate @a @b Qproduces a final synthesis that includes options, recommendation, and risks, within fixed rounds.Tests (first):
File(s):
tests/test_legatus.pyIntent: Debate orchestration dispatches only to listed centuriones and stops at the configured round limit.
File(s):
tests/test_praetorium.pyIntent: 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_actionsrequires a valid OTP before execution.Default policy requires OTP for
/remove <centurio>and/revoke <edictum>, and can be configured to includepublish_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.pyIntent: Authorization request is created and OTP reply advances state to approved.
File(s):
tests/test_config.pyIntent: Missing
LEGIO_TOTP_SECRETdisables TOTP-required actions with a clear error to Caesar.File(s):
tests/test_config.pyIntent:
security.totp_required_actionsandux.mission_assignment_modeare 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.pyIntent: Callback queries map to the correct internal actions and respect Caesar-only access.
Touched areas:
File(s):
legio/telegram/bot.pyDependencies:
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 -qruff check .Full gate:
pytest --cov --cov-fail-under=100When to run each:
After each WI implementation and before merging any branch.
Manual Test Checklist
- [ ] In a fresh chat,
/helplists commands and mention syntax. - [ ] Create a mission, assign 2 centuriones, send a message without mentions, and confirm only those 2 are dispatched.
- [ ] Send
@a @bmessage and confirm parallel dispatch and attributed replies. - [ ] Run
/consultand/debateand 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
- Telegram Bot API changelog (topics in private chats, inline button styles).
- https://core.telegram.org/bots/api-changelog
- Telegram Bot API main docs (forum topics, message_thread_id, inline keyboard callback_data limits, protect_content, deleteMessage limits).
- https://core.telegram.org/bots/api
- Telegram bot FAQ (privacy mode behavior in groups; relevant if HQ group mode is added later).
- https://core.telegram.org/bots/faq