Skip to content

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:

  1. Topic ledger: proactive sends, direct capability sends, background results, and participant exchanges are recorded for later injection into the resident topic session.
  2. 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:

  1. Validate the numeric Telegram user ID and allowed chat.
  2. Intercept replies that contain requested credentials; store the credential and attempt to delete the sensitive Telegram message.
  3. Handle quick controls such as reset, model/account switching, clux attachment, wait, and stop.
  4. Batch the remaining text, voice transcript, and photo paths by topic.
  5. Log the inbound turn and resolve its proactive conversation thread.
  6. If a live participant owns the topic, route the turn there and stop.
  7. Otherwise try explicit reply handlers, goal enrichment, executive-loop resume, micro-grit context, gap responses, and adaptive-outreach burst handling.
  8. 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:

  1. The bot sends ... in the same forum topic.
  2. process_request() creates per-turn image staging and injects the send timestamp.
  3. Unseen topic-ledger entries are prepended as <topic-context>.
  4. The topic system prompt is assembled.
  5. The turn streams through unified inference under session_key=telegram-{topic_id}.
  6. Tool/thinking status updates replace the placeholder while work runs.
  7. <telegram-message> content is extracted, inline stream-capture tags are removed, and the final rich message replaces the placeholder.
  8. 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:

  1. Topic identity.
  2. The agent bound to that topic, or the grounding prompt from topics.json.
  3. The shared Telegram formatting and voice addendum.
  4. Background-agent instructions.
  5. Stepwise instructions.
  6. Message-delivery and thread-control instructions.
  7. 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.py creates, renames, closes, reopens, deletes, and lists forum topics.
  • scripts/threadctl.py fork creates a real session fork with the parent's transcript, persona, model, and frozen prompt.
  • scripts/threadctl.py new creates a fresh session from a self-contained seed while optionally inheriting the source topic's persona and model.
  • vita.telegram.open_topic exposes 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():

  1. Read entries for the current topic newer than its topic_context_ts watermark.
  2. Prepend them as a <topic-context> block, oldest first.
  3. Run the resident session.
  4. Advance the watermark only after an inference COMPLETE event. 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 /reset calls the legacy command handler and clears global parallel state.
  • Plain reset reaches 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

  1. Polling drops backlog on restart. run_polling(..., drop_pending_updates=True) discards Telegram updates accumulated while the bot was down.
  2. Callback queries are not allowed. Credential and clux callback handlers are registered, but allowed_updates contains only message and message_reaction. Inline keyboards may therefore never reach those handlers.
  3. Participant exclusivity is inbound-only. Event-funnel work can still invoke the resident session while a claim is active.
  4. 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.
  5. 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.
  6. 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.
  7. Ledger success reporting is imperfect. topic_ledger.record() logs and swallows write errors to protect delivery, so vita.telegram.send cannot currently prove that ledger_recorded=true reflects a successful disk append.
  8. Participant history is not pruned. Release changes status but leaves claim and inbox rows in the shared SQLite database.
  9. Temp-photo cleanup can race long work. Inbound photos expire after 10 minutes.
  10. Voice has one transcription provider. A Groq failure cannot fall back locally.
  11. Rich fallback is inconsistent. Interactive fallback strips unsupported rich tags; the direct capability's classic fallback can send the original markup as plain text.
  12. 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.
  13. No automatic topic lifecycle. Named topics and _sent images have no archive or retention policy.

Improvement opportunities

Highest-value follow-ups, in order:

  1. Add participant status/release commands and make callback queries part of allowed_updates.
  2. Add an explicit inference-store invalidation API and make /reset and model switching uniformly topic-scoped.
  3. Make ledger writes return success so capability output can truthfully report the context-continuity guarantee; alert on failures.
  4. Route or wrap every remaining direct Bot API send through one ledger-aware dispatch primitive.
  5. Replace drop_pending_updates=True with replay-safe startup handling.
  6. Add pruning/retention for participant rows, ledger entries, topic metadata, and sent images.
  7. Preserve inbound photos until their consuming task completes, rather than deleting solely by age.
  8. Expose true fork through vita.telegram instead of requiring threadctl.py.
  9. Add an alternate/local voice transcription path.
  10. 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.telegram capability 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.