This repository has been archived on 2026-05-02. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
risv3-relay/README.md
ac 540b4f5b01 feat: relay daemon skeleton — queue, dispatch, conversation, ntfy (#1)
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>
2026-05-02 15:24:47 +00:00

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/
```