Architecture
arc has six major components that work together to accept prompts from multiple sources and route them to the correct backend.
Components
arc CLI (arc.cli)
The arc command is a Typer application that exposes all user-facing subcommands. For most operations that involve dispatching a prompt, the CLI first attempts to contact the daemon via Unix socket IPC. If the daemon is not reachable and daemon.auto_start is true, the CLI spawns the daemon as a detached background process and retries. If still unreachable, it falls back to direct dispatch in the calling process.
This means arc ask works correctly regardless of whether the daemon is running.
ArcDaemon (arc.daemon)
The daemon is an asyncio event loop that:
- Binds a Unix domain socket at
~/.arc/arc.sock(configurable) - Writes its PID to
~/.arc/daemon.pid - Registers SIGTERM and SIGINT handlers for graceful shutdown
- Starts the Discord bot as a background task if Discord is enabled
- Starts the CronManager and registers all enabled jobs
- Serves IPC connections indefinitely
The daemon holds two pieces of in-memory state: the Discord bot reference and a model_overrides dict mapping Discord channel IDs to user-selected models (set with the /model command).
All prompt handling flows through ArcDaemon.handle_request, which is called by IPC clients, the Discord bot, and the cron runner.
Dispatcher (arc.dispatcher)
The dispatcher is the routing layer. Given a prompt, an agent config, and an optional model override, it:
- Resolves the effective model (override or agent default)
- Validates the model against
allowed_modelsif the list is non-empty - Routes to
dispatch_acpxfor any model not starting withollama/, ordispatch_ollamaforollama/*models - Returns a
DispatchResultwith the output text, the model that was used, and the dispatch type
acpx path: Builds a system prompt by reading and concatenating system_prompt_files from the agent workspace. Writes the prompt to a temporary file (to avoid shell escaping issues with multi-line prompts). Calls acpx --format quiet --cwd <workspace> --model <model> <perm_flag> claude exec --file <tmpfile> for one-shot dispatch, or manages a named session for Discord threads.
Ollama path: Builds the same system prompt and appends any local_context_files as an additional system message. Posts to the Ollama-compatible /v1/chat/completions endpoint via httpx.
CronManager (arc.cron)
Wraps APScheduler's AsyncIOScheduler. On daemon start, it reads ~/.arc/cron/jobs.yaml, adds each enabled job as a CronTrigger.from_crontab(schedule) job, and starts the scheduler. When a job fires, it calls ArcDaemon.run_cron_job, which routes the job prompt through the standard handle_request path (including Discord notification and log appending).
Discord bot (arc.discord_bridge)
A discord.py Client subclass. On each message:
- Ignores messages from itself and from guilds that do not match
guild_id - Looks up which agent is bound to the channel (or the thread's parent channel)
- Checks
require_mentionand the per-channel rate limiter - If
thread_modeis enabled and the message is not already in a thread, creates a thread and uses it as the reply target - Calls
ArcDaemon.handle_requestwithsource=discordand the thread ID, which enables namedacpxsessions for multi-turn conversation - Splits long responses into 2000-character chunks and sends them
The /model command is intercepted before dispatch: it reads or sets the daemon's in-memory model_overrides dict for the channel.
IPC (arc.ipc)
A minimal framing protocol over Unix domain sockets. Every message is a 4-byte big-endian unsigned integer length prefix followed by a UTF-8 JSON payload. The request function handles the full connect-send-receive-close cycle and returns None if the daemon socket is not reachable.
File layout
~/.arc/
config.yaml Main configuration
.env Secrets (DISCORD_BOT_TOKEN, etc.) -- mode 600
daemon.pid PID file written by the daemon
arc.sock Unix domain socket (exists only while daemon runs)
agents/
<name>.yaml One file per agent
cron/
jobs.yaml All scheduled jobs
logs/
routing.jsonl One JSON record per dispatched prompt
cron.jsonl One JSON record per cron job run
Request lifecycle
- User runs
arc ask --agent coach "What's my workout today?" - CLI calls
ipc.request(cfg, {prompt, agent, model, source: "cli"}) - Daemon receives the request, loads the agent config, resolves model and session
- If
git.auto_pullis true,git pullruns in the agent workspace - Dispatcher builds the system prompt, calls
acpxor Ollama, returnsDispatchResult - Daemon logs the routing record to
routing.jsonl - Daemon sends
{status: "ok", result: <output>}back over the socket - CLI prints the output