Telegram
Telegram is Vita's mobile conversation and delivery surface. The bot runs locally, receives messages from the private Vita supergroup, and gives each forum topic its own long-lived agent session. It also carries scheduled outreach, system-event notifications, background-agent results, external-email approvals, and live consoles for work running elsewhere.
The important architectural rule is:
Everything that appears in a topic should reach that topic's resident session.
Interactive replies and mediated system events already pass through the resident session. Out-of-session sends are written to a per-topic ledger and injected on the session's next turn. This prevents the old failure where Telegram showed a message, bio-zack replied to it, and the topic agent had no idea what he was referring to.
July 2026 dispatch upgrade
The July 12 participant-aware upgrade added two related pieces:
- Topic ledger: proactive sends, direct capability sends, background results, and participant exchanges are recorded for later injection into the resident topic session.
- Live participants: a running process can claim a topic temporarily. While the claim is active, bio-zack's conversational turns route to that process instead of the resident session. Claims support pull-based mailboxes and inference-session auto-resume.
Together these turn a Telegram topic into a bounded, two-way handle on long-running work without discarding the topic's established agent context.
System boundaries
Telegram has three different concepts that should not be conflated:
| Concept | What it does | Durability |
|---|---|---|
| Resident topic session | Maintains the agent conversation for one forum topic | Session ID is persisted in data/telegram/parallel_state.json |
| Live participant claim | Temporarily routes inbound topic turns to another process/session | Registration is persisted in data/proactive/queue.db; bounded by TTL |
| Scheduler / deferred prompt | Delivers a message or starts work at a future time | Queue-backed and survives the current terminal or agent session |
A participant registration is routing state, not scheduling. Registering does not
start a worker, keep a terminal alive, or arrange a future invocation. A mailbox
participant still needs a live poll/await process. For future work, use VitaScheduler,
enqueue_message(), or enqueue_prompt(); for a live in-session console, use a
participant claim.
Trigger points
Inbound
| Trigger | Result |
|---|---|
| Text message | Batched, routed by topic, then handled by a participant, a mechanical reply handler, or the resident session |
| Voice message | Downloaded, transcribed through Groq Whisper, then routed like text |
| Photo or album | Downloaded to a temporary path and included in the same per-topic batch as its caption |
wait |
Holds the current input batch open; the next real message releases it |
stop |
Cancels the accumulating batch or the latest in-flight task for the topic |
release |
Ends the topic's active participant claim |
model / model N |
Lists or switches the topic's inference backend |
claude / claude N / claude save |
Inspects or switches Claude account credentials |
clux / clux detach |
Attaches, inspects, or detaches an external clux coding session |
| Message reaction | A thumbs-up can acknowledge a tracked reminder and reset its adaptive backoff |
Registered slash commands are /start, /help, /status, /reset, /queue,
/pending, /proactive, /home, /bug, /issue, /resolve, and /meeting.
Plain reset also exists as a topic-aware quick control; see
Session reset caveats.
Outbound
| Trigger | Delivery path |
|---|---|
| Interactive agent reply | Resident session edits the ... placeholder in place |
| System event | Event funnel batches events and calls the resident session; the agent decides whether to emit a message |
| Scheduled outreach or reminder | Durable proactive queue, then direct Telegram send plus topic-ledger record |
vita.telegram.send |
Synchronous direct Bot API send plus topic-ledger and conversation-log records |
| Background agent result | Direct topic delivery plus topic-ledger record |
| Participant session response | Direct topic delivery plus topic-ledger record |
The recurring outreach catalog and adaptive frequency policy live in Proactive Outreach; they are intentionally not duplicated here.
Architecture
Telegram client
|
v
Telegram Bot API <-----------------------------------------------+
| |
v |
scripts/telegram_bot.py |
| |
+-- auth / credential-reply interception / quick controls |
| |
+-- per-topic batch accumulator |
| |
v |
active participant? |
| yes | no |
v v |
+-----+---------+ mechanical reply handler? |
| mailbox | | yes | no |
| queue.db | v v |
+---------------+ direct handler resident session |
or telegram-{topic_id} |
+-----+---------+ | |
| inference | v |
| session_key | unified inference |
+---------------+ + tools + Vita repo |
| | |
+--------------------------------------+------------------+
Out-of-session producers
proactive scheduler / capability send / background agent / participant
|
+--> Telegram Bot API
|
+--> data/telegram/topic_ledger.jsonl
|
+--> injected into the resident session's next turn
The bot is a python-telegram-bot polling daemon. Inference goes through
scripts.inference.factory.stream_inference(), so routing can select the Claude
daemon or another configured backend while preserving the same topic/session
contract.
Inbound routing
Input batching
scripts/telegram_parallel/batch_accumulator.py coalesces rapid-fire bubbles into
one turn per topic:
| Setting | Value | Meaning |
|---|---|---|
| Quiet-window debounce | 2 seconds | Dispatch after no new bubble arrives |
| Hard cap | 15 seconds | Dispatch even if bubbles keep arriving |
wait |
Control word | Hold the batch open until the next substantive message |
stop |
Control word | Discard a batch or cancel the latest in-flight topic task |
Albums are grouped by media_group_id; each photo path is retained while the caption
is included once. A hard-cap flush appends a warning that the input may be incomplete.
Routing priority
Order matters. The active-participant check is deliberately early, but it does not override every bot control:
- Validate the numeric Telegram user ID and allowed chat.
- Intercept replies that contain requested credentials; store the credential and attempt to delete the sensitive Telegram message.
- Handle quick controls such as reset, model/account switching, clux attachment,
wait, andstop. - Batch the remaining text, voice transcript, and photo paths by topic.
- Log the inbound turn and resolve its proactive conversation thread.
- If a live participant owns the topic, route the turn there and stop.
- Otherwise try explicit reply handlers, goal enrichment, executive-loop resume, micro-grit context, gap responses, and adaptive-outreach burst handling.
- Anything still unhandled enters the resident topic session.
The participant claim therefore outranks normal reply handlers, including an explicit reply to a proactive message, but clux and bot-level controls remain above it.
Resident request flow
For a normal conversational turn:
- The bot sends
...in the same forum topic. process_request()creates per-turn image staging and injects the send timestamp.- Unseen topic-ledger entries are prepended as
<topic-context>. - The topic system prompt is assembled.
- The turn streams through unified inference under
session_key=telegram-{topic_id}. - Tool/thinking status updates replace the placeholder while work runs.
<telegram-message>content is extracted, inline stream-capture tags are removed, and the final rich message replaces the placeholder.- Outbound text is written to the proactive conversation log.
User-initiated turns allow bare response text when no <telegram-message> tag is
present. System events do not: no tag means the agent chose to stay silent.
Forum topics and resident sessions
Every Telegram forum topic is an independent conversation scope. The Telegram
message_thread_id becomes the string topic_id; traffic without a thread uses the
logical key "0". When forum topics are enabled, logical "0" is mapped back to
the actual General topic ID for delivery.
System prompt layers
build_topic_system_prompt() assembles:
- Topic identity.
- The agent bound to that topic, or the grounding prompt from
topics.json. - The shared Telegram formatting and voice addendum.
- Background-agent instructions.
- Stepwise instructions.
- Message-delivery and thread-control instructions.
- A static time-awareness convention.
The live timestamp and per-turn image path ride on the user turn rather than the
system prompt. This keeps the prompt prefix byte-stable for caching. A true fork stores
a verbatim frozen_system_prompt so it can reuse the parent's context and cache.
New, resume, and fork modes
The session coordinator chooses atomically per session_key:
| State | Mode | Behavior |
|---|---|---|
| No current session | new |
Start and track a new inference session |
| Current session, no request in flight | resume |
Continue the resident transcript |
| Current session, another request in flight | fork |
Fork from the current transcript so both requests can run |
When overlapping branches complete, one becomes current. A response that loses the
race is retained as stale context and injected into the next sequential turn. The
in-memory coordinator is locked; the current session ID is mirrored to
data/telegram/parallel_state.json so bot restarts and detached thread kickoffs can
resume it.
General is configured to reset on the date boundary. Named topics default to
reset_daily: false and persist until reset or deletion. The in-memory invalidation
caveat below means a disk reset is not always a truly fresh next turn.
data/telegram/topics.json also stores names, model overrides, grounding/frozen
prompts, origin metadata, and last activity.
Creating and moving conversations
scripts/telegram_tools.pycreates, renames, closes, reopens, deletes, and lists forum topics.scripts/threadctl.py forkcreates a real session fork with the parent's transcript, persona, model, and frozen prompt.scripts/threadctl.py newcreates a fresh session from a self-contained seed while optionally inheriting the source topic's persona and model.vita.telegram.open_topicexposes bare-topic creation and seeded fresh-topic creation through the capability layer. It does not expose the real-fork verb.
Kickoffs run in a detached process by default, persist their new session ID, and post their result through the durable proactive queue. Topics are closed manually; there is no automatic archive policy.
Model and clux overrides
model N stores a model_override in topics.json and requests a session reset.
The exact selectable models are code-owned and change frequently.
A clux attachment is a stronger routing override: text turns go to the attached clux
coding session before batching or participant routing. Inline keyboards are intended
for attachment selection, with clux and clux detach as text controls.
Live participant claims
A participant is an exclusive, time-bounded claim on bio-zack's conversational turns in one topic. It lets a terminal session, Stepwise flow, or daemon agent open a topic, post progress, and receive steering while work is still running.
The resident session is not deleted or replaced. It stops receiving ordinary user turns while the claim is active, then resumes with the participant exchange injected from the topic ledger. Event-funnel calls can still invoke the resident session during a claim; the exclusivity applies to inbound user routing, not to every system event.
Claim lifecycle
open or choose topic
|
v
register_participant (exclusive claim, default TTL 4h)
|
+----------------------------+
| |
v v
mailbox mode session mode
reply buffered reply resumes session_key
in queue.db response posts to topic
| |
poll / await = heartbeat | no heartbeat expiry
+----------------------------+
|
v
release / deregister / TTL / stale heartbeat
|
v
release notice is posted and ledger-recorded
|
v
resident topic routing resumes
Only one participant may own a topic. Registration conflicts return a structured error naming the current label, mode, and expiry. Different topics can be claimed independently.
Defaults and bounds:
| Setting | Value |
|---|---|
| Default TTL | 4 hours |
| Minimum direct-library TTL | 0.1 hour |
| Maximum TTL | 24 hours |
| Mailbox heartbeat stale threshold | 45 minutes |
| Suggested mailbox polling interval | 40 minutes or less |
participant_cli await default timeout |
30 minutes |
participant_cli await poll interval |
20 seconds |
The proactive response-time loop sweeps expired claims every 15 minutes, so the visible release notice may follow the exact threshold by up to one sweep interval.
Mailbox mode
Mailbox mode is for a live process that can block or poll. Telegram replies are
appended to participant_inbox; polling consumes them by default and refreshes the
registration heartbeat.
Sanitized end-to-end example:
# 1. Create a bare console topic.
./run capabilities call vita.telegram.open_topic \
--input '{"name":"garmin-debug console"}'
# 2. Claim the returned topic.
./run capabilities call vita.telegram.register_participant \
--input '{"topic_id":"12999","label":"garmin-debug session","mode":"mailbox","ttl_hours":2}'
# 3. Post progress through the ledger-aware send path.
./run capabilities call vita.telegram.send \
--input '{"topic_id":"12999","trigger":"participant_update","text":"reproduced the sync failure; checking the cursor now"}'
# 4. Run this as a background shell operation so the harness wakes on reply.
uv run python -m scripts.telegram_parallel.participant_cli await \
--topic 12999 --timeout 1800
# 5. Release when work ends.
./run capabilities call vita.telegram.deregister_participant \
--input '{"topic_id":"12999","reason":"work finished"}'
participant_cli await has explicit outcomes and exit codes:
| Outcome | Exit | Meaning |
|---|---|---|
reply |
0 | One or more replies were consumed and returned as JSON |
| error | 1 | Invalid call or participant error |
timeout |
2 | No reply yet; the claim is still active and await must be re-armed |
released |
3 | Bio-zack released it, the caller deregistered, or a sweep expired it |
The CLI calls the participant module directly on purpose. Sending a WRITE-class capability call every 20 seconds would flood capability provenance. Register, deregister, and outbound send remain capability calls; the blocking await is a transport helper.
Polling with consume=false peeks without consuming, but still refreshes the
heartbeat. A mailbox process that stops awaiting is presumed dead after about 45
minutes and is automatically released. An await timeout does not release it.
Session mode
Session mode is for an inference session already coordinated inside the bot process,
such as a Telegram-spawned background agent. It requires a stable session_key:
./run capabilities call vita.telegram.register_participant \
--input '{"topic_id":"12999","label":"race-planner","mode":"session","session_key":"bg-12999-1234","ttl_hours":4}'
For each routed turn the bot calls stream_inference() with that key. A response in
<telegram-message> tags, or a bare solicited response, is posted back to the topic.
Overlapping replies use the inference layer's normal new/resume/fork coordination.
Session claims do not heartbeat-expire because the original task does not need to
remain attached; only TTL or explicit release ends them. The key-to-session mapping is
process-local for non-telegram-* keys, however. A bot restart or a registration made
by an unrelated process can leave the claim intact without the intended transcript;
use mailbox mode across process boundaries.
What gets ledgered
For every participant-routed turn:
- The resident session receives a later ledger preview such as
[routed to participant 'garmin-debug session'] bio-zack: .... - The user message receives a best-effort
πreaction as a delivery acknowledgment. - Mailbox text is buffered, or the participant session is resumed.
- Session-mode responses and release notices are ledger-recorded automatically.
- Mailbox processes must use
vita.telegram.send(or another ledger-aware path) for their own updates and answers; a raw Bot API call bypasses the invariant.
Plain release is the reliable in-topic escape hatch. The routing function also
accepts /release, but no /release command handler is registered, so slash-command
delivery is not reliable under the current handler filters.
Topic ledger and context continuity
scripts/telegram_parallel/topic_ledger.py is an append-only JSONL ledger for sends
that happen outside the resident topic session. It is a context bridge, not a second
full transcript.
Example record:
{
"ts": "2026-07-12T14:05:31.812903",
"topic_id": "12999",
"trigger": "participant_update",
"text": "reproduced the sync failure; checking the cursor now",
"telegram_msg_id": 42001,
"source": "capability:agent"
}
On process_request() and process_event():
- Read entries for the current topic newer than its
topic_context_tswatermark. - Prepend them as a
<topic-context>block, oldest first. - Run the resident session.
- Advance the watermark only after an inference
COMPLETEevent. A crash or error therefore cannot consume context the agent never saw.
Read bounds keep a fresh or dormant topic from swallowing an unbounded history:
| Bound | Value |
|---|---|
| Text stored per entry | 2,000 characters |
| File tail scanned | 500 KiB |
| Maximum age without an older watermark | 48 hours |
| Entries injected per turn | 12 |
| Combined injected text budget | 4,000 characters, newest entries preferred |
The full outbound audit text remains in data/proactive/conversations.jsonl; the
topic ledger intentionally contains previews. In-session interactive responses and
event-funnel output are not ledgered because the resident session already produced
them. Double-recording them would cause duplicate context on the next turn.
Invariant coverage
The following paths enforce context continuity:
vita.telegram.send- proactive scheduler
send_message() - background-agent acknowledgment, result, and error delivery
- participant-routed inbound turns, participant session replies, and release notices
- seeded/forked thread kickoff messages delivered through the proactive queue
Any code that calls the Telegram Bot API directly without recording the topic ledger can still violate the invariant. New topic-bound send paths should use the capability or proactive scheduler rather than raw HTTP.
Outbound delivery paths
Interactive messages
The bot streams status into the initial ... placeholder. Edits are throttled to one
attempt every 2.5 seconds; a 30-second heartbeat refreshes status during quiet tool
runs. The final response uses Bot API 10.1 rich-message HTML and can contain headings,
tables, lists, collapsible details, footnotes, math, and spoilers.
Rich text supports 32,768 characters. If a rich edit fails, interactive delivery strips tags and falls back to classic plain text, then to a new message if editing also fails.
Event funnel
scripts/proactive/event_funnel.py owns a small set of system events, including
Stepwise completion/failure, session follow-ups, background outcomes, and executive
reflection completion:
event source -> queue.db -> per-topic buffer -> 5s debounce
-> <system-events> prompt -> process_event()
-> resident agent emits <telegram-message> or stays silent
This path is durable at admission, de-duplicates short bursts by fingerprint, and serializes dispatch per topic. It differs from a mechanical reminder: the resident agent sees the event before anything appears in Telegram and decides what is worth saying.
Scheduled and proactive sends
enqueue_message() persists static text or a builder invocation to
data/proactive/queue.db. The scheduler applies admission, timing, vacation,
adaptive-frequency, retry, and trigger-suppression policies before delivery. A
successful mechanical send is recorded in the topic ledger. This is the right path
for reminders and future delivery.
Examples of integrations using it:
- External-email arrival, approval, stale-request, and sent-confirmation notices are
routed to the topic named
Vita Email, with General as a fallback. - Feed discussion replies and RED-tier feed items enqueue desk notifications.
- Goal enrichment, meeting notices, home reminders, and the normal proactive portfolio share the same durable queue and conversation threading.
Direct capability sends
The promoted vita.telegram capability namespace is the stable programmatic surface:
| Capability | Side effect | Purpose |
|---|---|---|
vita.telegram.send |
SEND | Send rich text now; ledger and audit it |
vita.telegram.open_topic |
SEND | Create a bare topic or a seeded fresh topic |
vita.telegram.register_participant |
WRITE | Claim a topic in mailbox or session mode |
vita.telegram.poll_replies |
WRITE | Read/consume mailbox replies and heartbeat |
vita.telegram.deregister_participant |
WRITE | Release the active claim |
Calls return the standard capability envelope and structured errors. See the
generated vita.telegram reference
for the current schemas and examples.
vita.telegram.send attempts sendRichMessage, falls back to classic sendMessage,
stores a cap_* conversation thread mapping, and returns the Telegram message ID.
It is synchronous and immediate; it is not a durable retry queue.
Media delivery
Inbound media
- Voice is downloaded to memory and transcribed by
scripts/integrations/groq_whisper.py. A transcription error is shown to the user; there is no fallback transcriber. - The highest-resolution photo is downloaded under
/tmp/vita-telegram/and its path is given to the agent. Photos and captions participate in normal batching and participant routing. - Temporary inbound files older than 10 minutes are deleted by the reaper. This can race an unusually long-running agent turn.
- Other non-text media are ignored silently in group chats; private chats receive a text-only explanation.
URL auto-ingestion is disabled. Links pass to the agent as normal message text; the
legacy URL-validation helpers in telegram_bot.py are not on the active path.
Outbound images
Each resident turn receives a unique staging directory under
data/generated_images/_turns/. Files written there belong only to that turn and
topic. This fixed the former cross-topic race where a flat-root image could be sent by
multiple concurrent turns.
A new flat-root file in data/generated_images/ is still claimed best-effort under a
process lock and moved into the current turn directory. Short text (1,024 characters
or less) becomes the first photo's caption; longer text remains a rich message and the
photos follow separately. Sent files move to data/generated_images/_sent/; failed or
cancelled turn directories are removed.
Participant and capability send paths do not automatically scan a resident turn's image-delivery directory. They should send media explicitly or route work through a normal topic turn.
Conversation threading and logs
The proactive plane assigns a logical thread_id to outbound messages and stores the
Telegram-message mapping in SQLite. An inbound explicit reply resolves its parent
message; otherwise the most recent thread within the fallback window may be used.
Special IDs identify goal enrichment, micro-grit, home reminders, event-funnel batches,
background work, and capability sends.
Both directions are written to data/proactive/conversations.jsonl. This supports
conversation reconstruction, /sleep analysis, adaptive response metrics, and reply
handlers. Forum topic_id and proactive thread_id are different: the former scopes
an agent session; the latter links an outbound prompt to its response and trigger.
The separate hourly messaging-ingestion pipeline reads personal Signal, Slack, and Telegram DM history into searchable memory. It is a read-only sync path and is not the same system as this Bot API conversation surface. See Search.
Data stores
| Location | Contents |
|---|---|
data/telegram/parallel_state.json |
Topic session IDs, pending calls, stale responses, ledger watermarks, topic feature flag |
data/telegram/topics.json |
Topic names, reset policy, prompt/model inheritance, origins, activity |
data/telegram/topic_ledger.jsonl |
Out-of-session topic context previews |
data/telegram/state.json |
Legacy single-session/status compatibility state |
data/proactive/queue.db |
Scheduled messages, thread mappings, participant claims, participant inboxes |
data/proactive/conversations.jsonl |
Inbound/outbound conversational audit log |
data/proactive/frequency_state.json |
Adaptive source backoff and category state |
data/proactive/response_log.jsonl |
Outreach send/response/timeout tracking |
data/agents/telegram-addendum.md |
Shared Telegram voice and formatting instructions |
data/agents/templates/ |
Topic identity, background-agent, Stepwise, delivery, and thread-control prompt layers |
/tmp/vita-telegram/ |
Temporary inbound photos |
data/generated_images/_turns/ |
Per-turn outbound image staging |
.logs/telegram.log / .logs/telegram.pid |
Rotating runtime log and process ID |
secrets.toml |
Bot token; private and excluded from Git |
Participant tables are created lazily in the proactive SQLite database:
topic_participants
id, topic_id, label, mode, session_key, status, created_by,
created_at, expires_at, last_heartbeat, released_at, release_reason
participant_inbox
id, participant_id, text, telegram_msg_id, created_at, consumed_at
Security and privacy
- The bot is single-tenant. It authorizes by immutable numeric user ID, not username.
- Accepted chats are the authorized private chat and the configured Vita supergroup.
- Unauthorized traffic is silently rejected by normal handlers; command handlers may return an unauthorized message.
- The bot token is loaded from
secrets.toml; it must never appear in logs, docs, or capability input. - Credential-provision replies are stored in Vita's credential vault and then deleted from Telegram best-effort. If deletion fails, the bot warns the user.
- Inference runs with Vita's high-autonomy local permissions. Telegram is therefore a privileged control plane, not a public chatbot.
- Participant labels, replies, session keys, and actor provenance are stored locally. TTL releases routing but does not erase historical claim/inbox rows or ledger lines.
- Personal DM ingestion can include sensitive private/work messages. Public examples and reports must remain sanitized.
Operations and recovery
All service operations go through Vita's service harness:
./run telegram status
./run telegram restart
./run telegram log
Do not use systemctl for the Vita Telegram service.
Runtime recovery
- A 30-second status heartbeat reassures the user during quiet inference.
- No inference event for one hour triggers the inactivity watchdog and cancels the stream.
- The task registry reaps tasks older than two hours and completed registry orphans.
- Disk-backed pending entries let startup and the periodic reaper identify work that died with the bot; placeholders are changed to a crash/resend notice.
- Ledger watermarks advance only after complete inference, preserving unseen context across failures.
- Rich-message errors fall back to classic delivery.
- Participant TTL and heartbeat sweeps release abandoned claims and post a ledger-aware notice.
- Proactive/event-funnel input is queue-backed; immediate capability sends are not.
The bot starts the proactive scheduler, event funnel, zombie/orphan reaper, participant sweeper, spawn-queue watcher, temporary-file cleanup, and periodic memory profiling in its runtime. Shutdown cancels these tasks and closes the inference session registry.
Session reset caveats
There are two reset entry points with different behavior:
- Registered
/resetcalls the legacy command handler and clears global parallel state. - Plain
resetreaches the text quick-control path; in a named topic it clears only that topic and may send its grounding prompt again. In General it performs the legacy/global reset.
The unified inference layer also caches an in-memory session store. Its current disk-refresh logic deliberately keeps an in-memory session when the disk session ID is cleared, treating that state as a possible split-brain. As a result, reset/model-switch can fail to produce a truly fresh next turn until the in-memory store is invalidated or the bot restarts. This is a current implementation limitation, not a documented guarantee.
Known limitations
- Polling drops backlog on restart.
run_polling(..., drop_pending_updates=True)discards Telegram updates accumulated while the bot was down. - Callback queries are not allowed. Credential and clux callback handlers are
registered, but
allowed_updatescontains onlymessageandmessage_reaction. Inline keyboards may therefore never reach those handlers. - Participant exclusivity is inbound-only. Event-funnel work can still invoke the resident session while a claim is active.
- Mailbox continuity is cooperative. A mailbox process must keep awaiting and use ledger-aware sends. A raw Bot API call or dead poller breaks that contract.
- Session-mode keys are process-local. A claim persists in SQLite across a bot
restart, but a non-
telegram-*inference session mapping does not. The next reply can start fresh instead of resuming the intended participant transcript. - Ledger catch-up is bounded. More than 12 entries, more than 4,000 characters, entries older than 48 hours, or content outside the 500 KiB file tail can be omitted.
- Ledger success reporting is imperfect.
topic_ledger.record()logs and swallows write errors to protect delivery, sovita.telegram.sendcannot currently prove thatledger_recorded=truereflects a successful disk append. - Participant history is not pruned. Release changes status but leaves claim and inbox rows in the shared SQLite database.
- Temp-photo cleanup can race long work. Inbound photos expire after 10 minutes.
- Voice has one transcription provider. A Groq failure cannot fall back locally.
- Rich fallback is inconsistent. Interactive fallback strips unsupported rich tags; the direct capability's classic fallback can send the original markup as plain text.
- Some legacy code is still active-adjacent.
state.json, URL-validation code, old status text, and disk/in-memory reset paths describe earlier architectures and can produce misleading operational output. - No automatic topic lifecycle. Named topics and
_sentimages have no archive or retention policy.
Improvement opportunities
Highest-value follow-ups, in order:
- Add participant status/release commands and make callback queries part of
allowed_updates. - Add an explicit inference-store invalidation API and make
/resetand model switching uniformly topic-scoped. - Make ledger writes return success so capability output can truthfully report the context-continuity guarantee; alert on failures.
- Route or wrap every remaining direct Bot API send through one ledger-aware dispatch primitive.
- Replace
drop_pending_updates=Truewith replay-safe startup handling. - Add pruning/retention for participant rows, ledger entries, topic metadata, and sent images.
- Preserve inbound photos until their consuming task completes, rather than deleting solely by age.
- Expose true
forkthroughvita.telegraminstead of requiringthreadctl.py. - Add an alternate/local voice transcription path.
- Add end-to-end tests that run the complete routing precedence: active participant, explicit reply handler, event funnel, release, and resident catch-up.
File map
| File | Responsibility |
|---|---|
scripts/telegram_bot.py |
Polling daemon, auth, handlers, routing precedence, participant dispatch, background delivery |
scripts/telegram_parallel/parallel_processor.py |
Resident request/event inference, ledger injection, rich output, per-turn images |
scripts/telegram_parallel/parallel_state.py |
Topic metadata/session IDs, pending/stale state, watermarks |
scripts/telegram_parallel/batch_accumulator.py |
Per-topic input debounce, albums, wait, stop |
scripts/telegram_parallel/participants.py |
Claim lifecycle, inbox, heartbeat, TTL sweep |
scripts/telegram_parallel/participant_cli.py |
Blocking await/poll/status transport |
scripts/telegram_parallel/topic_ledger.py |
Out-of-session append and bounded context injection |
scripts/telegram_parallel/task_registry.py |
In-memory tasks, cancellation, zombie reaping |
scripts/telegram_parallel/system_prompt.py |
Stable layered topic prompt construction |
scripts/telegram_parallel/rich_message.py |
Raw Bot API 10.1 rich-message helpers |
scripts/capabilities/namespaces/telegram.py |
Promoted send/topic/participant capability namespace |
scripts/proactive/scheduler.py |
Durable queue delivery, ledger-aware mechanical sends, participant sweep |
scripts/proactive/event_funnel.py |
Durable, debounced system-event mediation through resident sessions |
scripts/proactive/queue.py |
Scheduled messages, thread mappings, reply context, participant database |
scripts/threadctl.py |
Detached real-fork and seeded-new topic creation |
scripts/telegram_tools.py |
Telegram forum administration API wrapper |
scripts/telegram_clux.py |
External clux attachment and routing |
scripts/proactive/conversation_log.py |
Inbound/outbound conversation audit |
scripts/external_email/{notify,approval,outbound}.py |
Named-topic email arrival, approval, stale digest, and sent confirmations |
scripts/feed/classifier/routes/dialogue.py |
Full-agent feed replies and Telegram desk notifications |
tests/test_participants.py |
Claim conflicts, modes, polling, heartbeat, TTL, await outcomes |
tests/test_topic_ledger.py |
Recording, topic isolation, watermarks, caps, failure safety |
Change history
- 2026-07-12: Added the
vita.telegramcapability namespace, topic ledger, participant registration, mailbox/session routing, heartbeat/TTL release, and resident-session catch-up. - 2026-06: Added rich messages, per-turn image staging, event-funnel delivery, topic forking/seeding, model overrides, and background-agent result hardening.