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.
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

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.
  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

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_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:

.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/
Description
Relay daemon between CC instances and Claude.ai chat via Anthropic API
Readme 59 KiB