Documentation

Architecture

Worker, acp-host, sockets, and store layout.

Goals

  1. Telegram is the UI — one private chat, one topic per agent session
  2. ACP is the agent wire — official TypeScript SDK, stdio spawn
  3. Testable core — pure daemon behind an Environment port
  4. Safe process boundaries — agent children never hold the bot token

Processes

┌─────────────────────────────────────────────────────────────┐
│  Telegram (Bot API, long poll)                              │
└──────────────────────────┬──────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│  acpbot worker  (acpbot worker / acpbot worker)                 │
│  · config.toml (TOML-first)                                 │
│  · allowlist operator                                       │
│  · lobby + topic command routing                            │
│  · session store (store_path)                               │
│  · worker-api.sock  (HTTP over Unix — outbound media)       │
│  · always talks to acp-host.sock (required)                 │
└───────────────┬─────────────────────────────┬───────────────┘
                │                             │
                │  worker → acp-host          │  MCP tools POST
                ▼                             ▼
┌──────────────────────────────┐   ┌──────────────────────────┐
│  acp-host (required)         │   │  Host MCP (stdio child)  │
│  · owns agent stdio slots    │   │  speak / telegram_* /    │
│  · schedule ticker           │   │  schedule_*              │
│  · GET /oauth/callback       │   │  → worker-api.sock       │
└───────────────┬──────────────┘   └──────────────────────────┘


┌──────────────────────────────┐
│  Agent process (ACP stdio)   │
│  grok | claude-adapter | …   │
└──────────────────────────────┘

Worker (src/main.tssrc/core/daemon.ts)

  • Long-polls getUpdates with an explicit allowed_updates list: message, edited_message, callback_query, message_reaction (reactions are not in Telegram’s default set — they must be listed)
  • Maps chat + message_thread_id → session
  • Slash commands never forward to the agent
  • Buffers agent text during a turn; flushes at end (Telegram chunk limits)
  • Indexes outbound bot message_id → session + text preview so reactions can be routed and the agent sees what was reacted to
  • Serves worker API on $state_dir/worker-api.sock

Operator message reactions

When the operator adds/removes a reaction on a bot message in a session topic:

Telegram message_reaction


  worker: resolve session (thread and/or outbound index)


  synthetic prompt → agent turn (or queue if busy)
  [telegram_reaction]
  added / removed / new / old
  === reacted_message ===
  <plain preview of bot text>

All reaction types are forwarded (emoji, custom_emoji, paid, unknown). The outbound index is persisted under $state_dir/outbound-messages.json so reactions still resolve after worker restart or when the agent slot is cold (TTL ~7d, cap ~5000). See Commands.

ACP session host (src/acp/)

