whiskers.git / crates / whiskersd / CLAUDE.md

For agents, on top of README.md, which they read first.

Notes for agents

  • This crate is an adapter; the policy is in whiskers-service. A rule about what a route does (a cap, an order of checks, what a refusal says, what may pass to the model) belongs in whiskers-service or whiskers-ports, where every backend gets it. What belongs here is keeping state and calling out. Adding a route means a test in whiskers-service/tests/service.rs, not code in main.rs.
  • The wire must not change. The tablet and the app depend on it. To check a change, run cargo test -p whiskersd (the end-to-end sync test), restart the service and run tools/preflight.sh; a refactor of this crate should also leave a recorded request script byte-identical against the old binary.
  • Held to the same laws as every adapter. tests/conformance.rs runs the whiskers-conformance suite on these adapters and adds what only threads and files show (eight pushes at one cursor take one; eight voice lines cannot pass a cap with room for one). A new adapter here gets its laws there.
  • Concurrent on purpose, and Jev is behind one thread. A thread per request, so a slow model call (the memory work after a turn) never holds up the next turn's safety check; that exact blocking was the "listens but doesn't reply" lag. The Jev client lives on its own thread (its futures are not Send) and request threads hand it jobs. The adapters block inside their async fns and never wait, so main.rs drives the service with run_ready on the request's own thread. Do not make the service single-threaded again, and do not give an adapter a future that really waits without giving main.rs an executor for it.
  • Never log message text. What she says is in the parents' log, written by the tablet. This service logs nothing but its own start-up, counts, ids and refusals. Never log a key or the voice id.
  • Spend goes through jev-http's ledger, shared with every other Jev program on the machine. Do not construct a client that bypasses it (judge.rs builds one with Ledger::open()).
  • The voice's daily cap is the service's, reserve-then-settle, separate from the tablet's VoiceBudget. The tablet's is a courtesy to the child; this one protects the plan. It lives in whiskers-ports (VoiceDay) and the service; speak.rs only speaks. Do not move a cap check into the adapter.
  • No default ElevenLabs voice. A voice id is not something to recall from memory; /speak refuses until ELEVENLABS_VOICE_ID is set. The free plan's API access is unconfirmed (open item in the brain's elevenlabs page).
  • TLS is on here (ureq default features), unlike the tablet crates, which are plain http. This runs on the operator's machine, where a C crypto backend is fine.
  • /v1/messages exists so the gateway is never put on the private network. Do not widen what it lets through (other models, tools, streaming, larger replies) to suit a feature: the gateway is the operator's subscription with no login of its own, and ThinkRequest::vet in whiskers-ports is its only guard. pkill -f whiskersd run from a shell whose own command line contains that string kills the shell; use the pid.