First-PR scope from #1. Single-process Python daemon that relays between Claude Code instances and chat-Claude (Anthropic API). Components: * relay.config — .env + config.yaml loader. Auto-generates ntfy topic on first run and persists it back to .env. * relay.state — atomic file I/O via tempfile + rename, advisory flock at state/.lock to enforce single-instance. * relay.conversation — append-only history with summarization. Triggers a summarize call when total chars exceed HISTORY_CHAR_CAP (default 400k); replaces history with the summary plus the most recent 10 turns. * relay.anthropic_client — SDK wrapper. Marks the system prompt cacheable (5-min ephemeral cache); concatenates text blocks; estimates per-call cost from the Anthropic price table with cache-write/read accounted for. * relay.queue — JSON envelope intake; oldest-by-mtime; malformed envelopes moved to queue/.rejected/. * relay.dispatch — one-input-at-a-time per session (dispatch/<session_id>/input.txt). Won't overwrite a pending dispatch; queues internally and waits for CC to delete. * relay.ntfy — best-effort POST to https://ntfy.sh/<topic>; failures logged but never block the main loop. * relay.daemon — main loop. Polls jc_input.txt (priority) then queue/. Detects [NEEDS-JC] in the first 200 chars of any response and pauses dispatch until JC writes jc_input.txt. JC override supports @session-N: prefix for direct dispatch without an API call. * relay.__main__ — CLI: relay run / relay status / relay topic. Tests: 57 unit tests pass (config, state, conversation, queue, dispatch, anthropic_client, ntfy, full daemon loop with a fake client). One real-API smoke test marked real_api, opt-in via pytest -m real_api; skips cleanly on credit-balance errors. Out of scope for this PR (deferred to follow-ups): Flask status endpoint, multi-session config in production, exponential backoff, systemd unit, cost-tracking aggregation. Closes #1. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
risv3-relay
Relay daemon between Claude Code instances and a Claude.ai chat-equivalent session via the Anthropic API.
What it does
When CC produces output that would normally be pasted to a Claude.ai chat for review or a decision, the daemon does the relay automatically:
- CC drops a JSON envelope into
queue/. - Daemon picks oldest-first, appends to a running conversation history, calls the Anthropic API with prompt caching on the system prompt.
- If the response contains
[NEEDS-JC]in its first 200 characters, the daemon pauses and notifies via ntfy.sh. - Otherwise, the response is written to
dispatch/<session_id>/input.txtfor the originating CC session to consume.
JC can override at any time by writing to state/jc_input.txt.
Install
git clone git@localhost:AC/risv3-relay.git
cd risv3-relay
python3.14 -m venv .venv
.venv/bin/pip install -e '.[dev]'
cp .env.example .env
# edit .env — add ANTHROPIC_API_KEY
Run
.venv/bin/python -m relay run
On first boot the daemon generates a random ntfy topic, persists it back to .env, and prints the subscription URL:
ntfy topic: https://ntfy.sh/<random-16-chars>
Subscribe on phone/laptop to receive needs_jc + error alerts.
Subscribe at that URL on phone/laptop to get pings on needs_jc and errors. The topic is functionally a password — anyone subscribed receives the messages, so don't share it.
CC-side protocol
A CC session integrates with the daemon by dropping queue envelopes and polling its dispatch input.
Sending CC output to chat-Claude
Drop a JSON file into queue/:
{
"session_id": "session-1",
"timestamp": "2026-05-02T15:30:00Z",
"content": "Sub-PR A is open at #438. Tests pass. Awaiting review."
}
File name doesn't matter (use <session_id>-<unix_ts>-<rand>.json for sortability). The daemon picks oldest-first by file mtime and processes one entry per loop tick.
Receiving chat-Claude responses
Poll your session's dispatch directory:
while true; do
if [ -s dispatch/session-1/input.txt ]; then
cat dispatch/session-1/input.txt
rm dispatch/session-1/input.txt
fi
sleep 1
done
Deletion is the acknowledgement. The daemon will not write a new input.txt until the previous one is consumed (deleted).
JC operations
Status:
.venv/bin/python -m relay status
ntfy URL:
.venv/bin/python -m relay topic
Override at any time — the daemon picks up state/jc_input.txt on the next tick (≤ 1 second):
| Format | Effect |
|---|---|
@session-1: do X |
Direct dispatch to session-1. No API call. Body after the prefix becomes the input. Clears needs_jc if set. |
(any text without @prefix) |
Treated as the next chat-side turn. The daemon sends it through the API, dispatches the response to the originating session (or sessions[0] if not in queue context). Clears needs_jc if set. |
Project layout
relay/— Python package (config, state, conversation, anthropic_client, queue, dispatch, ntfy, daemon, main)tests/— pytest tests;pytest -m real_apiopts into live-API smokequeue/,dispatch/,state/,logs/— runtime directories created on first run; gitignoredconfig.yaml— registered CC sessions, system prompt, summarization prompt; auto-seeded on first run.env— secrets and per-host overrides; gitignored
Trust model
- The daemon is pure transport between CC and chat-Claude. It does not make merge decisions, override CC's existing rules, or run arbitrary commands.
- The Anthropic API call is the only outbound integration besides ntfy. The system prompt and summarization prompt live in
config.yaml; edit them to shape chat-Claude's behavior. - JC's
jc_input.txtis authoritative — anything written there is treated as the next chat-side turn (or a direct dispatch with the@session-N:prefix).
Status of the project
First-PR scope (this repo's main after merge): daemon skeleton, queue + dispatch loop, single-CC-session integration, basic logging, ntfy notifications, conversation history with summarization, prompt caching on the system prompt, per-call cost estimation logged.
Follow-up PRs will add: status web UI (Flask endpoint), multi-session config and per-session prompts, exponential backoff on transient API errors, systemd unit, cost-tracking dashboard.
Development
Run the unit suite:
.venv/bin/python -m pytest
Run the live-API smoke (cost ~$0.0001 against Haiku 4.5; needs a billed ANTHROPIC_API_KEY):
.venv/bin/python -m pytest -m real_api
Lint / format:
.venv/bin/ruff check relay/ tests/
.venv/bin/ruff format relay/ tests/