Codever is an ACP to Channel bridge. It connects ACP-compatible coding agents to messaging channels, currently Telegram, and exposes Codever-specific MCP tools back to the running agent.
The current implementation is a Telegram Topic Session Gateway with a Semantic Runtime:
Telegram update
-> Telegram handlers
-> SessionManager
-> TopicSession
-> SemanticSessionRuntime
-> AgentProvider / ACP
-> ProviderSemanticAdapter
-> ConversationEvent
-> ChannelProjector
-> DeliveryOutbox
-> TelegramPort
This is the active architecture.
- Bridge, not orchestrator: Codever connects one channel session to one provider session. It does not implement multi-agent supervision.
- Semantic events are the internal contract: provider-specific
AgentEventvalues are normalized intoConversationEventbefore rendering. - Runtime owns turn lifecycle:
SemanticSessionRuntimeowns query/cancel/finalize behavior for a topic session. - Channel delivery is isolated:
ChannelProjectordecides what should be shown, whileDeliveryOutboxserializes send/edit operations. - Telegram details stay behind ChannelPort: message sending, editing, table rendering, topic routing, and inline keyboards are channel responsibilities.
- MCP is an extension boundary: agents interact with Codever through MCP tools and resources, not hidden provider-specific protocols.
Telegram message
-> channel/telegram/handlers/messageRouter.ts
-> find or create TopicSession for chat/topic
-> TopicSession.receiveInput()
-> SemanticSessionRuntime.dispatch({ kind: "user_message" })
-> AgentProvider.startQuery()
-> ACP events stream back
-> ProviderSemanticAdapter.toConversationEvents()
-> ChannelProjector.project()
-> DeliveryOutbox.send/edit()
-> TelegramPort.send/edit()
ACP providers emit tool start/update/result events in provider-specific shapes. The runtime normalizes them into ConversationEvent values with stable toolCallId, phase, toolName, input, output, and optional structured content.
ChannelProjector merges tool updates and produces channel messages. If the channel supports editing, DeliveryOutbox edits the existing tool message; otherwise it can fall back to sending a replacement.
Permission requests are handled at provider/runtime boundaries:
Provider permission request
-> SemanticSessionRuntime.createPermissionHandler()
-> ChannelPort.requestDecision()
-> Telegram inline keyboard
-> Telegram callback handler
-> TopicSession.dispatch({ kind: "decision_response" })
-> provider receives allow/deny
Permission UI is a channel concern. Runtime records the decision event and bridges the result back to the provider.
The daemon hosts a local HTTP API for MCP subprocesses. MCP tools use this API because the MCP server runs in a separate process.
Agent MCP tool call
-> mcp/stdio.ts
-> mcp/tools/*
-> daemon/api.ts
-> Scheduler or SessionManager
-> TopicSession.receiveInput()
-> SemanticSessionRuntime
Scheduled reminders and immediate send-message requests are injected into the same topic-session runtime path as user messages.
The composition root. It registers providers, loads persisted config, starts the scheduler, starts the daemon API, creates the Telegram bot, and shuts down active topic sessions gracefully.
Current handlers live under src/channel/telegram/handlers/ and are registered from src/channel/telegram/bot.ts.
Responsibilities:
- pair and authorize users;
- handle group commands such as
/cwd,/new,/stop,/provider,/resume,/verbose,/timeout; - route Telegram messages to the correct topic session;
- route callback queries back into the session runtime.
Telegram channel ownership is now consolidated under src/channel/telegram.
SessionManager is the runtime registry for group/topic state:
- group CWD and settings;
- topic key normalization;
- active
TopicSessionmap; SessionRecordlookup maps;- provider switch bookkeeping;
- archived topic and cooldown state.
It should remain the single place that answers "which runtime session belongs to this channel topic?"
TopicSession is the bridge object for a Telegram topic. It wraps:
- a
SessionRecordmetadata record; - a session-scoped provider instance;
- a
ChannelPort; - a
SemanticSessionRuntime.
TopicSession.receiveInput() is the public entry point for user/system messages. TopicSession.dispatch() is used for semantic inputs such as commands, cancel requests, and decision responses.
SemanticSessionRuntime is the execution core.
Responsibilities:
- serialize inputs with a mailbox;
- run provider turns;
- track runtime state:
idle,querying,canceling,finalizing,dead; - start, cancel, and finalize provider queries;
- create permission handlers;
- record a
ConversationJournal; - convert provider events into semantic events;
- project and deliver channel messages;
- handle runtime commands such as model/provider/session changes.
This is the main runtime. New lifecycle behavior should usually be implemented here, not in metadata records.
SessionRecord is the lightweight metadata record attached to a topic session.
Current role:
- stores session identity and settings used by handlers;
- exposes
groupChatId,messageThreadId,conversationId, provider name, model, verbose level, timeout, provider settings, and provider commands; - emits session lifecycle events used by manager cleanup paths.
There is no separate session loop in the active architecture.
Provider adapters translate provider-level AgentEvent streams into Codever's internal ConversationEvent model.
This layer absorbs provider-specific quirks, including:
- tool call patch/update shapes;
- missing or generic tool names;
- structured content blocks;
- replayed history from ACP
loadSession; - provider command/config updates.
ACP extension methods keep the same ownership boundary. The shared ACP client manager only exposes generic extMethod and extNotification hooks; it must not encode provider-specific method names. Provider-specific extensions are registered by the concrete provider. For Cursor's agent acp provider, src/providers/agent/cursorExtensions.ts owns all cursor/* parsing and maps it to existing Codever surfaces:
cursor/create_planuses the runtime decision handler for plan approval and renders Cursor todos throughTodoWrite.cursor/ask_questionuses the runtime decision handler and returns Cursor's selected option response shape.cursor/update_todos,cursor/task, andcursor/generate_imageare notifications rendered as existing tool/text events.
If another ACP provider adds extension methods, implement its mapping under that provider directory rather than adding provider-specific cases to runtime/providerAdapter.ts, ChannelProjector, or the shared ACP client.
ChannelProjector converts semantic events into ChannelMessage values.
Responsibilities:
- buffer assistant text until a flush boundary;
- merge tool updates by
toolCallId; - format tool bubbles;
- suppress internal command/config update noise;
- produce status/error messages for completed, cancelled, or failed turns.
The projector should stay channel-aware only through ChannelMessage, not through Telegram API calls.
DeliveryOutbox serializes channel delivery.
Responsibilities:
- order send/edit operations;
- retry Telegram rate-limit errors;
- fall back from edit to send when appropriate;
- record delivery failures for debugging.
TelegramPort implements ChannelPort for Telegram.
Responsibilities:
- send markdown/html/plain messages;
- edit existing messages for progressive tool display;
- render markdown tables as images;
- keep table history for
/tables; - send chat actions;
- request user decisions through inline keyboards.
The active MCP stdio entry is src/mcp/stdio.ts. It registers:
- context resources/tools from
src/mcp/resources.ts; - notify tools from
src/mcp/tools/notify.ts.
Shared registration lives in src/mcp/register.ts; session tools from src/mcp/tools/session.ts are registered only when a daemon/runtime context provides a SessionToolContext.
src/
daemon.ts # composition root
config.ts # persistent config and topic state
bridge/
channelPort.ts # ChannelPort and TopicSession interfaces
sessionManager.ts # session registry and persisted group/topic state
topicSession.ts # Telegram topic -> SemanticSessionRuntime bridge
runtime/
semantic.ts # SessionInput and ConversationEvent model
semanticSessionRuntime.ts # active execution runtime
providerAdapter.ts # AgentEvent -> ConversationEvent
channelProjector.ts # ConversationEvent -> ChannelMessage
deliveryOutbox.ts # serialized send/edit delivery
channel/
telegram/
bot.ts # bot factory, registers handlers
telegramPort.ts # ChannelPort implementation
pairing.ts # user pairing
toolBubble.ts # tool bubble HTML formatting
handlers/ # active Telegram command/callback/message handlers
keyboard.ts # Telegram inline keyboard builders
renderer.ts # markdown/html Telegram renderer
providers/
provider.ts # AgentProvider interface
types.ts # AgentEvent model
registry.ts # provider catalog and per-session factories
acp/ # shared ACP implementation
opencode/ # opencode provider
codebuddy/ # codebuddy provider
agent/ # Cursor agent ACP provider
mcp/
stdio.ts # active MCP stdio server entry
register.ts # shared MCP surface registration
resources.ts # codever context resources/tools
tools/ # notify/session tools
core/
scheduler.ts # timed tasks
eventBus.ts # session lifecycle events
types.ts # session event/state types
Topic-level state is the only active session persistence model. Group state stores shared channel settings such as cwd, model, provider, permission mode, verbosity, and timeout defaults.
The current ownership chain is:
Telegram handlers
-> SessionManager
-> TopicSession
-> SemanticSessionRuntime
-> AgentProvider
-> ProviderSemanticAdapter
-> ChannelProjector
-> DeliveryOutbox
-> TelegramPort
Component ownership:
SessionManagerowns lookup and persistence.TopicSessionowns wiring.SemanticSessionRuntimeowns lifecycle and commands.ProviderSemanticAdapterowns provider normalization.ChannelProjectorowns visible message projection.DeliveryOutboxowns delivery reliability.TelegramPortowns Telegram API details.
Anything outside this chain should either be a utility or a test helper.
- One Telegram topic maps to one active
TopicSession. - One
TopicSessionowns one session-scoped provider instance. - Runtime input is serialized through
SemanticSessionRuntime.dispatch(). - Provider events are normalized before rendering.
- Tool updates are merged by stable tool call id before editing channel messages.
- Channel sends/edits go through
DeliveryOutbox. - Provider switch creates or installs a new provider context and clears incompatible provider session identity.
- Scheduled and MCP-injected messages use the same runtime path as user messages.
- Telegram-specific behavior does not leak into provider adapters.
- Persistence is topic-level for session state; group state stores shared settings such as cwd/provider/model defaults.