Patches the agent-tap design with seven findings from PM-side review
before the implementation plan gets written. Resolves invariant-breakers
and deferred-decision debt that would compound across steps 3-6.
- P1 (high): §8.5 #6 'byte-identical loop_driver.rs' was the wrong bar
for the seam test. Reworded: the trait-method *call sites* are
unchanged; impl bodies are free to differ. That's what 'the seam
held' actually means — the contract is the test.
- P2 (high): tap: Option<TapHandle> and ChannelState were parallel
state machines without an invariant tying them. Added an explicit
state-pair table: tap is Some iff state == Connected. Lookups tolerate
transient inconsistency; the source-of-truth is the pair, not either
field alone.
- P3 (medium): v1 wire format was host-endian (silent big-endian
hazard on paper). Nailed LE explicitly via i16::to_le_bytes /
i16::from_le_bytes. v2 can negotiate endianness if a big-endian
brain ever materializes; v1 contract is LE-only. Added a §3.4
byte-order invariant alongside the sample-count invariant.
- P4 (medium): wss:// URLs were accepted by schema but returned
501 at connect time (UX: 2 s into a call). Now rejected at
POST /v1/sessions with 400 + clear message — fail fast at
session-create. Updated §4.4, §7.1, §7.3, §9, and the decisions
table for consistency; removed all stale '501 at connect' claims.
- P5 (low): TapEngine spawn owner was unspecified. Added explicit
ownership: session_map::drive_all_sessions (the binary's poll
task) observes the Connected transition from run_poll_once and
spawns the engine. Keeps loop_driver.rs behaviorally unchanged
(the §8.5 #6 seam test still holds).
- P6 (low): 'two new deps' in §1.1 undercounted. Updated list:
tokio-tungstenite, futures-util, url, serde_json (if not already
pulled).
- P7 (low): ADR-0007 was 'post-implementation follow-up, not a
slice-2 deliverable.' Tightened: strongly recommended to land
alongside or immediately after slice-2 implementation — once
step 3 ships against an unpinned protocol, the wire shape will
silently drift. The spec lists the decisions ADR-0007 should
capture (LE byte order added to the list).
Slice 2 hardens the presumptive agent-tap shape from ARCHITECTURE.md §"Agent tap"
(PORT_PLAN §10 open decision) against a working implementation:
- Replace slice-1's in-process EchoAudioPipe with a real out-of-process brain
reached over WSS; core-as-client, no inbound tap port (ADR-0006 posture).
- Versioned JSON event protocol (v1) over WS text frames with base64 PCM.
Event-named + JSON-over-text-WS so the step-3 OpenAI-Realtime adapter is a
translation shim, not a gRPC-bridge project.
- Decoupled TapEngine (cold-path tokio task) owns the WS connection; TapAudioPipe
is a thin sync wrapper the AudioSource/AudioSink seam holds. RtcSession's media
loop is byte-identical to slice-1 — the seam-test payoff.
- Core-authoritative playout buffer (5-frame/100ms ring; drop-oldest on overflow,
silence on underflow) — the place where "brain proposes, core disposes" lives.
- Bounded-backoff reconnect (250ms→5s cap, infinite retries); Channel stays
Connected; stateless-brain reconnect contract.
- Both a Python reference echo brain (examples/echo_brain/, not in CI) and a
Rust echo brain crate (crates/rutster-tap-echo, the in-process test server
+ standalone dev binary).
- ws:// loopback-only enforced; wss:// URL accepted at schema, 501 at connect
(cert/mTLS impl deferred to step 6).
Out-of-scope table (§1.2) locks the spearhead sequencing: real brain (3),
barge-in/VAD-driven playout kill (4), PSTN trunk (5), spend cap (6), wss:// TLS
posture (6), binary PCM mode (future-rung), byte-endian negotiation (tracked
open decision §9).