The Lilith bot turns Telegram updates into policy-checked responses and real effects. The supported live inbound path is the Oshun BFF webhook; the shared Telegram library contains the pure handler, command registry, delivery translation, keyboards, limits, and provider seams.
Entry and discoverability#
At startup/deployment, command-surface registration makes capabilities visible:
setMyCommandspublishes the full member list to private chats;- a reduced list is published to group chats;
setChatMenuButtonopens Open Lilith as a Web App when a Mini App URL is configured, otherwise it opens the command list;t.me/<bot>?start=<payload>links map a closed payload vocabulary to rooms or the greeting.
Registration failures are reported but do not stop message handling. Operations must still alert: a bot that answers only commands users cannot discover is a functional outage.
Registered private-chat commands#
The canonical menu in deep-links.ts registers:
| Command | Member job |
|---|---|
/sit |
Start a paced Tara ritual in chat. |
/checkin |
Open the member's Arete check-in. |
/tonight |
Show the Nyx sky window for the member's location/time. |
/passage |
Read the current Nisaba passage. |
/ask |
Ask Sophia for a grounded answer or an honest abstention. |
/remind |
Create a reminder from a supported time expression. |
/continue |
Resume the member's latest supported activity. |
/library |
List the member's open items. |
/streak |
Show streak/recovery state. |
/save |
Capture a note or link through the effects port. |
/timezone |
Set the member's IANA time zone or supported offset. |
/settings |
Show quiet hours, voice, delivery, and clock settings. |
/upgrade |
Begin the configured upgrade/payment path. |
/help |
Explain the available command surface. |
The handler also recognizes supporting controls and compatibility commands:
/start, /menu, legacy /today, /sources, /quiet, /stop, /resume,
/voice, and /unlink. Some are contextual/help actions rather than menu
entries. A command being recognized does not mean its effect adapter or product
room is configured; unavailable paths say so.
Group-chat boundary#
Personal commands are not allowed to reveal or mutate one member's state in a group/supergroup. The runtime—not only the menu—enforces the private-only set, including sitting/check-in/tonight/library/continue/streak/settings/reminders/ save/preferences/timezone/today/upgrade controls.
/passage and /ask can be shared because their content belongs to the shared
passage or question, not a member profile. Buttons that need a Telegram Web App
are private-chat-only; group messages use a safe deep-link fallback when a known
mapping exists. Channel posts are not bot conversations.
Commands addressed as /command@botusername are normalized for Telegram group
behavior. Another bot's addressed command must not be claimed. Edited messages
and non-message update kinds are classified deliberately so a write is not
silently repeated.
Update kinds#
The handler classifies text messages, voice messages, callback queries, inline queries, pre-checkout queries, successful payments, membership changes, and other supported Telegram update shapes. Each kind has an explicit claim/dedupe and response/delivery path. Unsupported or irrelevant updates produce no effect rather than a misleading reply.
Chat/member updates detect blocked/stopped state without trying to message a user who just blocked the bot. The delivery layer also recognizes Telegram's blocked-user descriptions and transitions delivery state accordingly.
Command execution model#
The handler separates reads and writes through ports. A room read can return ready, unavailable, forbidden, or missing-location/context states. A write can return applied, duplicate/idempotent, denied, invalid, unavailable, or failed. Copy reflects the outcome; no port means “not connected,” never “saved.”
Text and rendering#
Responses preserve disclosure and provenance for grounded content, escape or choose parse behavior deliberately, build supported inline keyboards, and degrade rich content only where the simpler form retains meaning.
Telegram text is bounded. Long output is split on safe boundaries into multiple calls rather than sent as one rejected payload. Callback-answer text, callback data, inline-result ids/counts, and start payloads have their own byte/length limits. Tests pin these limits and multibyte behavior.
Keyboards and callbacks#
Inline buttons can encode namespaced callback data, URLs/deep links, and Web App buttons. Callback data is parsed as a closed structure and bounded by byte length. Unknown, expired, malformed, unauthorized, or duplicate callbacks are answered safely and do not invoke arbitrary actions.
Callbacks support contextual save, reminder delivery/snooze/done, ritual cues, room actions, and upgrade/payment affordances where configured. The handler answers the callback query so Telegram removes the loading indicator, then executes or reports the actual effect.
Presence and media delivery#
The runtime can emit typing, record_voice, or upload_voice chat actions
for update/response work. Presence is best effort; failure never becomes proof
the underlying command failed or succeeded.
Delivery translates a response into one or more Bot API calls for text and declared media/file/voice forms, resolves upload/download data where required, and records each outcome. A null/invalid response does not create a provider call. Partial multi-message delivery is reported as partial, not success.
Rate and abuse controls#
Rate limiting keys actor/chat/update scope and policy, returns retry information, and runs before expensive grounding/provider work. Inline mode has additional abuse detection. Limits cannot be bypassed by command casing, bot addressing, callbacks, duplicate update delivery, or alternating update shapes.
Rate-limit and abuse copy avoids exposing internal thresholds or cross-user state. Crisis handling remains available under its dedicated policy and does not turn rate bypass into general command access.
Failure behavior#
- Missing bot token/webhook secret or invalid request fails closed at the edge.
- Duplicate or already-claimed update does not repeat the write.
- Personal command in a group redirects the member to private chat without revealing state.
- Unwired room, reminder, grounder, effect, upgrade, STT/TTS, or provider path says unavailable or abstains.
- Provider application-level
{ok:false}is a failure even with HTTP 200. - Telegram timeouts/5xx/rate limits use bounded retry policy and idempotency; malformed/forbidden/blocked failures do not retry indefinitely.
- Long replies split; invalid buttons/media are withheld or safely degraded.
/stophalts delivery and/resumeprovides an explicit recovery path where the durable state adapter is available.
Source and tests#
Primary implementation is under
libs/oshun/messaging-channels/src/telegram/{bot,command-surface,room-commands,deep-links,keyboard,deliver,bot-api-limits,rate-limit}.ts.
Live composition is under apps/oshun/bff/src/routes/telegram.ts and
apps/oshun/bff/src/telegram.
Contract, handler, group-scope, Bot API limit/conformance, delivery, webhook, dedupe/claiming, effect wiring, blocked-user, and deployment-parity tests live next to those modules and in BFF tests. The user journey is documented in Telegram assistant delivery.