[T2] feat: relay daemon skeleton — queue, dispatch, conversation history, ntfy, summarization #1

Open
opened 2026-05-02 15:14:37 +00:00 by AC · 0 comments
Owner

Goal

First-PR scope: daemon skeleton, queue + dispatch loop, single-CC-session integration, basic logging, ntfy notifications, conversation history with summarization. Get one full loop working end-to-end against a real Anthropic API call.

Architecture

Single-process Python daemon. Main loop polls:

  1. queue/*.json — CC session output envelopes; oldest-first.
  2. state/jc_input.txt — JC override; treated as next chat-side turn (with optional @session-N: prefix for direct routing).

For each picked-up turn:

  1. Append content to state/conversation.json history.
  2. Call Anthropic API with full history + new content. Use prompt caching on the system prompt + early turns.
  3. Parse response:
    • First 200 characters contain [NEEDS-JC] → set status to needs_jc, send ntfy notification, do NOT dispatch. Daemon waits on jc_input.txt.
    • Otherwise → write response to dispatch/<session_id>/input.txt (only when prior file is consumed/deleted by the session). CC sessions poll their own input.txt.
  4. If history exceeds HISTORY_CHAR_CAP (default 400k chars), send a summarization turn, replace history with summary + last 10 turns verbatim.

Trust model

  • Daemon is pure transport between CC and chat-Claude. No merge decisions, no overriding CC's existing rules.
  • JC override (state/jc_input.txt) is treated as authoritative. With prefix @session-N: … it dispatches directly without an API call. Without prefix it goes through the API as if JC were responding from chat.

Files / structure

relay/
├── __init__.py
├── __main__.py          # CLI: relay run
├── config.py            # .env + config.yaml loader
├── daemon.py            # main loop
├── queue.py             # queue intake
├── dispatch.py          # dispatch writer with one-input-at-a-time per session
├── conversation.py      # history mgmt + summarization
├── anthropic_client.py  # SDK wrapper, prompt caching, cost calc
├── ntfy.py              # notifications
├── state.py             # atomic file I/O, instance lock
└── logs.py              # logging setup

tests/
├── test_config.py
├── test_state.py
├── test_queue.py
├── test_dispatch.py
├── test_conversation.py
├── test_anthropic_client.py  # mocked SDK
├── test_daemon_loop.py       # integrated, mocked SDK
└── test_real_api.py          # gated on ANTHROPIC_API_KEY env var; one round-trip

config.yaml                   # sessions registry (one default for first PR)

ntfy

Generates a random ~16-char topic on first run, writes back to .env. Daemon prints https://ntfy.sh/<topic> prominently on startup. Subscribe from phone/laptop. Notifies on:

  • needs_jc flagged on a chat-side response
  • daemon error (uncaught exception, API failure after retry)
  • queue stuck >10min (stale entry)

Out of scope (deferred to follow-up PRs)

  • Status web UI (Flask endpoint to render queue depth, recent activity, current conversation length, last needs_jc time)
  • Multi-session config (more than one CC session at a time)
  • systemd unit
  • Rich error recovery (exponential backoff, dead-letter queue)
  • Cost tracking dashboard (basic per-call cost in logs ships in this PR; aggregation later)

AI tier

Tier 2. Touches secrets (API key), sends data to a third-party API, runs as a long-lived process. Octopus security-auditor + database-architect (for state-file integrity) reviews before PR.

Done when

  • Daemon starts, prints ntfy URL, picks up a queue entry, calls the Anthropic API with conversation history, dispatches the response to the right session's input.txt.
  • [NEEDS-JC] token detection pauses dispatch and notifies via ntfy.
  • Conversation summarization triggers at HISTORY_CHAR_CAP and reduces the in-memory + on-disk size.
  • JC override via state/jc_input.txt resumes from needs_jc or routes to a session via @session-N: prefix.
  • All unit tests pass; the gated real-API smoke test passes when run with a valid key.
  • README + .env.example sufficient for someone to clone and run.
## Goal First-PR scope: daemon skeleton, queue + dispatch loop, single-CC-session integration, basic logging, ntfy notifications, conversation history with summarization. Get one full loop working end-to-end against a real Anthropic API call. ## Architecture Single-process Python daemon. Main loop polls: 1. `queue/*.json` — CC session output envelopes; oldest-first. 2. `state/jc_input.txt` — JC override; treated as next chat-side turn (with optional `@session-N:` prefix for direct routing). For each picked-up turn: 1. Append content to `state/conversation.json` history. 2. Call Anthropic API with full history + new content. Use prompt caching on the system prompt + early turns. 3. Parse response: - First 200 characters contain `[NEEDS-JC]` → set status to `needs_jc`, send ntfy notification, do NOT dispatch. Daemon waits on `jc_input.txt`. - Otherwise → write response to `dispatch/<session_id>/input.txt` (only when prior file is consumed/deleted by the session). CC sessions poll their own `input.txt`. 4. If history exceeds `HISTORY_CHAR_CAP` (default 400k chars), send a summarization turn, replace history with summary + last 10 turns verbatim. ## Trust model - Daemon is pure transport between CC and chat-Claude. No merge decisions, no overriding CC's existing rules. - JC override (`state/jc_input.txt`) is treated as authoritative. With prefix `@session-N: …` it dispatches directly without an API call. Without prefix it goes through the API as if JC were responding from chat. ## Files / structure ``` relay/ ├── __init__.py ├── __main__.py # CLI: relay run ├── config.py # .env + config.yaml loader ├── daemon.py # main loop ├── queue.py # queue intake ├── dispatch.py # dispatch writer with one-input-at-a-time per session ├── conversation.py # history mgmt + summarization ├── anthropic_client.py # SDK wrapper, prompt caching, cost calc ├── ntfy.py # notifications ├── state.py # atomic file I/O, instance lock └── logs.py # logging setup tests/ ├── test_config.py ├── test_state.py ├── test_queue.py ├── test_dispatch.py ├── test_conversation.py ├── test_anthropic_client.py # mocked SDK ├── test_daemon_loop.py # integrated, mocked SDK └── test_real_api.py # gated on ANTHROPIC_API_KEY env var; one round-trip config.yaml # sessions registry (one default for first PR) ``` ## ntfy Generates a random ~16-char topic on first run, writes back to `.env`. Daemon prints `https://ntfy.sh/<topic>` prominently on startup. Subscribe from phone/laptop. Notifies on: - `needs_jc` flagged on a chat-side response - daemon error (uncaught exception, API failure after retry) - queue stuck >10min (stale entry) ## Out of scope (deferred to follow-up PRs) - Status web UI (Flask endpoint to render queue depth, recent activity, current conversation length, last `needs_jc` time) - Multi-session config (more than one CC session at a time) - systemd unit - Rich error recovery (exponential backoff, dead-letter queue) - Cost tracking dashboard (basic per-call cost in logs ships in this PR; aggregation later) ## AI tier Tier 2. Touches secrets (API key), sends data to a third-party API, runs as a long-lived process. Octopus security-auditor + database-architect (for state-file integrity) reviews before PR. ## Done when - Daemon starts, prints ntfy URL, picks up a queue entry, calls the Anthropic API with conversation history, dispatches the response to the right session's `input.txt`. - `[NEEDS-JC]` token detection pauses dispatch and notifies via ntfy. - Conversation summarization triggers at `HISTORY_CHAR_CAP` and reduces the in-memory + on-disk size. - JC override via `state/jc_input.txt` resumes from `needs_jc` or routes to a session via `@session-N:` prefix. - All unit tests pass; the gated real-API smoke test passes when run with a valid key. - README + .env.example sufficient for someone to clone and run.
This repo is archived. You cannot comment on issues.
No Label
1 Participants
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: AC/risv3-relay#1