Architecture¶
Runtime Overview¶
sygen supports multiple messaging transports. The transport config field ("telegram" or "matrix") selects a single ingress/delivery layer via a transport registry (messenger/registry.py). The transports list field enables parallel multi-transport execution (e.g. ["telegram", "matrix"]); when empty it falls back to the single transport value.
Telegram path: Matrix path:
Telegram Update Matrix sync event
-> aiogram Dispatcher/Router -> matrix-nio callback
-> AuthMiddleware -> room/user allowlist check
-> SequentialMiddleware -> MatrixBot handler
-> TelegramBot handler -> Orchestrator
-> Orchestrator -> CLIService
-> CLIService -> provider subprocess
-> provider subprocess -> Matrix room message
-> Telegram message (stream edits)
Background/async results (both transports):
-> Observer/TaskHub/InterAgentBus callback
-> bus.adapters -> Envelope
-> MessageBus
-> optional lock + optional session injection
-> transport-specific delivery (TelegramTransport or MatrixTransport)
When transports lists more than one entry, MultiBotAdapter starts all transports in parallel and exposes a unified BotProtocol to the orchestrator.
Direct API path (api.enabled=true) uses ApiServer and calls orchestrator streaming callbacks directly.
Transport dispatch¶
messenger/registry.py maps config.transport to a bot factory:
"telegram"->TelegramBot(aiogram)"matrix"->MatrixBot(matrix-nio)
Both implement BotProtocol. Adding a new transport requires only a new factory entry.
Startup Flow¶
sygen entry (sygen_bot/__main__.py)¶
- parse CLI args and dispatch command (implementation in
cli_commands/*) - default run path:
_is_configured()only checks the minimal onboarding gate for active transports- Telegram gate: non-placeholder token + non-empty
allowed_user_ids - Matrix gate: non-empty
homeserver+ non-emptyuser_id - deeper transport validation (Matrix password/access token + allowlists) happens later in
_validate_*_config() - if not configured: onboarding (includes transport selection)
- load/deep-merge config (
load_config()) - initialize workspace (
init_workspace(paths)) - run supervisor via
run_bot(config)(transport-agnostic) run_bot()acquires PID lock and startsAgentSupervisor
Supervisor startup (multiagent/supervisor.py)¶
- start
InterAgentBus - start
InternalAgentAPI - optional shared
TaskHub(tasks.enabled=true) - create/start main
AgentStack - wait for main readiness (
_main_ready) - load/start sub-agents from
agents.json - start
SharedKnowledgeSync - start
agents.jsonwatcher - block on main completion and return its exit code
Bot startup (Telegram: messenger/telegram/startup.py, Matrix: messenger/matrix/startup.py)¶
Telegram startup:
- create orchestrator (
Orchestrator.create(...)) - initialize chat tracker (
chat_activity.json) - seed
TopicNameCachefrom persisted sessions and wire topic name resolver intoSessionManager - consume restart sentinel and optional upgrade sentinel
- wire observers to message bus (
orch.wire_observers_to_bus(...)) - register config hot-reload callback for auth/group updates
- startup classification (
first_start/service_restart/system_reboot) + startup notification policy - recovery planning (
inflight_turns.json+ recovered named sessions) - start update observer (upgradeable installs only, main agent only), sync Telegram commands, start restart watcher
- run group audit immediately + start periodic 24h audit loop
Matrix startup follows a similar pattern (orchestrator creation, bus wiring, observer startup) but uses matrix-nio's AsyncClient sync loop instead of aiogram polling. UpdateObserver starts only for the main agent on both transports (sub-agents skip it).
Orchestrator factory (orchestrator/lifecycle.py)¶
- resolve paths and set
SYGEN_HOMEfor main agent - optional Docker setup + Docker-mode skill resync
- inject runtime environment note into workspace rule files
- instantiate
Orchestrator - check provider auth and apply provider availability
- initialize model cache observers (Gemini + Codex)
- initialize task observers (
BackgroundObserver,CronObserver,WebhookObserver) - start observers (
cron,heartbeat,webhook,cleanup) + rule/skill watchers - optional API server startup
- start config reloader
Command Ownership and Routing¶
Bot-level handlers (messenger/telegram/app.py):
/start,/help,/info,/showfiles,/stop,/stop_all,/interrupt,/restart,/new,/session,/sessions,/tasks,/agent_commands- main-agent-only handlers:
/agents,/agent_start,/agent_stop,/agent_restart
Matrix command ownership (messenger/matrix/bot.py):
- direct transport commands:
!stop,!stop_all,!interrupt,!restart,!new,!help,!info,!session,!showfiles,!agent_commands - orchestrator-routed commands:
!status,!model,!memory,!cron,!diagnose,!upgrade,!sessions,!tasks - main-agent-only multi-agent commands:
!agents,!agent_start,!agent_stop,!agent_restart(/prefix also supported)
Orchestrator command registry (orchestrator/commands.py):
/new,/status,/model,/topicmodel,/memory,/cron,/diagnose,/upgrade,/sessions,/tasks- multi-agent commands are registered at runtime by supervisor hook
Abort behavior:
/stopand/stop_allare handled before normal lock routing- main-agent
/stop_alluses supervisor callback to abort across all stacks
Quick-command bypass (SequentialMiddleware):
/status,/memory,/cron,/diagnose,/model,/topicmodel,/showfiles,/sessions,/tasks,/where,/leave
Session and Topic Model¶
Sessions are keyed by SessionKey(transport, chat_id, topic_id).
- Telegram forum topics are isolated from each other and from the base chat
- Matrix rooms use
transport="mx"with deterministic int room mapping - API sessions use
transport="api"and optionalchannel_id -> topic_id sessions.jsonremains backward-compatible with legacy unprefixed keys- topic names are cached from forum topic events and shown in
/statusand/sessions /new @topicnameresets a specific topic session without switching to that topic
Provider isolation inside a session:
- each session has provider-local buckets (
provider_sessions) - switching provider/model preserves other provider buckets
/newresets only the active provider bucket
Per-topic /model behavior:
- inside a topic, model/provider switch updates that topic session only
- global config (
config.json/agents.json) is updated only outside topic scope
Per-topic default model (/topicmodel):
/topicmodel [model]sets a persistent default model for a specific topic- stored in
config.jsonundertopic_defaults: {"<topic_id>": {"model": "<model_id>"}} - resolution priority:
@directive override>topic_default>global config.model - new sessions in the topic automatically use the topic default
- works with any provider (Claude, Gemini, Codex)
Flow Details¶
Normal and streaming flows (orchestrator/flows.py)¶
- resolve runtime target (provider/model)
- resolve session by
SessionKey - new session: append
MAINMEMORY.md(+ agent roster context if available) - apply message hooks
- build
AgentRequestwithtopic_id - persist in-flight foreground turn (
InflightTracker.begin) - execute CLI (
executeorexecute_streaming) - session recovery (single retry) on:
- SIGKILL
- invalid resumed session
- update session metrics and ID on success
- clear inflight marker in
finally
Gemini safeguard:
- if Gemini is in API-key mode and
gemini_api_keyis empty/null, flow returns warning text and skips CLI execution.
Heartbeat flow¶
- read-only active-session lookup (no create)
- skips when no session, provider mismatch, or cooldown not reached
- executes prompt with session resume
- suppresses pure ACK responses
- updates session only for non-ACK alerts
Named sessions (/session)¶
BackgroundObserverexecutes named session turns asynchronously- follow-up support:
- foreground:
@session-name <message> - background:
/session @session-name <message> /sessionsinteractive management via selector callbacks
Delegated tasks (TaskHub)¶
- shared registry:
~/.sygen/tasks.json - folders:
~/.sygen/workspace/tasks/<task_id>/ - endpoints via internal API (
/tasks/*) - topic-aware routing: task results/questions retain
thread_idand are injected back into originating topic session - task tools receive
SYGEN_CHAT_IDand optionalSYGEN_TOPIC_ID - single-task permanent delete:
/tasks/delete+TaskRegistry.delete()
MessageBus and Delivery¶
MessageBus replaces fragmented delivery paths.
Envelopecaptures origin, lock mode, injection requirements, delivery mode- observers are wired in one call:
ObserverManager.wire_to_bus(...) - Telegram transport formatting is centralized in
messenger/telegram/transport.py - shared Telegram/message-bus
LockPoolprevents lock drift across middleware and background delivery ApiServercurrently uses its ownLockPool, so API locking is isolated from Telegram/message-bus locking- Transport-aware delivery: each
Envelopecarries atransportfield; UNICAST envelopes are routed only to the matching transport, with cascading fallback to other transports when the target is unavailable
Callback Query Routing¶
Special callback namespaces:
mq:*queue cancelupg:*upgradems:*model selectorcrn:*cron selectornsc:*session selectortsc:*task selectorns:*named-session follow-upsf:*/sf!file browser
Selector callbacks use transport-agnostic selector types (Button, ButtonGrid, SelectorResponse) from orchestrator/selectors/models.py.
API Architecture¶
ApiServer (api/server.py) provides:
- websocket auth + E2E (
type=auth,token,e2e_pk) - optional auth-time session overrides:
chat_id(required > 0 when provided)channel_id(maps toSessionKey.topic_id)- encrypted message streaming events (
text_delta,tool_activity,system_status,result) - encrypted abort
- bearer-auth HTTP endpoints (
/files,/upload)
Restart and Shutdown¶
Restart triggers:
/restartsentinel + exit code42- external restart marker file (auto-writes sentinel from last active session so the restart is still announced)
- main-agent restart propagates to process/service level
Shutdown (orchestrator/lifecycle.shutdown):
- kill active CLI processes
- stop API server
- cleanup managed skill links
- stop observers + config reloader + cache observers + watchers
- optional Docker teardown
Workspace Seeding Model¶
Source: sygen_bot/_home_defaults/.
Zone rules (workspace/init.py):
- Zone 2 overwrite:
CLAUDE.md,AGENTS.md,GEMINI.md- tool scripts under
workspace/tools/{cron,webhook,agent,task}_tools/*.py - Zone 3 seed-once for other files
RULES*.mdtemplates are selected/deployed byRulesSelector
Rule sync:
- recursive mtime sync for
CLAUDE.md,AGENTS.md,GEMINI.md - task-folder provider rules backfilled by
ensure_task_rule_files(...)
Multi-Agent Notes¶
- sub-agents are full stacks with own transport credentials/workspace/session files (each sub-agent can use a different transport)
- all stacks share one event loop, inter-agent bus, and optional shared task hub
- async inter-agent results are injected via bus envelopes
- provider switch during
ia-<sender>conversations auto-resets that named session and surfaces a provider-switch notice