Files
skills/docs/requirements/004-radio-set.md
T
Kevin Veen-BirkenbachandClaude Opus 5.5 8711c23a39 docs(requirements): add 004 radio set
The Agent Orchestrator radio as a minimalist marine VHF set: a server CPU gate for speech, a WebAudio noise burst, modes Auto, Quiet and Silence, procedure words that always sound, every radio setting in the panel and a log with Sent, Received and All plus filters. Operator decisions 2026-11-12.

🎯 Fix: 95 %
💥 Break: 1 %
💸 Cost: 100 %
⏱️ Time: 100 %
🛡️ Security: 100 %
🏆 Score: 94
🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 %

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 16:19:16 +00:00

7.3 KiB

004 - Radio Set

User Story

As the operator of many parallel agents, I want the Agent Orchestrator radio to work like a minimalist marine VHF set, with every setting at hand, a filterable log and speech only when the host has CPU to spare, so that I hear that someone transmits without the speech engine loading a busy host.

Decisions (operator-confirmed 2026-11-12; do not re-litigate)

  1. CPU gate on the server: /api/tts renders an uncached line only while the latest host CPU sample of the W4 resource feed (samples.jsonl, at most 120 s old) is below the threshold; a cached line is always served; otherwise it answers 503 with the reason busy, or unknown without a fresh sample. Rejected: a gate only in the browser (another tab or an old page still renders), browser speechSynthesis (same host CPU, other voices), rendering held-back speech later (stale).
  2. Noise burst: when no speech plays, the page plays a squelch-tail noise burst made by WebAudio in the browser (band-passed white noise, about 0.6 s, at the tone's volume and squelch); no server request. Rejected: a recorded sound file.
  3. Modes: Auto (speech below the threshold, otherwise the burst), Quiet (the burst only, never speech), Silence (no sound; bubbles and log only). Default Auto.
  4. Procedure words: for MAYDAY, PAN-PAN and SÉCURITÉ the procedure word, spoken three times in the sender's voice, always sounds, in every mode and under load; under load or in Quiet the burst replaces the rest of the message. The word line is the same text every time, so after its first render per voice it is a cache hit.
  5. Settings in the panel: mode, volume per tone, squelch, read-aloud filters (classes, agents) and the internet radio station are radio-set buttons and controls in the radio panel; the radio section leaves the settings dialog. They live per browser, without a passkey. Rejected: signed hub settings (one passkey per button press).
  6. Threshold: adjustable in the panel, 30 to 90 % in steps of 10, default 50 %; a signed hub setting speech.threshold, since the server enforces it. Rejected: a fixed 50 %.
  7. Log: tabs Sent (from the operator), Received (to the operator or to all) and All (every radio row the hub logs); filter chips class, sender, recipient and agent plus text search, combined with AND, newest first. Rejected: Received as all agent traffic.
  8. Look: one faceplate inspired by a marine VHF set, functional and minimalist: a display line (class, mode, CPU, TX/RX lamp), a row of mode buttons, sliders as knobs, a PTT send button; palette tokens, light and dark, no textures. Rejected: a full VHF set with dual watch, scan and DSC (high effort, no gain).
  9. Order: builds on W4's resource feed; implementation starts once W4 is on master.

Approach

flowchart TD
    subgraph P["🧭 0 · Preparation"]
        P1["📋 Write requirement"] --> P2{"✅ Operator approves?"}
        P2 -- "✏️ corrections" --> P1
        P2 -- "🤖 yes" --> P3{"📊 W4 on master?"}
        P3 -- "⏳ no: wait for the unify" --> P3
    end
    P3 -- "🟢 yes" --> A1
    subgraph A["🛡️ A · Server gate"]
        A1["🧩 CPU gate in /api/tts, gate state in /api/state, signed threshold"] --> A2{"🧪 Unit and HTTP tests green?"}
        A2 -- "🔴 no: fix" --> A1
    end
    A2 -- "🟢 yes" --> B1
    subgraph B["🔊 B · Sound"]
        B1["🎚️ Noise burst, modes, procedure words"] --> B2{"🧪 Tests green?"}
        B2 -- "🔴 no: fix" --> B1
        B2 -- "🟢 yes" --> BF{"👂 Operator listens: fits?"}
        BF -- "✏️ changes" --> B1
    end
    BF -- "👍 fits" --> C1
    subgraph C["📻 C · Panel"]
        C1["🎛️ Faceplate, settings into the panel, threshold control"] --> C2{"🧪 e2e at 1920, 1366, 390 green?"}
        C2 -- "🔴 no: fix" --> C1
        C2 -- "🟢 yes" --> CF{"🖼️ Screenshots: operator fits?"}
        CF -- "✏️ changes" --> C1
    end
    CF -- "👍 fits" --> D1
    subgraph D["📜 D · Log"]
        D1["🗂️ Sent, Received, All, chips, search"] --> D2{"🧪 Tests green?"}
        D2 -- "🔴 no: fix" --> D1
        D2 -- "🟢 yes" --> DF{"🖼️ Screenshots: operator fits?"}
        DF -- "✏️ changes" --> D1
    end
    DF -- "👍 fits" --> E1["🏁 Review, security scan, unify gate, commit and master on the operator's word"]

Acceptance Criteria

Server gate

  • POST /api/tts with an uncached line answers 503 {"reason": "busy"} while the latest samples.jsonl row has cpu >= the threshold, and the fake Piper records no call.
  • Without a sample younger than 120 s it answers 503 {"reason": "unknown"}, and the fake Piper records no call.
  • A cached line answers 200 whatever the CPU.
  • Below the threshold an uncached line renders as before.
  • GET /api/state carries speech.gate with threshold, cpu and open.
  • speech.threshold accepts only 30 to 90 in steps of 10, default 50, and changes only with a valid passkey signature.

Sound

  • Auto below the threshold: the page plays the spoken line; on a 503 it plays the burst once and does not retry that row.
  • Quiet: the page sends no /api/tts request except for procedure-word lines, and every row plays the burst.
  • Silence: no audio node starts except for a procedure-word line.
  • The burst is WebAudio noise of 0.4 to 0.8 s at the tone's volume times the squelch, and it sends no network request.
  • A MAYDAY, PAN-PAN or SÉCURITÉ row requests its procedure-word line first, in every mode; the second such line for the same voice is a cache hit with no Piper call.

Panel

  • The display line shows class, mode, CPU % and a TX/RX lamp that is lit while sound plays.
  • Auto, Quiet and Silence form one button group (aria-pressed); a press changes the mode without a passkey and survives a reload.
  • Volume per tone, squelch, the read-aloud filters for classes and agents, and the internet station sit in the panel; the settings dialog has no radio section.
  • The threshold control asks for the passkey once per change and shows the server's value.
  • At 1920, 1366 and 390 px the panel has no horizontal scroll, in the light and the dark palette.

Log

  • Tabs Sent, Received and All show their counts; Received holds exactly the rows addressed to the operator or to all.
  • The chips class, sender, recipient and agent and the text search combine with AND; one control clears them all.
  • Rows run newest first with time, direction, class badge, sender → recipient and text; a click opens the full message.
  • Tab, chips and search survive a reload in the same browser.

Tests

  • Unit: gate busy, unknown, cache hit and open; threshold validation; procedure-word lines from phrasing.py; every case with a negative control.
  • Integration: hub HTTP tests with a fake Piper and a fake samples.jsonl: 503 and no Piper call under load, 200 for a cache hit, speech.gate in /api/state.
  • E2E: Playwright in Chromium and Firefox at 1920, 1366 and 390 px: mode buttons, burst through a spy on the AudioContext, procedure word under load, the settings dialog without a radio section, the log tabs and filters.