Thin client over @agentclientprotocol/sdk:

  • session/new / session/load
  • prompt, permissions, elicitation, _x.ai/ask_user_question
  • Client fs/* + terminal/* (process-group aware terminal manager)
  • Injects MCP servers (repo + built-in acpbot)
  • Rewrites remote http/sse MCP to per-slot acpbot mcp-proxy (official MCP TS SDK; empty tools until OAuth; no agent restart on reauth)
  • Host-side permission gates for fs/* + terminal/* (coalesced; prompts deleted after settle)
  • Model / config options for /model

acp-host (src/acp-host/, required)

Long-lived process that owns agent stdio so the Telegram worker can restart without killing agents. The worker fails boot if the host socket is missing. Also:

  • Scans each [repos] entry for due schedules
  • Serves OAuth callback HTTP when [oauth].callback_base is set

Socket: $state_dir/acp-host.sock

Multi-agent spawn (optional)

A parent topic can spawn child sessions via MCP (agent_spawn, …). Each child is parent-linked, gets its own forum topic, and runs in a new git worktree. See Multi-agent.

Multi-host (optional)

The worker can open more than one host client: local Unix plus remote WSS endpoints from [hosts.*].
Each [repos] entry may set host = "<id>" so agents for that workspace run on the matching machine.
Details: Multi-host.

Background install (acpbot setup)

The guided setup can install both host and worker as user services (same config.toml):

macOS LaunchAgentLinux systemd user
Hostapp.acpbot.host (acpbot host)acpbot-host.service
Workerapp.acpbot.workeracpbot.service

See Configuration.

Environment port

src/core/     pure daemon logic
src/env/      ports + fakes + real telegram / agents / speech / store

Environment bundles telegram, agents, clock, store (and speech when configured). Acceptance tests run against fakes (fakeTelegram, echoAgents, …); production wires realTelegram + realAgents.

On-disk layout

PathContents
~/.config/acpbot/config.tomlPrimary process config (TOML)
store_path (default ~/.local/share/acpbot/store.json)Durable acpbot JSON (sessions registry, offsets, …)
$state_dir/sessions/ACP session records (for session/load) — atomic write + fsync; corrupt files are quarantined so ensure can recover
$state_dir/worker-api.sockOutbound worker HTTP API
$state_dir/acp-host.sockRequired host control socket
$state_dir/mcp-oauth/Pending PKCE + tokens (mode 0600)
<repo>/.acpbot/mcp.jsonPer-repo MCP servers
<repo>/.acpbot/config.jsonOptional repo defaults / mcpProfile
<repo>/.acpbot/schedules/Durable schedule jobs
<repo>/.acpbot-inbox/Inbound media drop (per workspace; ignore in that repo if you use git)

Default state_dir is ~/.local/share/acpbot/state (absolute after load). Worker and acp-host must share the same config / state_dir.

Session model

  1. Lobby /new → pick repo (+ name) → create forum topic (stable title repo/name)
  2. Topic messages become ACP prompt turns (with optional media prep)
  3. Permissions / questions → inline keyboards; answers complete the RPC
  4. /agent may respawn a different process for the same topic
  5. /model uses ACP session/set_model or config options when available

Topic titles are not rewritten for turn status. Live status lives in a single in-topic working bubble (see below).

Turn UX (working bubble)

While a turn is in flight the worker posts one message in the topic and keeps it current:

StateBubble text (examples)
Turn running⏳ Working…
MCP update⏳ … (edits the same message)
Permission / question❓ Waiting for your decision…
Turn ends / cancel / failBubble deleted, then final reply or error
  • Multi-topic / multi-agent safe: status is always in that session’s thread, not a chat-level typing indicator
  • telegram_send / photo / file / speak still create separate permanent messages; the bubble is then re-posted so stays the last message in the topic while the turn runs
  • Session status is still tracked in the store for /status and steer vs prompt; only the Telegram topic name stays fixed

Operator prompt queue

Per-session FIFO in the worker (not host-only):

Input while turn busyBehavior
Free-text / mediaEnqueued; ack with Remove; runs after turn end
/steer <text>Abort in-flight turn (queue kept), start steer turn immediately
/cancelAbort + clear queue

Host promptQueue (acp-host) remains a separate serialization for concurrent host clients. Operator UX is owned by the worker queue. Details: Commands.

Message volume policy

Agent text is buffered until the turn ends, then posted once (chunked to Telegram limits). The ⏳ working bubble is the live progress line; tool-call payloads and diffs are never mirrored as chat text.

Telegram notifications: chatter is silent (disable_notification) — working bubble, queue acks, slash replies, telegram_send. These still notify: final agent reply, failures, plan-ready, permission/ask, EVE done/ask, photos/files/voice, child summaries.

Mid-turn pings still use MCP tools when the agent chooses them (update → edit working bubble; telegram_send, photo, file, speak → permanent messages).

Security notes

  • Single-operator allowlist via CLI pairing (acpbot pair approve; state under $state_dir/pairing/)
  • Bot token only in the worker process
  • Repo path containment for photo/file tools (Worker API)
  • OAuth tokens never written under the repo tree
  • OAuth HTTP listen defaults to 0.0.0.0 when enabled — prefer Tailscale Serve; protection is high-entropy state + PKCE