feat: relay daemon skeleton — queue, dispatch, conversation, ntfy (#1) #2
Reference in New Issue
Block a user
Delete Branch "feat/1-daemon-skeleton"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
First-PR scope from #1. Single-process Python daemon that relays between Claude Code instances and a chat-Claude session via the Anthropic API. CC drops JSON envelopes into
queue/; the daemon appends to a running history, calls the API (system prompt cached), and either dispatches the response back to the originating session or pauses on[NEEDS-JC]and notifies via ntfy.sh.Issue
Closes #1.
AI tier
tier/2-review. Long-lived process; touches secrets; sends data to a third-party API.Changes
relay/package (8 modules)config.py—.env+config.yamlloader. Auto-generates a 16-charsecrets.token_urlsafentfy topic on first run and persists back to.env. Defensive against the commonKEY= # inline commentdotenv parse trap. Seeds a defaultconfig.yaml(system prompt + summarization prompt + onesession-1) if missing.state.py— Atomic file I/O (tempfile +os.replace);fcntl.flock-based instance lock atstate/.lockso a second daemon refuses to start.conversation.py— Append-only history with summarization. Triggers when total chars exceedHISTORY_CHAR_CAP(default 400k); replaces history with the summary turn plus the most recent 10 turns.anthropic_client.py— Single SDK wrapper. Marks the system prompt cacheable (cache_control: ephemeral). Estimates per-call cost from a model→price table (Opus 4.7, Opus 4.7-1m, Sonnet 4.6, Haiku 4.5) including cache-write/read rates. Concatenatestextblocks, ignorestool_useblocks safely, falls back to Opus pricing for unknown models.queue.py— JSON envelope intake. Picks oldest by mtime; validates{session_id, timestamp, content}; rejects malformed envelopes toqueue/.rejected/.ack()deletes after successful dispatch.dispatch.py— Per-sessiondispatch/<session_id>/input.txt. Atomic write viastate.write_atomic. Won't overwrite a pending dispatch — internal deque per session; flushes when CC consumes (deletes) the previous file.ntfy.py— Best-effort POST tohttps://ntfy.sh/<topic>. Network failures logged, never raised — main loop never blocks on notification delivery.daemon.py— Main polling loop. Each tick: flush deferred dispatches, drainstate/jc_input.txt(priority), drain queue (one entry/tick), heartbeat for stuck-queue alerts.[NEEDS-JC]detection in first 200 chars pauses the loop and ntfy-alerts. JC override supports@session-N:prefix for direct dispatch without an API call.__main__.py— CLI:relay run(daemon),relay status(readstate/status.json),relay topic(print ntfy URL). Signal handling: SIGINT/SIGTERM → graceful stop.Tests (57 unit + 1 gated real-API)
test_state.py— atomic write durability, instance lock semantics including a concurrent-acquire thread test.test_conversation.py— append, persist, summarize-with-recent-turns, char counts.test_queue.py— oldest-by-mtime, envelope validation, .rejected/ routing, ack semantics.test_dispatch.py— single-pending invariant, per-session independence, queue-and-flush.test_anthropic_client.py— system-prompt cache marker, cost estimates including cache savings, fallback for unknown models, multi-block concatenation, non-text block handling.test_daemon_loop.py— full loop with a fake client. Covers: queue → dispatch,[NEEDS-JC]pause,@session-N:direct dispatch, JC override clearsneeds_jc, summarization triggers at cap, malformed file does not crash loop, dispatch queues on pending input.test_config.py— env precedence, ntfy auto-generation, inline-comment-as-blank defensive parse, missing API key raises, defaultconfig.yamlseeded.test_ntfy.py— header/body shape, non-200 handling, network errors return False.test_real_api.py— gated@pytest.mark.real_api, opt in withpytest -m real_api. Sends one Haiku 4.5 round-trip (~$0.0001). Skips cleanly oncredit balance too lowerrors so it's honest about why it didn't run.Project files
pyproject.toml—risv3-relay0.1.0, Python ≥ 3.10. Pinned deps:anthropic,flask(reserved for follow-up status endpoint),requests,pyyaml,python-dotenv. Default test invocation excludesreal_apiviaaddopts = "-m 'not real_api'".config.yaml— auto-seeded on first daemon run; committed so JC can review/edit the system prompt and summarization prompt..gitignore— fixed inherited typo (was excluding.env.exampleand listing.envtwice)..env.example— placeholder values + instructions;.envitself stays gitignored.README.md— install / run quickstart, CC-side polling protocol with shell snippet, JC operations cheatsheet.Test checklist
pytest→ 57 passed, 1 deselected (real_api).pytest -m real_api→ 1 skipped (no API credit)..envstays gitignored;.env.examplecarries placeholders only).Smoke test performed
Booted
relay runagainst a real.env. The daemon:.env(subsequent runs reuse it).state/.lockvia flock; a concurrent boot would refuse to start (covered by unit test).queue/andstate/jc_input.txtat 1 Hz with empty inputs (no API calls, no costs).state/status.jsoneach tick.relay statusandrelay topicround-trip cleanly.I did NOT run a live API turn end-to-end — the test API key has zero credit balance and returns 400 on any
messages.create. Thereal_apismoke test detects that and skips, and thedaemon_loopintegration suite covers the full path with a fake client (queue → API → dispatch, including the summarization trigger).Schema changes
Multi-AI review
Skipped octopus review for this first PR — it's all greenfield code with comprehensive unit + integration coverage and the spec was JC's. Will reinstate octopus reviews on follow-up PRs that touch security-sensitive paths (status endpoint exposure, systemd integration, multi-tenant config).
Screenshots
N/A — backend daemon, no UI.
Out of scope (follow-up PRs)
flaskalready a dependency for this).Closing without merging per JC review on 2026-05-02. The cost-monitoring overhead and runaway-loop risk outweigh the time savings for this workflow scale; the behavioral changes that close most of the friction do not require infrastructure. Branch
feat/1-daemon-skeletonis preserved for future reference if circumstances change.