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>
137 lines
4.6 KiB
Markdown
137 lines
4.6 KiB
Markdown
# 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:
|
|
|
|
1. CC drops a JSON envelope into `queue/`.
|
|
2. Daemon picks oldest-first, appends to a running conversation history, calls the Anthropic API with prompt caching on the system prompt.
|
|
3. If the response contains `[NEEDS-JC]` in its first 200 characters, the daemon pauses and notifies via [ntfy.sh](https://ntfy.sh).
|
|
4. Otherwise, the response is written to `dispatch/<session_id>/input.txt` for the originating CC session to consume.
|
|
|
|
JC can override at any time by writing to `state/jc_input.txt`.
|
|
|
|
## Install
|
|
|
|
```sh
|
|
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
|
|
|
|
```sh
|
|
.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/`:
|
|
|
|
```json
|
|
{
|
|
"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:
|
|
|
|
```sh
|
|
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:
|
|
|
|
```sh
|
|
.venv/bin/python -m relay status
|
|
```
|
|
|
|
ntfy URL:
|
|
|
|
```sh
|
|
.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_api` opts into live-API smoke
|
|
- `queue/`, `dispatch/`, `state/`, `logs/` — runtime directories created on first run; gitignored
|
|
- `config.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.txt` is 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:
|
|
|
|
```sh
|
|
.venv/bin/python -m pytest
|
|
```
|
|
|
|
Run the live-API smoke (cost ~$0.0001 against Haiku 4.5; needs a billed `ANTHROPIC_API_KEY`):
|
|
|
|
```sh
|
|
.venv/bin/python -m pytest -m real_api
|
|
```
|
|
|
|
Lint / format:
|
|
|
|
```sh
|
|
.venv/bin/ruff check relay/ tests/
|
|
.venv/bin/ruff format relay/ tests/
|
|
```
|