Argus conversational chat (Telegram + claude -p) — design & plan¶
Goal: Talk to Argus in natural language from Telegram. A channel in
respond/both mode gets a conversational agent — capture thoughts, ask "what
needs my attention", recall memory, start/triage tasks — backed by claude -p
(the Claude Code subscription, no incremental API cost), which also gives us
the agent loop + MCP tools for free.
Provenance: a Go port of YACT's proven conversational-agent design
(zerocool/YACT docs/plans/2026-05-26-conversational-agent.md,
Yact.Chat.Runner, Yact.Agent.ClaudeProc). The design is language-agnostic;
the Elixir code is reimplemented in Go. Argus is a better fit than YACT was: it
is the memory hub, so the prime digest + tools come straight off its APIs.
Locked decisions¶
claude -presume-per-turn. Each inbound message spawnsclaude -p; we generate the session UUID and pass--session-id <uuid>on turn 1,--resume <uuid>after. No long-lived per-chat process (YACT got bitten by orphanedclaudesubprocesses); survives restarts; retry once as a fresh session if a stored id is dead (expired/pruned/CLI-upgraded). Subscription, not API — the cost lever Aaron wanted.- Runs host-side on .96.
claude+ its auth live on the .96 host (/home/aaron/.local/bin/claude, 2.1.190 — same as the Tdarr-monitor cron); the hub/memory images are distroless (no node/claude), so the turn CANNOT run in-container. A small host-side chat worker on .96 owns theclaude -pcall. The hub stays the single Telegram poller (getUpdates); the worker only sends replies (sendMessage) — no poller conflict. - Queue via a
chat_turnstable. The hub's existing ingest, on arespond/bothmessage, enqueues a pending turn (chat_id, text, session_id). The host worker claims pending turns, runs the turn, posts the reply, persists the new session_id. Decoupled, restart-safe, observable (a stuck turn is a visible row, not a silent hang — the trust-first requirement). - Prime digest, re-primed. A compact (~1-2k token) Argus-state snapshot
(identity + recent decisions/notes + what-needs-attention) injected via
--append-system-prompteach turn, so the agent orients without being amnesiac, then calls tools for live detail. - Lean MCP toolset. A small Argus chat-MCP surface (recall/save/list), NOT shell/file/git. Mutations echo-before-doing. Facts come from tools, not the model's guess.
- General agent first, paired later. Build the cross-project assistant
(channel with no
project), then paired project-scoped chat (channel.project set) falls out by swapping the digest + tool scope.
Carried-over gotchas (already solved in YACT — do not re-discover)¶
- MCP servers load from
~/.claude.jsonper-cwd (claude mcp add); the--mcp-configflag was unreliable in CLI 2.1.x. --permission-mode acceptEditsdoes NOT pre-authorize MCP calls — every chat tool must be in--allowed-toolsexplicitly or it prompts (and there's no human at the CLI to answer).- Bound the turn with a timeout (~120s); reap orphaned subprocesses on timeout.
- Telegram replies are plain text — instruct the agent: no markdown/emoji.
Phasing (each phase = its own PR)¶
A1 — Prime digest (pure, no claude)¶
internal/hub (or internal/memory): ChatPrimeDigest(ctx, project) → a compact
string: identity + recent decisions/notes (from memory recall) + what-needs-
attention (open tasks count + top ready, recent captures). Caps sizes; bounded
length. project=="" → cross-project; a project slug → that project's digest.
- Tests: digest with data + the empty case.
A2 — Turn runner around claude -p (mechanics)¶
ChatTurn(chatID, text, opts): runs one turn through
claude -p --output-format json --append-system-prompt <role+digest>
with --session-id <uuid> (turn 1) / --resume <uuid> (after), a 120s timeout,
and the dead-session retry. The CLI call is an injectable seam so tests use a
fake; the default impl runs the real binary. Returns {reply, session_id}.
- Tests (fake claude): new session generates+passes an id; resume round-trips it;
the system prompt carries the digest; a dead-resume retries fresh; CLI errors
propagate; timeout reaps the subprocess.
A3 — Lean chat MCP server¶
A tiny Argus chat-MCP (stdio or HTTP) exposing read + safe-write tools over the
existing memory/task APIs: recall, save_note, what_needs_attention,
list_tasks, start_task (echo-before-doing). Registered into ~/.claude.json
on .96 and allow-listed in A2's --allowed-tools.
- Tests per tool (handler-level).
A4 — Routing + host worker + ship¶
- Migration:
chat_turnsqueue table (chat_id, text, session_id, status, reply, error, timestamps). - Hub ingest: a
respond/bothchannel's free-text message enqueues achat_turnsrow (slash-commands//notestill take precedence). - Host worker (
cmd/chat-worker, runs on .96 where claude lives): claims pending turns, callsChatTurn, posts the reply via the botsendMessage, persists the returned session_id on the channel, marks the row done/errored. - Heartbeat + staleness alert (a wedged worker is visible, never silent).
- Tests: a
respondfree-text message enqueues; the worker (fake claude) processes a row and records the reply + session.
Phase B — paired (project-scoped) chat¶
A channel with project set → prime with that project's digest + scope tools to
the slug; the agent defaults to that project. Reuses A1-A4 plumbing; only the
context changes (mirrors YACT's Phase C).
Open architecture question for Aaron¶
The host worker is a new long-running process on .96. Options: (a) a small Go
binary under systemd/docker run on the host with claude mounted; (b) a host
process like the Tdarr-monitor pattern. (a) is cleaner + restart-safe. Decide at
A4. Everything before A4 (digest, turn runner, MCP tools) is worker-agnostic and
can be built + tested first.
Sources¶
zerocool/YACT:docs/plans/2026-05-26-conversational-agent.md,lib/yact/chat/runner.ex,lib/yact/agent/claude_proc.ex.- Argus:
internal/hub/telegram_ingest.go,telegram_channels.go(themodefield already models respond/both),telegram.go(Telegram.Send), migration0036_telegram_channels.sql.