Reviewer: Claude (Opus 4.8). Companion to the vision sanity-check
(2026-06-28-vision-sanity-check.md, idea-level) and the slice-1
code assessment (2026-06-28-slice-1-claude-adversarial-assessment.md).
Filed but not addressed. Seven GTM findings:
- G1: revenue vectors are a sequence (OSS → contract → hosted), not a menu
- G2: do-now, irreversible — secure CLA + commercial dual-license before
contributor #2 (directly affects ADR-0004; gates monetization doors)
- G3: the consulting trap
- G4: hosted = BYO-cloud, not multi-tenant SaaS
- G5+: the two near-free pivots — reposition as a voice-agent substrate
and beachhead on outbound-compliance
Cross-cutting with the vision-sanity-check's reframe (moat = boundary +
data loop, not determinism). Addressing these is strategy-doc + ADR
work that warrants user signoff; not auto-applied alongside the
code-track fixes.
Reviewer: Claude (Opus 4.8). Filing the strategy/vision-level critique
(for the slice-1 code-review see the companion
2026-06-28-slice-1-claude-adversarial-assessment.md already committed
in 8680f2b).
Filed but not addressed. Six strategic findings:
- S1: wedge leads with determinism (its most contestable leg)
- S2: differentiation valley between spearhead-end and capability ladder
(the load-bearing critique)
- S3: compliance paradox — regulated buyers won't move on bus-factor-1
- S4: turn-detection ownership at the tap — OpenAI Realtime has its own
server-side VAD by default; ARCHITECTURE.md says core-authoritative.
Belongs in the slice-3 (brain) spec as an explicit decision.
- S5: solo scope vs. category timing window
- S6: self-improvement loop is the actual moat, under-sold
Addressing these is strategy-doc work that warrants user signoff and
architecture pre-reading per AGENTS.md; not auto-applied alongside the
code-track fixes.
Apply findings from the Claude adversarial assessment
(docs/reviews/2026-06-28-slice-1-claude-adversarial-assessment.md)
after GLM-5.2 verification against the codebase + str0m 0.21 source.
Spec-only patches (code fixes follow as atomic commits):
- §1.2: add F5 (resource ceiling) to out-of-scope (deferred to step 5).
- §3.4: pin both str0m inputs as mandatory (Input::Receive + Input::Timeout)
and the deadline-honoring sleep semantics (Option B from brainstorming).
Without Input::Timeout, str0m's clock froze during inbound silence;
consent checks stopped; the browser tore the call down before the 60s
idle timeout (F1).
- §4.5: clarify the idle timeout keys on MediaData only. STUN/DTLS keepalives
do not reset it. Spec already said 'no RTP'; wording now makes the
intent unambiguous so the deviation cannot return (F4).
- §5.1: drop dead Channel.created_at field. The idle timeout keys on
RtcSession.last_rtp_rx, not creation time. The field was never read (F6).
- §10 (new): record the review patches lineage + cleared findings (F8, F9).
Findings F8 and F9 cleared by reading str0m 0.21 source
(packet/payload.rs:91 rebases MediaTime to the negotiated Opus clock;
RFC 7587 fixes SDP at opus/48000/2 regardless of internal rate). Cleared
with the same standard the review used for 'Browser ICE gathering'.
See AGENTS.md slice-1 boundaries — no scope additions; this is ratification
of fixes for bugs the spec already prohibited or that the dev-loop surfaced.
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).
Builds out the user-facing docs tree alongside the slice-1 build target.
Kept the implementer's planned Task 7 'Slice 1 dev loop' README section
untouched — these docs are the canonical destination for that pointer.
- docs/QUICKSTART.md: 5-min path to 'hear the echo' (libopus install,
cargo run, browser steps, troubleshooting, what's happening under the
hood).
- docs/DEVELOPMENT.md: dev loop — workspace layout, per-crate iteration,
running tests, the 20 ms loop / 'drop + observe' rule, slice-1
boundaries (what NOT to add yet).
- CONTRIBUTING.md (at repo root, conventional): trunk-based dev,
CI gates, commit message style, atomic commits, code style +
learner-facing documentation policy, terminology policy, PR workflow
+ review checklist, GPL-3.0-or-later license.
- README.md: add a Quickstart pointer at the top, a Documentation table
linking to every doc, and the slice-1 build-target status block.
uuid 1.x → getrandom 0.4.x requires edition = "2024", stabilized in Rust 1.85.
The Task 1 pin of 1.80 was written when uuid 1.x resolved to a getrandom that
still compiled on edition 2021; the resolution has shifted. The brief said
'confirm the latest stable at impl time' — bumping the pin is faithful to that.
Plan doc's Task 6 CI matrix also updated: [stable, "1.85"].
Cross-task correction in response to Task 2 implementer's DONE_WITH_CONCERNS
escalation; not a Task 2 code change.
Verified str0m 0.21 + opus 0.3.1 API surfaces via docs.rs subagents;
patched the plan with the real signatures and several design-level fixes.
API-surface patches (str0m 0.21, verified):
- Global Constraints: full verified API surface documented inline
(Rtc::new takes Instant; SdpOffer::from_sdp_string is the entry
point, NOT from_str_unchecked; add_local_candidate returns
Option<&Candidate>; Writer::write takes rtp_time not media_time;
MediaTime has no add(Duration) -> use mt + MediaTime::from(d);
payload_params returns impl Iterator; MediaData.data is Arc<[u8]>).
- Task 4 accept_offer: rewritten with from_sdp_string + correct error
mapping; RtcSessionError::SdpOffer changed to String (collapses parse
+ accept failures uniformly).
- Task 4 loop_driver: MediaTime::add -> + MediaTime::from(Duration);
media.data deref coercion documented.
Design-level patches:
- Task 4: RtcSession::new_for_test -> pub fn new() (single idiomatic
constructor, no test/prod split).
- Task 4: added accept_offer_transitions_channel_to_connecting test
(the transition was claimed but untested in the original plan).
- Task 5: dropped reqwest from workspace deps (unused — integration
test uses tower::ServiceExt::oneshot); added tower as workspace dep
+ binary crate dev-dependency.
- Task 5: removed duplicate DELETE /v1/sessions route (was incorrectly
chained via .delete() on the collection route AND on /v1/sessions/:id
— only the latter is correct).
- Task 5: clarified pub mod requirement (must be pub because the
integration test references modules via absolute paths).
- Global Constraints: added task/PR strategy (one commit per task,
merged in numeric order, granular history is load-bearing for the
learning-codebase goal).
Self-review section updated to reflect the patches.
Plan is now implementation-ready.
API-surface corrections (verified against str0m 0.21 docs):
- str0m::Live -> str0m::Rtc (no Live type exists)
- needs_input_timeout() -> Rtc::poll_output() returning Output::Timeout(Instant)
- RtcConfig::with_dtls_cert -> RtcConfig::set_dtls_cert (also: str0m auto-gens
a cert if none passed; slice 1 still passes explicitly for ownership clarity)
- Event::RtpPacket(pkt, rid) -> Event::RtpPacket(RtpPacket) { single struct,
RID is a field }
- str0m::ice::IceAgent -> no public 'ice' module; ICE types at crate root:
Candidate, CandidateKind, IceCreds, IceConnectionState
- str0m.write_rtp() on Rtclive -> Frame API path: Rtc::writer(mid) -> Writer::write
New design decision surfaced by API verification:
- Media API path: Frame API (default) vs RTP API (set_rtp_mode(true)).
Slice 1 uses Frame API — the proof target is the codec-to-PCM boundary,
not RTP packetization. RTP API is a candidate for step 5.
Design-level fixes from close reading:
- §3.3 AudioSource/AudioSink: clarify who drives the traits (the poll loop
drives both; sink on inbound MediaData, source on Output::Timeout deadline)
- §3.6: clarify cert/CrossRef — fresh in-memory cert each restart is safe
because browser fetches fresh SDP each session, no caching layer to go stale
- §4.1: add Content-Type: application/sdp to /offer (browser-side fetch needs
it)
- §4.5: resolve 5-min-vs-60s idle-timeout contradiction (60s, not 5 min)
- §4.5: add graceful-shutdown story (drop DashMap on SIGINT, browsers see
dead peer — acceptable for dev loop)
- §6.4: fix 'mocked str0m Rtc' — str0m's Rtc is a concrete struct, not a
trait; sans-IO means drive real Rtc with synthetic Inputs, not mock it.
Stronger than mocking: production path exercised exactly.
Resolves 5 load-bearing API errors + 3 design-level ambiguities. Spec is now
implementation-ready pending user review.
Folds the 2026-06-26 vision-revision pressure-test into the canonical
docs. The spec moves from Proposed to Ratified; the decisions it produced
land in the docs it said it would amend, with ADRs recording each
load-bearing reversal.
- README.md: reframe north star (AI-era contact-center engine, not
Asterisk successor); persona; revised pillars (add data-ownership,
demote WASM, promote spend-control); update 'what it is/isn't'.
- ARCHITECTURE.md: replace three-plane framing with fused per-call
vertical + composable horizontal platform; remove control<->media
hot-path gRPC hop; make the agent tap the central interface; add
DX spine + GUI-as-API-client + k8s declarative/operational model.
Also: 'too slow to police' -> 'too slow to enforce' (terminology).
- PORT_PLAN.md: recharacterize as capability checklist (not template);
graduate contact-center capabilities to first-class domain; Rust-native
trunk SIP rows; WASM demoted; thin-slice + capability ladder phasing.
- ADR-0001: marked Superseded by ADR-0003.
- ADR-0002 (new): north star + fused per-call core.
- ADR-0003 (new): Rust-native trunk SIP, no SBC shield.
- ADR-0004 (new): GPL-3.0-or-later license.
- ADR-0005 (new): Valkey as event bus + state store.
- ADR-0006 (new): WebRTC-first ingress; SIP endpoint deferred.
- vision-revision spec: status -> Ratified 2026-06-26.
- Clarify RtcSession/Channel ownership: ChannelId IS the session id;
RtcSession owns both str0m Live + codecs and the Channel. Fixes ambiguity.
- Drop the separate /ice endpoint: non-trickle ICE bundles candidates
into the SDP offer/answer, so the endpoint was redundant. Fixes
contradiction.
- Clarify idle timeout = 'no RTP for 60s' (not 5 min blanket), and that
no per-session tokio task is spawned (pre-paves the wrong pattern
for step 4's dedicated thread).
- 'reflex to police' -> 'reflex to enforce' (terminology: avoid
authoritarian verbs).
Workspace scaffold + spearhead step 1: browser talks WebRTC to the core,
core terminates DTLS-SRTP, decodes Opus to canonical 16-bit PCM @24 kHz mono,
echoes back. Proves the media core + the codec-to-PCM boundary (pre-paves
the tap). Stubs lock the ADR-0002 fused-vertical boundaries for steps
2/5/6.
Drafted in brainstorming session 2026-06-28.
Records the decisions from pressure-testing the four founding docs:
- North-star reframe: the engine for the AI-era contact center, not an
Asterisk-protocol successor (Asterisk's map = checklist, not template)
- Fused per-call vertical + composable horizontal platform (tightens the
three-plane model; removes the control<->media hot-path gRPC hop)
- Rust-native trunk SIP, IP-allowlisted; drops the Kamailio/rtpengine C
shield (reverses ADR-0001, to be formalized as ADR-0003)
- Pillars re-weighted: +data-ownership, spend-control promoted to
constitutive, WASM demoted out of the core story
- Developer-first DX spine (config-as-code, AEL lesson, engine+batteries)
- GUI as pure API client (FreePBX model) + k8s declarative/operational split
- Sequenced thin-slice first proof + capability ladder
For review before applying to README/ARCHITECTURE/PORT_PLAN and writing
ADR-0002 (identity) and ADR-0003 (SIP).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUtgQTo3wHGXP1TixHr4gd
Record the SIP edge decision and align the docs:
- docs/adr/0001-sip-strategy.md: layered strategy (own Rust parser, rent the
interop tail via a Kamailio + rtpengine SBC, grow native core behind the shield);
pjproject FFI explicitly rejected for breaking the memory-safety thesis at the
most exposed seam.
- PORT_PLAN §1 + open decisions: SIP row updated to the decided strategy.
- ARCHITECTURE: "biggest technical risk" now points at ADR-0001.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C2bfD7MkqEdfnMXxXBu456
Establish rutster — a memory-safe, API-first, security-first telephony
platform; spiritual successor to Asterisk for the WebRTC/microservices era.
- README: project framing, design pillars, open decisions
- docs/ARCHITECTURE.md: three-plane (control/media/app) model
- docs/PORT_PLAN.md: every Asterisk subsystem mapped to a disposition
(core / WASM-plugin / service / edge-FFI / dropped / replaced) with rationale
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C2bfD7MkqEdfnMXxXBu456