Architecture
aipager is a single-process asyncio daemon. It bridges three worlds:
your Telegram chat, the Claude Code CLI processes you run under
dtach, and the Claude Code hooks system. Everything funnels through
one event loop — there are no worker pools, no databases, no
inter-process queues.
Component diagram
Loading diagram…
The arrows are all in-process or local — no third-party servers participate beyond Telegram's API. See security for the network surface.
Process model
One asyncio.run invocation in aipager/cli.py:206 runs the whole
show. Inside that:
TelegramBot(aipager/telegram_bot.py) — owns apython-telegram-botApplication. Polls for updates, dispatches to message / callback / voice handlers, emits message edits for busy-state animations.HookReceiver(aipager/hook_receiver.py) — opens a unix datagram socket at/tmp/aipager.sock, decodes JSON payloads from theaipager-hookhelper, dispatches by"event"field.SessionMonitor(aipager/session_monitor.py) — wakes every 2 s to scan/tmp/claude-dtach-*.sock, reconcile against the registry, and time out stuckINTERACTIVEsessions (AIPAGER_INTERACTIVE_TIMEOUT).SessionRegistry(aipager/state.py) — in-memory dict ofTrackedSessionobjects keyed by name. Serializes to~/.claude/aipager-sessions.jsonon shutdown and on every state transition that matters.ObserverBroadcaster(optional,aipager/observer.py) — ifAIPAGER_OBSERVERSis set, also forwards messages to read-only observer bots.
All four run as async tasks on the same loop. No threads (except the faster-whisper executor for voice transcription, which is fire-and- forget per call).
Boot sequence
From aipager/cli.py:159-184:
SessionRegistry.load()— reads~/.claude/aipager-sessions.json, rehydratesTrackedSessionobjects, drops stale queue entries (>24 h).TelegramBot.__init__+bot.start()— verifies token / chat, starts the polling loop.ObserverBroadcaster.start()— only when configured.HookReceiver.start()— unlinks any stale/tmp/aipager.sock, binds fresh, listens.bot.recover_sessions()— for everyBUSYsession whosebusy_msg_idexists, edit the Telegram message to reflect the live state. Skipsvanished,too_old,floodedcases gracefully.SessionMonitor.start()— begins the 2 s tick.- Daemon enters its
asyncio.Eventwait, ready for signals.
Shutdown sequence
From aipager/cli.py:186-193:
- SIGINT or SIGTERM sets the
stopevent. registry.save()— persist state.session_monitor.stop()— cancel the tick task.hook_receiver.stop()— close the datagram transport andos.unlink(/tmp/aipager.sock).observers.stop()if running.bot.stop()— cancel per-session animation tasks, stop theApplication(which flushes pending edits).- Process exits cleanly.
The Telegram-driven self-restart in
telegram_bot.py:_restart_daemon relies on this entire path running
to completion before the spawned replacement binds the socket. See
bot commands → restart for the user-facing
behaviour.
File and socket layout
| Path | Purpose | Owner |
|---|---|---|
/tmp/aipager.sock | Unix datagram for hook events | aipager daemon (binds) |
/tmp/claude-dtach-<name>.sock | dtach control socket per session | dtach |
/tmp/claude-status-<name>.json | Statusline data per session | aipager-statusline hook |
~/.claude/aipager-sessions.json | Durable registry state | aipager daemon |
~/.claude/aipager-audit.jsonl | Allow / Deny / answer log | aipager daemon (append-only) |
~/.claude/settings.json | Claude Code hook config | written by aipager config |
~/.claude/settings.json.bak.* | Backups before each rewrite | aipager config |
~/.config/aipager/config.env | Bot token, chat ID, observer bots | aipager config (mode 600) |
~/.config/aipager/keyboard.json | Optional keyboard overrides | user |
The daemon writes nothing outside ~/.config/aipager, ~/.claude/,
and /tmp/aipager.sock. It never elevates — see
security.
Why dtach
dtach gives each Claude Code session a real PTY without binding it
to a terminal that has to stay open. The aipager daemon attaches
non-interactively via dtach -a -E to read the output stream and
inject keystrokes; the user can also attach interactively from any
shell via aipager session <name> for direct access.
The result: Claude Code runs as if you typed in a terminal, but
that terminal can come and go without disturbing the running session.
The daemon discovers sessions by scanning /tmp/claude-dtach-*.sock
on each 2 s monitor tick.
See also
- Hook events — what flows in over
/tmp/aipager.sock. - Bot commands — what flows in from Telegram.
- Security model — privilege boundary, secrets, audit.
- Troubleshooting —
aipager doctorreference.