diff --git a/docs/reviews/2026-06-28-gtm-path.md b/docs/reviews/2026-06-28-gtm-path.md new file mode 100644 index 0000000..edd779c --- /dev/null +++ b/docs/reviews/2026-06-28-gtm-path.md @@ -0,0 +1,182 @@ +# Claude GTM, Positioning & Pivots — Rutster + +- **Reviewer:** Claude (Opus 4.8), strategy pass. Companion to `2026-06-28-vision-sanity-check.md` (idea) and `2026-06-28-slice-1-claude-adversarial-assessment.md` (code). +- **Targets:** the reframed vision (see the sanity-check doc), `docs/adr/0004-license.md`, ADR-0002, `docs/ARCHITECTURE.md`, as of `main@22d3f03`. +- **Question asked:** path to market across three vectors (OSS / hosted / deployed-on-contract), and which **pivots make it more attractive.** + +## How to use this document (for the GLM-5.2 reviewers) + +Same contract as the other two reviews: **claims to refute, not conclusions to accept.** Each finding carries the strategy triad: + +- **Steelman:** the strongest version of the *opposing* position (sometimes the status quo, sometimes the pivot). +- **What would change the verdict:** the evidence/decision that flips it. +- **Decision implied:** the concrete, actionable change if it holds. + +This builds on the reframe from the sanity-check doc (moat = auditable boundary + data-ownership + the learning loop; *not* no-GC determinism). If you reject that reframe, several findings here move with it — flag that explicitly rather than arguing each downstream point in isolation. + +--- + +## Thesis + +The three revenue vectors are **not parallel options — they're a sequence** (OSS → contract → hosted), each de-risking the next. The **load-bearing, irreversible** decision is not which vector but **copyright control (a CLA), which must happen before contributor #2** or three monetization doors silently close. And the project becomes materially **more attractive** with two near-free pivots — reposition as a **voice-agent substrate** (not a contact-center engine) and **beachhead on outbound-compliance** (where the spend-gate is a must-have, not a nice-to-have) — that ride the live 2026 category, shorten the differentiation valley, and make the most-defensible feature the *lead* feature. + +--- + +## Where it fits — the 2026 board + +| Player | Winning on | Losing on (rutster's opening) | +|---|---|---| +| Cloud voice-agent platforms (Vapi/Retell/Bland) | speed-to-deploy | data-residency, cost-at-scale, customization, lock-in | +| LiveKit (+ Agents) | horizontal real-time media, mindshare | no domain, no compliance-appliance story, cloud-data by default | +| CCaaS bolting on AI (Five9/Genesys/Connect) | full product, enterprise reach | proprietary, un-self-hostable, AI bolted-on | +| DIY glue (LiveKit + trunk + custom VAD + guards) | full control | nobody wants to maintain the glue | + +**The open slot:** the place the DIY-glue crowd and the Vapi-ceiling crowd *graduate to* — **the self-hostable, compliance-grade, data-owned substrate for voice agents.** Every pivot below moves rutster toward that slot; the anti-pivots move it away. + +--- + +## Part A — The path to market + +### G1 — The three vectors are a sequence, not a menu + +**Confidence: high.** OSS makes *trust*, not money. Contract makes *early money* and hardens the product. Hosted makes *scalable money* but only after the first two de-risk it. For a solo builder the question is which vector funds which stage; the answer is **OSS → contract → hosted.** + +| Vector | Stage | Job | Success metric | Failure mode | +|---|---|---|---|---| +| **OSS self-host** | 0 (mo 0–18) | cross the "nobody trusts bus-factor-1 telephony" chasm; readable, data-owned code *is* the rebuttal | first real (non-critical) call routed by a builder who isn't you | optimizing for stars instead of first-real-call | +| **Contract / services** | 1 | funded R&D + reference deployments + hardening; reach regulated buyers via *your accountability* | engagements that productize into repeatable features | the consulting trap (see G3) | +| **Hosted (BYO-cloud)** | 2 | scalable revenue once proven + a team exists | recurring control-plane revenue without holding customer data | building multi-tenant SaaS (see G4) | + +- **Steelman:** a well-funded team could run all three at once. — True, but this is a solo build; parallelism is the thing you *don't* have. +- **What would change the verdict:** a co-founder/team + capital arriving early enough to operate hosted before the engine is proven. +- **Decision implied:** sequence deliberately; don't let hosted ambitions pull ops effort forward before Stage 1 reference customers exist. + +### G2 — Do-now, irreversible: secure copyright control (CLA) before contributor #2 + +**Confidence: high. This is the highest-leverage, most time-sensitive item in any of the three review docs.** + +ADR-0004 reasons GPL-3-vs-AGPL on "downstream-embedding friction" but skips two business-gating facts: + +1. **GPL-3 blocks the proprietary-integrator economy you implicitly want.** "Lower friction than AGPL" ≠ low friction — a commercial integrator can't ship a *proprietary* product on GPL-3 rutster. Asterisk's integrator ecosystem existed because **Digium held copyright and dual-licensed.** GPL-3 alone forecloses that; GPL-3 **+ commercial dual-license** restores it. +2. **The ADR's "switch to AGPL later via or-later" escape hatch is overstated.** `or-later` adopts future *GPL* versions; **AGPL-3 is a separate license, not a later GPL.** Relicensing the project to AGPL — or dual-licensing commercially — requires **every copyright holder's consent**, i.e. a **CLA / DCO+assignment collected from day one.** + +Today you are the sole author → you have total copyright control **for free.** The first un-CLA'd contributor removes the options to ever go AGPL (SaaS protection), dual-license (integrator revenue), or sell paid-embedding. None of these must be *used* — but all close silently without a CLA. + +- **Steelman:** a CLA dampens community contribution; pure GPL-3 with no CLA is the maximal software-freedom signal and best for trust. — Real cost, but a CLA/DCO is one-time and reversible *in spirit* (you can always choose not to exercise the options); losing copyright control is **not** reversible. +- **What would change the verdict:** a firm decision to *never* dual-license, *never* go AGPL, and monetize purely via services/hosting-convenience — then skip the CLA and lean into the freedom signal. +- **Decision implied:** adopt a contributor-licensing posture (DCO at minimum; CLA with copyright control if you want the commercial options open) **before merging the first external PR.** Decide it consciously; don't let it be decided by default. + +### G3 — Contract/services is the right early engine — with a tripwire + +**Confidence: high.** It's the Asterisk-integrator motion you've personally run, and it's the founder's unfair advantage: high-margin, zero-scale-required, monetizes scarce expertise, and — crucially — it's how you reach **regulated buyers** (the sanity-check's S3 paradox): they won't self-serve a bus-factor-1 binary, but they'll buy *your accountability + auditable software.* Each engagement is funded R&D + a reference deployment + a real customer pulling you toward the differentiated rungs (surviving the S2 valley with a paycheck instead of building breadth in the dark). + +**The tripwire:** services don't scale and will consume the founder. Discipline — cap the count, treat them as funded R&D + reference customers, and productize anything that recurs across three engagements. **Services funds the product; it is not the product.** + +- **Steelman:** services *is* a fine lifestyle business and "trap" is just a VC framing. — Valid if a lifestyle business is the goal; if the capability-ladder vision is the goal, undisciplined services starves it. +- **What would change the verdict:** explicit choice that a sustainable consultancy *is* the destination. +- **Decision implied:** write the cap and the "productize-on-third-repeat" rule down before the first engagement; review it each quarter. + +### G4 — Hosted must be BYO-cloud / managed control plane — never multi-tenant SaaS + +**Confidence: high (follows directly from the reframe).** The reframed moat is data-ownership, so a hosted offering that *holds customer call/training data* torches the moat and lands you in a race-to-the-bottom against Vapi where you also lose on convenience. The wedge-consistent hosted product: **the engine runs in the customer's VPC; you operate the control plane** (provisioning, updates, monitoring, trunk-bundling-as-one-bill). Removes ops burden; data never leaves their boundary. "Managed, but your data stays yours" is a SKU Vapi/Retell structurally can't offer — it *is* the reframe made into a product. + +- **Steelman:** multi-tenant SaaS is simpler to operate and has better margins. — True operationally, but it's the one model that contradicts your only durable moat. +- **What would change the verdict:** a customer segment that demonstrably values convenience over data-residency *and* pays enough to matter — then a multi-tenant tier could exist *alongside* BYO-cloud, never instead of it. +- **Decision implied:** if/when hosted is built, build BYO-cloud first; treat any multi-tenant tier as a strictly secondary, clearly-labeled convenience option. + +### G5 — Open-core seam: free wedge, paid convenience-and-scale + +**Confidence: medium-high.** Keep the moat free; monetize the layer around it. + +- **OSS (GPL-3):** the engine, the reference distro (`compose up`), the reference GUI, **and the raw ML-loop primitive** (capturing auto-labeled takeover data). Paywalling "you own your data" would be self-contradicting. +- **Commercial / hosted:** the containment-analytics *product*, eval-set + fine-tune *tooling* at scale, multi-region orchestration, SSO/RBAC + compliance attestations (SOC2/HIPAA), support SLAs, the BYO-cloud control plane, trunk-bundling-as-one-bill. + +- **Steelman:** putting the ML-loop primitive in OSS gives away the crown jewel. — No: the *data* is the customer's by thesis; you sell the *tooling* that makes it useful at scale. Free primitive drives adoption; paid tooling captures value. +- **Decision implied:** draw the seam explicitly in a CONTRIBUTING/COMMERCIAL doc so the boundary is legible to the community and doesn't drift. + +### G6 — Funding fork: bootstrap-via-services → optional raise + +**Confidence: medium.** Bootstrapped keeps the ideology + control but is slow; VC forces the hosted/scalable path early and tends to push toward the data-holding SaaS that fights your wedge. For a solo founder with this ideology: **bootstrap on services → raise only once the BYO-cloud hosted wedge is proven** (raise to scale a known thing, not to discover one). + +- **What would change the verdict:** a credible threat that a funded competitor closes the white space before you can reach the differentiated rungs — that argues for raising earlier to move faster, accepting ideological pressure. +- **Decision implied:** set a tripwire (e.g., "if a funded OSS competitor ships self-host + BYO-cloud + domain, revisit raising"). + +--- + +## Part B — Positioning & pivots (what makes it more attractive) + +Each pivot is labeled by the *kind* of attractiveness it buys: market-size / speed-to-value / fundability / defensibility / execution-survivability / adoption. + +### P1 — Identity: "contact-center engine" → "self-hostable voice-agent substrate" +**Buys: market-size + speed-to-value + shorter valley. Cost to pivot: near-zero (positioning + build-order).** +Contact-center aims at a slower category and forces the S2 valley before anything's differentiated. The *substrate* framing rides the live, funded 2026 category (voice agents) and makes the differentiated part — safe tap + barge-in + in-boundary spend-gate, all in your boundary — valuable to *any* voice-agent builder on day one. Contact-center (ACD/queues/CDR) becomes the **flagship vertical distro you grow into**, not the entry tax. + +- **Steelman:** "contact center" is a *narrower, ownable* domain; "voice-agent substrate" collides head-on with LiveKit's horizontal positioning. — Real risk; mitigated by leading with self-host + compliance + data-ownership, which LiveKit doesn't own. You're not out-LiveKit-ing LiveKit on media; you're the *data-owned* substrate. +- **What would change the verdict:** evidence the voice-agent category is consolidating around incumbents fast enough that a substrate play is already too late — then the narrower contact-center domain is the safer ownable niche. +- **Decision implied:** change the headline and the order-of-generality now; keep contact-center as the first vertical distro. + +### P2 — Beachhead: lead with **outbound + compliance**, not inbound contact center +**Buys: execution-survivability + a must-have moat. (Pressure-tested in R1 — read it before adopting.)** +Outbound is a simpler call model (you originate — no inbound-trunk/ACD/queue complexity) → faster to value, survivable solo. And it turns your **most under-sold structural win** (in-boundary spend/pacing/abuse gate) into the **lead feature**: outbound lives or dies on TCPA, pacing caps, DNC, per-campaign spend ceilings — which a 3-vendor stack can't structurally enforce and rutster can, because it holds the wire. ROI is unambiguous. + +- **Steelman:** inbound is where the contact-center domain (your eventual moat) lives; leading outbound delays building it. — But inbound is also the slower, more complex path; outbound gets a paying, differentiated wedge in market faster, then funds inbound. +- **What would change the verdict:** R1's regulatory blast-radius risk proving large enough to outweigh the speed advantage. +- **Decision implied:** beachhead on **consented enterprise outbound** (see R1), not cold lead-gen. + +### P3 — North star: "nothing leaves your boundary — *including the model*" +**Buys: maximal defensibility + the buyer with budget.** +Today the brain is external by necessity, capping the data story. Make the *endgame* a fully in-boundary stack — trunk + media + **open-weights speech-to-speech** + data, all in the customer's VPC — riding open-model maturation. You don't bet the company on open-model quality: the tap stays brain-agnostic (cloud brain *or* in-boundary model). Stating "the only fully air-gapped voice AI" as the direction makes the moat **total** and aims it at compliance buyers — a slot the cloud players structurally can't follow you into (their model *is* you sending them data). + +- **Steelman:** open speech-to-speech may stay quality-behind cloud for years; promising air-gap oversells. — Which is why it's the *direction/option*, not a present claim; the brain-agnostic tap means you ride it as it arrives, no downside if it's slow. +- **Decision implied:** keep the tap strictly brain-agnostic; name in-boundary-brain as the endgame in positioning, not the roadmap-now. + +### P4 — Form: "Home Assistant for voice AI" +**Buys: adoption flywheel + the answer to bus-factor-1.** +Lean into the HA model as operating identity, not metaphor: self-hosted, batteries-included-but-hackable, community-loved, with an add-on ecosystem (community trunk adapters, brain adapters, call-flow packs). The community *is* the rebuttal to "one maintainer" — buyers trust a thriving project, not a person. + +- **Steelman:** HA-style community takes years and a delightful UX you can't afford solo early. — True; this is the *later* identity pivot, seeded early by API-completeness + add-on seams, not built day one. +- **Decision implied:** design the add-on/adapter seams early (cheap) so the ecosystem *can* form; invest in delight once Stage-1 revenue exists. + +### How they compose + +P1+P2+P3+P4 stack into one sharper product: **"the Home Assistant of voice agents — a self-hostable, compliance-grade substrate you run in your own boundary; beachhead on outbound where the spend-gate is a must-have; endgame where even the model runs in-VPC."** Lower-risk and more attractive than "AI-era contact-center engine," abandoning nothing — contact center is the vertical you grow into. Fully continuous with the reframe (moat = boundary + data + loop) and the path (G1–G6). + +### Anti-pivots (attractive-looking traps → guardrails) + +| Anti-pivot | Why it's a trap | +|---|---| +| → multi-tenant managed SaaS to undercut Vapi | holds customer data (kills the moat); you lose on convenience anyway. Hosted stays BYO-cloud (G4). | +| → closed / source-available for fundability | kills the trust that's your only chasm-crosser. Use CLA-enabled commercial dual-license (G2) to get revenue *without* closing the core. | +| → broaden to general telephony / PBX | back into the Asterisk death-march and Teams-Phone's jaws. Stay in voice *agents*. | + +--- + +## R1 — Pressure-test: the outbound-compliance beachhead is double-edged + +**Confidence: high that it cuts both ways; this gates P2.** + +**The edge that helps:** the harder the outbound regulatory surface (TCPA, STIR/SHAKEN attestation, DNC scrubbing, state robocall laws, carrier spam-labeling), the *stronger* your in-boundary spend/pacing/consent/audit gate is as a must-have. Regulatory pain creates willingness to pay for compliance tooling. Your constitutive feature becomes the purchase reason. + +**The edge that cuts:** +1. **Reputational adjacency to robocalling.** "The engine that powers outbound AI calls" sits next to exactly what regulators and carriers are cracking down on. Being associated with the spam end is an existential brand risk for a compliance-led pitch. +2. **OSS can't choose its users.** GPL means anyone can self-host rutster to run cold/illegal robocalling; you cannot license-control that. Your abuse-governance is *reputational/positioning*, not technical. +3. **Selling "compliance-grade" raises your own liability bar.** You're implicitly warranting behavior; getting consent/DNC/attestation tooling *wrong* in a product sold as compliant is legal exposure a solo shop can't absorb. +4. **The compliance surface is itself a big build** (consent management, DNC sync, STIR/SHAKEN signing, audit trails) — credible tooling is a real lift, not a checkbox. + +**The resolution (this is the actionable part):** +- Beachhead on **consented enterprise outbound** — a company calling *its own* customers (appointment reminders, account servicing, proactive support) where consent is established — **not** cold outreach / lead-gen / collections-to-strangers (the spammy, litigious end). The wedge shines in legitimate outbound; the abuse risk lives in cold-calling. +- **Lead outbound through the contract/hosted vectors, not pure OSS** — because those vectors let you *choose your customers* (gate abuse at the relationship), while pure OSS self-host cannot. The OSS engine stays general-purpose; the *paid* outbound offering is where you enforce a "legitimate, consented use" posture. +- Add an **AUP / acceptable-use posture** and abuse-governance stance to positioning early, so "we are the substrate for legitimate operators, not a robocall cannon" is explicit before someone makes you the headline. + +- **What would change the verdict:** if consented-enterprise-outbound is too small a beachhead to fund Stage 1, you may have to take riskier outbound work — at which point the reputational/liability math may flip P2 toward an inbound-first beachhead instead. +- **Decision implied:** adopt P2 **only** scoped to consented enterprise outbound, sold through contract/hosted, with an AUP — or fall back to an inbound-support beachhead if that scoping can't fund the stage. + +--- + +## Verification checklist for the GLM-5.2 reviewers + +1. **G2 is the time-sensitive one.** Decide the CLA/copyright posture before merging external contributions — every other monetization option depends on it and it's the only truly irreversible item. +2. **P1+P2 are the high-leverage, near-free pivots.** Pressure them against R1 (outbound risk) and the LiveKit-collision risk (P1 steelman) before committing positioning. +3. **G4 follows from the reframe** — if you reject "moat = data-ownership," G4 (BYO-cloud-only) reopens; flag that dependency rather than arguing G4 alone. +4. **R1 gates P2** — do not adopt the outbound beachhead without the consented-enterprise scoping + sell-through-contract + AUP guardrails. +5. Strike anything you can dissolve, reasoning recorded — same standard the vision held itself to when it reversed ADR-0001. diff --git a/docs/reviews/2026-06-28-slice-1-claude-adversarial-assessment.md b/docs/reviews/2026-06-28-slice-1-claude-adversarial-assessment.md new file mode 100644 index 0000000..50b552d --- /dev/null +++ b/docs/reviews/2026-06-28-slice-1-claude-adversarial-assessment.md @@ -0,0 +1,236 @@ +# Claude Adversarial Assessment — Slice 1 (WebRTC media loopback) + +- **Reviewer:** Claude (Opus 4.8), static read of the tree. No code executed; no browser e2e run. +- **Target:** `crates/rutster*` as of `main@22d3f03` (slice-1 implemented; slice-2 is spec/plan only and **out of scope** for this code review). +- **Spec under test:** `docs/superpowers/specs/2026-06-28-slice-1-webrtc-loopback-design.md`. +- **Scope:** ~1,550 LoC of Rust across the workspace. Media core (`rutster-media`), call model (`rutster-call-model`), signaling binary (`rutster`). Stub crates (`-tap`, `-spend`, `-signaling-sip`) are empty and not assessed. + +## How to use this document (for the GLM-5.2 reviewers) + +This is **adversarial**, not authoritative. Each finding is a *claim Claude is asking you to refute*. For every one: + +1. Read the cited `file:line` yourself — do not trust the quote. +2. Run the **Falsification test**. If it passes (i.e. the bug does *not* reproduce), the finding is **rejected** — say so and show why. +3. Where Claude marks **Confidence: verify**, that means Claude could not confirm it by static reading alone (usually a str0m 0.21 API semantic). Treat those as *open questions*, not bugs, until you check the dependency source. + +Claude has already cleared one suspected issue (the browser client's ICE-gathering wait — see "Cleared" at the bottom). Hold this review to the same standard: try to clear findings, don't just nod along. + +--- + +## TL;DR — findings + +| # | Severity | Confidence | One-line | Location | +|---|----------|-----------|----------|----------| +| F1 | **High** | High (contract) / Med (user-visible) | str0m is never fed `Input::Timeout`; its clock only advances on inbound packets, so all timer-driven behavior (DTLS retransmit, ICE consent, RTCP) stalls during inbound silence | `loop_driver.rs:56` | +| F2 | **Medium-High** | High | Client-triggerable panic: a second offer to the same session hits `assert!` | `rtc_session.rs:183`, `routes.rs:60` | +| F3 | Medium | High | `next_timeout` is computed with great care, then discarded; `session.next_timeout` is written but never read — dead plumbing that hides F1 | `loop_driver.rs:227`, `session_map.rs:114` | +| F4 | Medium | High | Idle timeout is spec'd as "no **RTP**" but resets on **any** datagram (STUN/DTLS keepalives included), so a silent-but-connected peer is never reaped within 60 s | `loop_driver.rs:87` | +| F5 | Low-Med | High | No cap / rate-limit on `POST /v1/sessions`; each session binds a UDP socket — unbounded FD/port growth (reaped only after 60 s idle) | `routes.rs:28`, `session_map.rs:53` | +| F6 | Low | High | `Channel.created_at` is dead and mis-documented ("for the idle timeout"); spec also says "5-min" in one place, "60 s" in another | `rutster-call-model/src/lib.rs:140`, spec §4.5 vs §5.6 | +| F7 | Low | High | Unreachable `needs_redrain` branch in the Step-2 drain loop (always false there) | `loop_driver.rs:113` | +| F8 | Low | **verify** | Outbound RTP timestamp via `MediaTime::from(Duration)` — correctness depends on str0m rebasing to the 48 kHz payload clock in `Writer::write` | `loop_driver.rs:187` | +| F9 | Low | **verify** | Codec is 24 kHz mono while SDP advertises `opus/48000/2`; decode buffer is exactly 480 i16, so any inbound frame > 20 ms overflows → dropped | `opus_codec.rs:23`, `:45` | + +**Plus:** a material **test-coverage gap** — there is *no* automated test that drives `loop_driver::drive()`, the heart of the slice. F1, F2, and F4 would all have been caught by one. + +--- + +## F1 — str0m is never given `Input::Timeout`; its clock is frozen during inbound silence + +**Severity: High. Confidence: High** that the contract is violated; **Medium** on how visible it is in the happy-path demo. + +**Evidence.** `grep -rn 'Input::Timeout' crates/` returns **only comments** — no call site. The sole `handle_input` call is `Input::Receive`: + +```rust +// loop_driver.rs:83 +if session.rtc.handle_input(Input::Receive(now, recv)).is_err() { … } +``` + +`str0m::Rtc::poll_output()` takes no `now`. The **only** way str0m's internal clock advances is a `handle_input(...)` call carrying an `Instant`. Since the loop only calls `handle_input` when a datagram is actually waiting (`recv_from` returns `Ok`), during any stretch with no inbound packets str0m's notion of "now" is **stuck at the last received packet's timestamp**. + +The spec (§3.4, lines 197-199) describes the intended drive loop as: *"`poll_output() -> Output` … where `Output::Timeout(Instant)` gives the next deadline we sleep tokio until."* The implied second half — *sleep, then feed `Input::Timeout(now)` back* — is missing. + +**Why it matters.** str0m schedules on timers: DTLS handshake retransmission (on loss), ICE connectivity checks + **consent freshness** (RFC 7675, ~every few seconds), RTCP SR/RR, pacing/BWE. All of these fire from `poll_output` *only after the clock has advanced past their deadline*. With the clock frozen during inbound silence, none of them fire. + +**The honest nuance (test this).** During an *active* two-way echo, the browser sends RTP every 20 ms, so `Input::Receive` advances str0m's clock ~every 20 ms and the late-but-present clock lets RTCP/consent fire (a little behind). **That is why the loopback demo can appear to work.** The failure modes are specifically: +- **One-way / sustained silence** (mic muted, hold): no inbound RTP → clock frozen → str0m stops emitting consent checks → the *browser* may tear the call down on consent failure (~15-30 s) **before** the 60 s idle timeout ever triggers. +- **Handshake packet loss** with no other inbound traffic to advance the clock → server-side DTLS/ICE retransmit never scheduled → potential stall. + +**Adversarial challenge.** Refute by showing **either** (a) str0m 0.21 advances internal time without `Input::Timeout` (check `Rtc::poll_output` / `Rtc::handle_input` in the str0m 0.21 source — Claude believes it does not), **or** (b) that the browser's continuous traffic provably masks every timer path that matters for the slice-1 acceptance ("hear yourself echo"), in which case downgrade severity but keep it as a latent bug for slices 2-4 (barge-in especially needs reliable timing). + +**Suggested fix.** Feed the deadline back. Minimal shape: + +```rust +// near the top of drive(), after Step 1, before the drain: +if session.next_timeout.map_or(true, |t| now >= t) { + let _ = session.rtc.handle_input(Input::Timeout(now)); +} +``` + +…and/or have the driver actually honor the returned `Duration` (sleep until `min(str0m_deadline, outbound_tick)`), which makes the existing `next_timeout` computation load-bearing instead of dead (see F3). + +**Falsification test.** Add a headless test that: constructs a session, feeds a synthetic `Input::Receive`, drains, then advances wall-clock 6 s **with no further packets** and asserts that `drive()` causes a `handle_input(Input::Timeout(_))` (spy via a wrapper) — or, end-to-end, that str0m emits an RTCP/consent `Transmit` on schedule during inbound silence. Today it will not. + +--- + +## F2 — A second offer to a session panics (`assert!` on an attacker-reachable path) + +**Severity: Medium-High. Confidence: High.** + +```rust +// rtc_session.rs:183 +pub fn accept_offer(&mut self, offer_sdp: &str) -> Result { + assert!(self.audio_mid.is_none(), "accept_offer called twice"); +``` + +`post_offer` calls this under the session lock with **no guard** against repeats (`routes.rs:60`). The endpoint is unauthenticated (slice-1 by design). A client that POSTs `/v1/sessions/:id/offer` twice for the same id panics the handler task on the second call. + +`tokio::sync::Mutex` does **not** poison on panic, so the process survives and other sessions are unaffected — but it is still a remotely-triggerable panic returning a broken/aborted response instead of a clean `409`. `assert!` is the wrong tool on a request path fed by external input. + +**Suggested fix.** Return a typed error (`RtcSessionError::AlreadyNegotiated` or similar) and map it to `409 Conflict` in `post_offer`. Keep `debug_assert!` if you want the invariant documented for tests. + +**Falsification test.** `POST` the bundled `BROWSER_SDP_OFFER` twice to one session id; assert the second returns a 4xx, not a panic. Today it panics. + +--- + +## F3 — Dead deadline plumbing (couples to F1) + +**Severity: Medium. Confidence: High.** + +`drive()` computes `next_timeout` through three arms (incl. borrow-checker-driven comments), stores it (`session.next_timeout = next_timeout`, `loop_driver.rs:227`) and returns `next_timeout.map(|t| t.saturating_duration_since(now))`. But: + +- `grep next_timeout` shows `session.next_timeout` is **written, never read**. +- The caller discards the return: `let _ = s.run_poll_once(now);` (`session_map.rs:114`), driving instead on a fixed `tokio::time::interval(10ms)` (`session_map.rs:86`). + +So the loop carefully derives str0m's next deadline and routes it to `/dev/null`, while the thing str0m actually needs (the `Input::Timeout` feed, F1) is absent. This is the smell that *most* corroborates F1: the loop looks like a deadline-driven sans-IO loop that lost its final wiring step. + +**Decide one way:** either (a) the fixed 10 ms tick is the intended slice-1 design — then delete `next_timeout`/`session.next_timeout`/the return value and update the spec §3.4 "sleep until the deadline" language, **and** still fix F1; or (b) honor the deadline — wire the return into the sleep and feed `Input::Timeout`, which fixes F1 and F3 together. Right now it is neither, which is how F1 hid. + +--- + +## F4 — Idle timeout keys on "any datagram," not "RTP" (spec deviation) + +**Severity: Medium. Confidence: High.** + +Spec §4.5: *"Idle timeout: 60 s of no **RTP** packets received from the peer → close."* Implementation: + +```rust +// loop_driver.rs:83-87 — runs for EVERY datagram str0m demuxes (STUN/DTLS/RTP) +if session.rtc.handle_input(Input::Receive(now, recv)).is_err() { … } +session.last_rx = now; +``` + +`last_rx` is bumped on every inbound datagram, including the browser's periodic STUN consent checks. A peer that stops sending **audio** but keeps ICE alive resets the timer forever and is never reaped within 60 s. The code comment ("60 s no RX") quietly redefines the spec's guarantee. + +**Fix.** Bump an RTP-specific timestamp only on `Event::MediaData` in `handle_event` (that *is* "RTP received, depacketized"), and key the idle check off that. Or amend the spec to say "no datagrams" and accept the weaker guarantee. + +**Falsification test.** Drive a session past DTLS, then feed only STUN-shaped keepalives (no MediaData) for 61 s; assert it closes. Today it stays open. + +--- + +## F5 — Unbounded session creation (resource exhaustion) + +**Severity: Low-Medium. Confidence: High.** + +`POST /v1/sessions` → `create_session` → `RtcSession::new()` binds a UDP socket (`rtc_session.rs:132`) with no cap, no rate limit, no per-client accounting. Orphan sessions are reclaimed only after the 60 s idle timeout, so a tight create loop can hold up to (rate × 60 s) live sockets/FDs/ephemeral ports. "No authn" is documented as deferred (§1.2), but a resource ceiling is a *separate* concern from auth. Acceptable for the loopback dev build; flag explicitly for the step-5 deployment posture so it isn't forgotten. + +**Note:** `axum`'s default body limit (~2 MB) does cap the offer body, so the SDP path itself isn't an unbounded-memory vector — only the session/FD count is. + +--- + +## F6 — `created_at` is dead and mis-documented; spec self-contradicts (5-min vs 60 s) + +**Severity: Low. Confidence: High.** + +`Channel.created_at` (`rutster-call-model/src/lib.rs:140`) is documented "For the 60 s idle timeout," but the idle timeout uses `RtcSession.last_rx`, not `created_at`; the field is never read. The spec adds confusion: §4.5 says "60 s," but the struct sketch (spec line 371) and `created_at`'s lineage say "5-min idle timeout." Pick one number, and either wire `created_at` or drop it. + +--- + +## F7 — Unreachable `needs_redrain` branch in Step 2 + +**Severity: Low (clarity). Confidence: High.** + +```rust +// loop_driver.rs:108-119 +let mut needs_redrain = false; // set true ONLY in Step 3, below +loop { + match session.rtc.poll_output() { + Ok(Output::Timeout(t)) => { + next_timeout = Some(t); + if needs_redrain { needs_redrain = false; continue; } // always false here → dead + break; + } + … +``` + +`needs_redrain` cannot be true during the Step-2 loop (Step 3 sets it afterward; the real re-drain is the separate loop at `:201`). The branch is dead and misleads the reader into thinking Step 2 re-drains. Remove it. + +--- + +## F8 — Outbound RTP timestamp clock — **verify against str0m 0.21** + +**Severity: Low. Confidence: verify.** + +```rust +// loop_driver.rs:187 +session.next_media_time += str0m::media::MediaTime::from(Duration::from_millis(20)); +``` + +The comment reasons "960 ticks at 48 kHz," but the code adds a `MediaTime` built from a `Duration`. Whether the wire RTP timestamp advances by the correct 960/frame depends entirely on `str0m::media::Writer::write` rebasing the supplied `MediaTime` to the negotiated Opus payload clock (48 kHz). Claude believes str0m rebases (so this is *probably* fine), but could not confirm statically. + +**Verify:** read `Writer::write` + `MediaTime::from` in str0m 0.21. If `write` uses the numerator directly without rebasing, the timestamps are microsecond-scaled and playout timing is wrong (audible as garbled/late audio). Confirm with a packet capture of the answer stream if in doubt. + +--- + +## F9 — 24 kHz mono codec vs `opus/48000/2` SDP, and a tight decode buffer — **verify** + +**Severity: Low. Confidence: verify.** + +`SAMPLE_RATE = 24_000`, `Channels::Mono` (`opus_codec.rs:23-27`) while the negotiated rtpmap is `opus/48000/2`. Opus is self-describing (RFC 7587 fixes the SDP clock at 48000/2 regardless of internal rate), so this is *likely* legal and Chrome should decode it — **verify** the browser actually renders it cleanly. + +Separately, the decode buffer is exactly `[i16; 480]` (`opus_codec.rs:45`). A 20 ms frame at 24 kHz mono is exactly 480 samples, so anything larger — a 40/60 ms frame, or stereo — returns `OPUS_BUFFER_TOO_SMALL` → `decode()` yields `None` → frame silently dropped. Chrome defaults to 20 ms mono-ish, so the echo works, but it's a latent assumption worth a one-line comment or a guard. + +--- + +## Test-coverage gap (not a bug, but the reason F1/F2/F4 shipped) + +There is **no automated test that calls `loop_driver::drive()`** — the central function of the entire slice. Current coverage: +- `rtc_session` tests: `accept_offer` returns an answer of the right shape; state goes `New → Connecting`. +- `opus_codec` tests: encode→decode RMS; garbage → `None`. +- `pcm` tests: frame size; echo round-trip; bounded sink. +- `api_integration` tests: `POST /v1/sessions` shape; `GET /` content-type. + +None feed packets through the poll loop. A sans-IO test harness (synthetic `Input::Receive` / `Input::Timeout` in, assert `Output::Transmit` / state out — exactly the pattern str0m is designed for) would have caught the missing `Input::Timeout` (F1), the double-offer panic (F2), and the idle-timeout semantics (F4). This is the single highest-leverage addition. + +--- + +## Informational — design notes (deliberate, not defects) + +- **`tokio::sync::Mutex` for a synchronous critical section.** No lock is held across an `.await` anywhere (`accept_offer`, `run_poll_once`, `close` are all sync under the guard). A `std::sync::Mutex` / `parking_lot::Mutex` would be cheaper and avoid async-mutex overhead. Defensible given handlers are async; noting for later. +- **Single poll task, O(n) per 10 ms tick, sequential lock acquisition** (`drive_all_sessions`). Fine at slice-1 scale; `tokio::time::interval` defaults to `MissedTickBehavior::Burst`, so if a tick ever overruns 10 ms it will catch up in a burst. Revisit before multi-hundred-session scale (the spec already earmarks a dedicated timing thread for step 4). +- **`drive_all_sessions` does two map lookups** (`iter().collect()` then `get(id)`); a single `iter_mut`/retain pass would halve it. Micro. + +## What's solid (calibration — so the findings above are read in context) + +This is careful work, and saying so keeps the critique honest: +- Clean workspace with pinned `[workspace.dependencies]`; `Cargo.lock` committed for the binary. +- Sound error taxonomy: cold-path `RtcSessionError` enum vs hot-path `Option` "drop + observe" (§3.8) applied consistently in the codec and the loop. +- The `EchoAudioPipe` is genuinely non-blocking and bounded, with an overflow-drop test (`pcm.rs:147`). +- `ChannelId` newtype; `ChannelState` as a closed enum for exhaustiveness. +- Graceful shutdown on Ctrl-C/SIGTERM; sensible `tracing` throughout. +- CI gates are real: `fmt --check`, `clippy -D warnings`, `test --all`, `cargo deny`. (The current CI red is infra — runner DNS — not the code.) +- Doc comments explain *why*, not just *what*, and even record prior dead-ends (the BUNDLE-line fixture note, the `[0u8;8]` "silence not garbage" note). Above-average. + +## Cleared during this review (so you don't re-flag it) + +- **Browser client ICE gathering.** Suspected the client POSTs the offer before candidates are gathered. **False** — `static/index.html` awaits `iceGatheringState === 'complete'` before the POST (non-trickle done correctly). Not a finding. + +--- + +## Verification checklist for the GLM-5.2 reviewers + +1. **F1 is the one that matters.** Confirm str0m 0.21 does not self-advance time, then decide severity by testing the muted-mic / one-way-silence path against a real browser. Everything else is secondary. +2. Reproduce **F2** in under a minute (double POST) — cheapest confirmable bug. +3. Decide the **F3/F1** wiring direction (fixed tick vs honor-deadline) as one coherent fix, not two. +4. Treat **F8/F9** as str0m/Opus semantics homework — read the dependency, don't guess. +5. Push back on anything here you can falsify. A finding Claude can't defend should be struck, with the reasoning recorded, same as the cleared item above. diff --git a/docs/reviews/2026-06-28-vision-sanity-check.md b/docs/reviews/2026-06-28-vision-sanity-check.md new file mode 100644 index 0000000..5d40856 --- /dev/null +++ b/docs/reviews/2026-06-28-vision-sanity-check.md @@ -0,0 +1,141 @@ +# Claude Adversarial Sanity-Check — Rutster Vision & Strategy + +- **Reviewer:** Claude (Opus 4.8), peer-level read of the strategy docs (not the code — see the companion `2026-06-28-slice-1-claude-adversarial-assessment.md` for the code). +- **Targets:** `docs/superpowers/specs/2026-06-26-vision-revision-design.md`, `docs/ARCHITECTURE.md`, ADRs 0002–0006, as of `main@22d3f03`. +- **Question asked:** "Sanity-check the *idea*." This is a strategy critique, not an architecture-correctness review. + +## How to use this document (for the GLM-5.2 reviewers) + +Same contract as the code assessment: **these are claims to refute, not conclusions to accept.** Strategy has no unit tests, so each critique below carries three fields instead of a falsification test: + +- **Steelman (the doc's defense):** the strongest version of the current position. Try to make it win. +- **What would change the verdict:** the evidence or decision that flips the critique. +- **Decision implied:** the concrete thing to change *if* the critique holds — so this stays actionable, not philosophical. + +The author already pressure-tested this vision once (the 2026-06-26 revision reversed ADR-0001). Hold it to that standard: a critique you can dissolve should be struck with reasoning recorded. + +--- + +## Verdict + +**Stronger than "decent."** This is an unusually coherent strategy precisely because it already cannibalized its own founding premises — reversing the Kamailio/rtpengine C shield instead of rationalizing it is the tell that the thinking is honest. The **identity, architecture, and security posture are sound.** The vulnerability is **not the bet** — it's **sequencing and emphasis**: the pitch leads with the one leg a funded competitor can contest, and the moat-grade capabilities sit on the far side of a long solo valley. Both are fixable by reordering, not by changing the thesis. + +Confidence: high on the architecture/security being sound; medium on the strategic critiques (they're judgment calls about a market in motion, which is exactly why they're written to be argued with). + +--- + +## TL;DR — strategic findings + +| # | Type | One-line | +|---|------|----------| +| S1 | **Emphasis** | The technical wedge leads with no-GC determinism — the weakest, most benchmarkable leg — on ground LiveKit Agents will contest directly | +| S2 | **Sequencing (dominant risk)** | The spearhead (steps 1–6) lands at Vapi/Retell parity; every *differentiating* capability is past it, deep in the capability ladder — a long solo valley before the moat exists | +| S3 | **Positioning** | The wedge is "strongest for regulated" but regulated buyers are the most allergic to bus-factor-1 infra; first adopters are the builder persona, not the compliance prize | +| S4 | **Integration risk** | Core-authoritative VAD vs. the brain's own server-side turn detection (OpenAI Realtime) — "AudioOut advisory" meets a vendor with opinions | +| S5 | **Scope/timing** | Solo attack on a 1.18M-LOC-category problem against a window that may be closing; the long pole is integration/hardening, which AI assistance collapses least | +| S6 | **Reframe** | The self-improvement loop (own the call → auto-labeled takeover data) is the durable moat and is buried under determinism in the wedge framing | + +**Net:** S2 is the one that matters. S1 and S6 are the same fix from two directions (move emphasis off determinism, onto the boundary + data loop). S3–S5 are clear-eyes caveats to record, not blockers. + +--- + +## S1 — The technical wedge leads with its weakest leg + +**Type: Emphasis. Confidence: medium-high.** + +§3 frames the technical wedge as **(1) no-GC real-time determinism, (2) one secure auditable boundary, (3) operational simplicity**, and calls determinism *"the one thing a Go SFU + external agent structurally can't match on quality."* That ordering puts the most contestable claim first: + +- The dominant latency is the **brain round-trip** (hundreds of ms to a speech-to-speech API) — which rutster does not own. No-GC buys **tail smoothness** in the local loop, real but second-order to the RTT. +- The latency-critical reflex is **local VAD barge-in** — and competitors can also run local VAD. Barge-in *quality* in a no-GC loop is an edge, but it's a number a competitor can benchmark against, not a category they're locked out of. +- **LiveKit Agents** is the specific threat. The doc characterizes LiveKit as "horizontal media infra, no contact-center domain" — accurate for LiveKit *core* — but LiveKit is actively building the agent/turn-taking layer. Leading on determinism points the pitch at exactly the ground they're moving onto. + +The **structural** legs — single auditable boundary (#2) and data-ownership — can't be benchmarked away. Those should lead. + +- **Steelman:** for tight turn-taking the floor latency *does* matter, and a no-GC loop genuinely gives more predictable barge-in under load; "structurally can't match" is about the **whole** (self-host + audit + reflex), not the reflex alone — §3's honest caveat already says this. +- **What would change the verdict:** evidence that no-GC barge-in is a *primary* buying criterion (not a refinement) for the target persona — e.g., a head-to-head where GC jitter visibly degrades a competitor's turn-taking in normal use, not a synthetic stress test. +- **Decision implied:** reorder §3 — lead the technical wedge with the auditable boundary + data-ownership; keep determinism as the third support, framed as "quality refinement," not "the thing they can't match." + +## S2 — The differentiation valley (dominant strategic risk) + +**Type: Sequencing. Confidence: high that the valley exists; medium on its severity.** + +Walk the spearhead (§10) to its end: *"I called my Rust box, an AI answered, it barged in, and it can't blow my budget."* Motivating and real — but at that milestone the product is **at Vapi/Retell feature-parity while behind them on convenience** (they're managed; this is DIY self-host). Everything *categorically* different is **past** the spearhead, in the capability ladder (§11): human escalation (rung 2), containment measurement (rung 3), the self-improvement loop (rung 4), plus the actual contact-center domain library (ACD/queues/CDR/supervisor). + +So a solo builder spends enormous effort to reach "another AI voice bot you happen to host yourself" **before** reaching anything that is a moat. Motivation, validation, and early adoption all have to survive that crossing. + +- **Steelman:** the spearhead's job is to prove the *combination is technically real*, not to be differentiated — and "I called my Rust box and an AI answered" is the momentum fuel a solo build needs. Differentiation can follow once the core is proven. +- **What would change the verdict:** if "self-hostable AI voice bot" is *itself* enough of a draw for the §2 persona (data-ownership alone sells it), the valley is shallow and this de-rates to a caveat. +- **Decision implied:** pull **rung-2 human takeover** forward to sit adjacent to the spearhead. "AI answers, and the instant it flounders a human seamlessly barges in via the audiohook" is the earliest demo that is **not** Vapi-parity and proves the contact-center thesis — and §11 already notes rung 2 reuses the audiohook primitive PORT_PLAN kept. This reorders the proof so differentiation appears before momentum runs out. (Tradeoff: it widens the spearhead — weigh against "brutally thin.") + +## S3 — The compliance paradox + +**Type: Positioning. Confidence: medium-high.** + +§3 says the wedge is "strongest for regulated/high-trust (PCI/HIPAA/TCPA)." True for *fit* — but regulated buyers are the **most** risk-averse about brand-new, single-maintainer infra in the call path. Self-host cuts both ways: it removes "my data on a vendor's cloud" (good) and adds "we now operate unproven Rust telephony ourselves with bus-factor 1" (bad, for exactly a compliance-conscious org). The realistic first adopters are the **§2 builders** (doing it for a less-regulated client, or internal/experimental use), who *reach* the compliance segment later — the same arc Asterisk walked. + +- **Steelman:** self-hosting + auditability + data-ownership are *literally the buying criteria* in regulated spaces, and a builder-led bottom-up motion is how open infra always enters the enterprise — this is a feature of the path, not a bug. +- **What would change the verdict:** a regulated design partner willing to route real (or even shadow) traffic early — that would prove the compliance segment is reachable before the project is "proven," collapsing the paradox. +- **Decision implied:** state explicitly that **regulated is the eventual prize reached via the builder persona, not the beachhead.** Keep the §2 builder as the actual day-1 customer in all positioning, so the messaging doesn't chase a buyer who won't move first. + +## S4 — Turn-taking ownership at the tap + +**Type: Integration risk. Confidence: medium (depends on brain vendor).** + +ARCHITECTURE.md is right that the core should be authoritative on VAD/barge-in ("the tap carries the *results* of reflexes, not the responsibility"; "`AudioOut` advisory / core-authoritative"). But the most likely first brain — **OpenAI Realtime** — does **its own** server-side VAD and turn detection by default. Integrating step 3–4 means either: +- disable the brain's turn detection and feed it clean, locally-detected turns (preserves the reflex-authoritative principle, but is *more* integration work and fights the API's grain), or +- accept split-brain turn-taking where local VAD and the brain's VAD can disagree (double-triggers, dropped barge-ins). + +This is precisely where the elegant "core disposes" principle meets a vendor with opinions, and it's a *design* decision, not an implementation detail. + +- **Steelman:** the tap is deliberately core-as-client and format-canonical specifically so the core can drive turns and treat the brain as a dumb speech-to-speech transducer — Realtime supports disabling server VAD, so the clean path is available. +- **What would change the verdict:** a step-3 prototype showing Realtime-with-server-VAD-disabled gives acceptable turn quality fed by local VAD — then it's just integration, not a tension. +- **Decision implied:** make "who owns turn detection" an explicit decision in the step-3 (brain) design doc, defaulting to **core-authoritative, brain VAD off**, with the integration cost budgeted. Don't let it be discovered at wiring time. + +## S5 — Solo scope vs. category timing + +**Type: Scope/timing. Confidence: medium.** + +The thin-slice sequencing and AI-pair leverage are the right mitigations and they're real. The honest framing: this is a **1.18M-LOC-category** problem attacked solo, and the long pole isn't writing code (AI helps most there) — it's the **integration/hardening surface** of real-time telephony: NAT traversal, carrier signaling quirks (re-INVITE for hold/transfer, DTMF transport negotiation, PRACK/early media, IP-auth vs registration trunks, SRTP keying on the trunk side), codec edge cases, behavior under load, and security-hardening every wire parser. AI assistance collapses *that* the least. Meanwhile the "AI-era contact center is white space" window is open **now** but contested (incumbents bolting on AI; Vapi/Retell maturing and funded). + +- **Steelman:** "engine not product" bounds the scope hard, the capability ladder is genuinely shippable rung-by-rung, and a focused solo builder with AI leverage in 2026 is materially faster than the team-years Asterisk took — the comparison to 1.18M LOC is unfair because most of that is breadth rutster deliberately rejects. +- **What would change the verdict:** sustained shipping velocity through steps 1–5 (the trunk client especially) on the planned timeline — evidence the hardening pole is shorter than feared. +- **Decision implied:** ruthlessly sequence toward the **differentiated** demo (see S2), not breadth — every month spent on capability that overlaps Vapi is a month the window can close. Treat the SIP-trunk hardening (ADR-0003) as the schedule risk it is; WebRTC-first ordering already hedges it. + +## S6 — Reframe: the self-improvement loop is the actual moat + +**Type: Reframe. Confidence: high that it's the most durable advantage.** + +The capability-ladder rung 4 — **own the whole call → every human takeover auto-labeled "AI failed here → human did this" → containment analytics + eval sets + fine-tune export, all on the operator's own infra** — is the one advantage that is: + +1. **structurally impossible** for cloud bots (they don't own your data) and CCaaS (not their business model), +2. a **compounding data-network-effect** (better the more you use it), and +3. **uniquely enabled** by self-hosting + owning the entire call. + +That is a durable moat in a way no-GC determinism is not. The north-star one-liner already names it ("a closed ML loop that learns from every escalation") — but the §3 *technical-wedge* framing buries it under determinism. S1 and S6 are the same correction from opposite ends: **the wedge is the auditable boundary + the data loop; determinism and simplicity are supporting cast.** + +- **Steelman:** the loop is rung 4 — years out — so leading with it oversells what exists; determinism is what the *spearhead* actually demonstrates, so it's honest to lead with it now. +- **What would change the verdict:** nothing about *whether* the loop is the moat; the open question is *timing of the claim* — see decision. +- **Decision implied:** separate "what the spearhead proves" (the combination works) from "what the wedge is" (boundary + data-ownership + loop). Lead strategy/positioning with the latter even while the spearhead demonstrates the former. Don't let the demo's emphasis become the strategy's emphasis. + +--- + +## Architecture decisions — spot-check (mostly sound; recorded for completeness) + +These are not strategy critiques; they're the architecture calls the strategy rests on, and they hold up: + +- **Remove the control↔media gRPC hot-path hop / fused per-call vertical** (ADR-0002) — correct; that hop was always a latency + failure-mode liability. +- **Spend gate co-located with trunk termination** — genuinely strong and *under-sold*; "a runaway brain can't exceed spend because it doesn't hold the wire" is a demonstrable structural property a 3-vendor stack cannot match. Promote it in the pitch. +- **Core-as-client tap, no inbound port; core-authoritative playout** — correct security instinct; deletes an attack class. +- **WebRTC-first ingress** (ADR-0006) — smart de-risking; first-call never blocks on SIP. +- **Rust-native trunk SIP, no C shield** (ADR-0003) — right call for the scoped use case (interop tail collapses to a few cooperative carriers; parser-security thesis becomes literally true). Mild caveat: even cooperative carriers carry a provider-independent signaling-state-machine complexity that "a few documented providers" slightly undersells — the cost is accepted, just don't let the framing make it sound small. +- **Valkey event bus, 20 ms loop never on the bus, bus not source-of-truth for billing-critical state** (ADR-0005) — correct discipline. + +--- + +## Verification checklist for the GLM-5.2 reviewers + +1. **S2 is the load-bearing critique.** Decide whether "self-hostable AI voice bot" is independently compelling to the §2 persona. If yes, S2 de-rates and the spearhead stands as-is. If no, pull rung-2 takeover forward. Everything else is secondary to this call. +2. **S1 + S6 are one decision:** move the wedge's emphasis off determinism onto boundary + data loop — or refute that determinism is benchmarkable/contestable. +3. **S4 belongs in the step-3 design doc**, not here — make sure it lands there as an explicit turn-ownership decision. +4. Push back on **S3/S5** with evidence (a regulated design partner; demonstrated velocity) — those are the critiques most likely to dissolve with a single data point. +5. Strike anything you can dissolve, with reasoning recorded — same standard the vision held itself to when it reversed ADR-0001. diff --git a/docs/reviews/2026-06-29-strategic-reviews-post-pivot-rescore.md b/docs/reviews/2026-06-29-strategic-reviews-post-pivot-rescore.md new file mode 100644 index 0000000..e66f9d1 --- /dev/null +++ b/docs/reviews/2026-06-29-strategic-reviews-post-pivot-rescore.md @@ -0,0 +1,122 @@ +# Re-scoring the strategic reviews under ADR-0007 / ADR-0008 + +- **Date:** 2026-06-29 +- **Re-scores:** `2026-06-28-vision-sanity-check.md` (S1–S6) and `2026-06-28-gtm-path.md` (G1–G6, P1–P4, R1) +- **Trigger:** the 2026-06 strategic pivot — ADR-0007 (rent the trunk transport; no first-party SIP stack) + ADR-0008 (the FOB / green-zone build-vs-reuse doctrine). README, ARCHITECTURE, PORT_PLAN, AGENTS all updated to match. + +This doc does **not** ratify any strategy changes. It re-scores each finding against the +post-pivot state so the load-bearing decisions surface as actionable items rather than +sitting in unaddressed review docs. Per AGENTS.md, ratification of strategy/ADR changes +warrants the user's signoff — this is the input to that decision, not the decision itself. + +--- + +## Verdict changes (summary) + +| Finding | Pre-pivot | Post-pivot | Action | +|---|---|---|---| +| **S1** (wedge leads with contestable determinism) | open | **Strengthened** — pivot ratifies the critique verbatim | Note only | +| **S2** (differentiation valley — pull rung-2 forward) | open, load-bearing | **Sharper but easier** — ADR-0007 retires the longest pole (trunk-SIP rewrite); valley shortens, core thesis stands | Sequencing (capability ladder planning) | +| **S3** (compliance paradox — builders are day-1, not regulated) | open | **Reinforced** — ADR-0007's "PSTN data-ownership is a graduation, not day-one" matches S3's "regulated is the eventual prize, not the beachhead" | Note only (README already honest) | +| **S4** (turn-taking ownership — OpenAI Realtime server VAD vs. core-authoritative) | open, slice-3 spec decision | **Unchanged + clarified** — ADR-0008 makes the OpenAI adapter green-zone and the reflex loop FOB; core-authoritative VAD is the only doctrine-consistent choice | Slice-3 spec decision (deferred until slice-2 green) | +| **S5** (long pole = integration/hardening) | open | **Partially retired** — ADR-0007 retires the named schedule risk (SIP-trunk rewrite); remaining long pole is the reflex loop, which is also the differentiator | Note only | +| **S6** (self-improvement loop is the actual moat) | open | **Strengthened** — ADR-0008 admits "the differentiating" as a FOB criterion; ARCHITECTURE's "Biggest technical risk" re-write makes the same correction from the other end | Note only | +| **G1** (OSS→contract→hosted sequence) | open | **Unchanged** | Note only | +| **G2** (CLA + commercial dual-license before contributor #2) | open — *the* load-bearing finding | **Strengthened + urgent.** ADR-0008 explicitly states multiple coding agents now work this repo — the CLA window is *already closing*. Every commit an agent makes without a CLA in place erodes the option. | **Needs active decision** (not a doc edit) | +| **G3** (services as early engine + tripwires) | open | **Unchanged** | Note only | +| **G4** (hosted = BYO-cloud, not multi-tenant SaaS) | open | **Strengthened** — ADR-0008's green-zone framing makes multi-tenant SaaS a doctrine violation (FOB-protected data on green-zone infrastructure) | Note only | +| **G5** (open-core seam: free wedge, paid convenience/scale) | open | **Unchanged** — gated by G2 | Note only | +| **G6** (bootstrap-via-services → optional raise) | open | **Unchanged** | Note only | +| **P1** (identity: substrate, not contact-center engine) | open, high-leverage | **Sharpened** — pivot makes the "white space = contact-center + reflex/eval loop + tap-as-open-protocol" framing explicit; README still says "AI-era contact center." Positioning decision. | Positioning decision | +| **P2** (beachhead: outbound + compliance, gated by R1) | open, gated by R1 | **Reshaped but not retired** — ADR-0007 makes outbound *easier* (no trunk to build; CPaaS call-control API for outbound dial); R1's consented-enterprise scoping + AUP guardrails stand | Sequencing decision | +| **P3** (north star: nothing leaves your boundary, including the model) | open, directional | **Strengthened as direction** — ADR-0008 admits the brain as green-zone but doesn't prevent an in-boundary open-weights brain later; the tap is brain-agnostic | Note only | +| **P4** (form: "Home Assistant for voice AI") | open, later identity | **Unchanged** | Note only | +| **R1** (outbound beachhead is double-edged) | open, gates P2 | **Unchanged** — guardrails stand | Note only | + +--- + +## The one finding that needs active decision: G2 + +> **Adopt a contributor-licensing posture (DCO at minimum; CLA with copyright control +> if you want the commercial options open) before merging the first external PR.** + +ADR-0008 explicitly states: *"multiple coding agents now work this repo and need one +explicit, mechanical rule so they classify consistently."* The same observation applies +to copyright: every commit an agent makes without a CLA in place narrows the option to +ever go AGPL, dual-license commercially, or sell paid-embedding. None of these must be +*used* — but all close silently without a CLA. + +Today the user is the sole author → total copyright control is free. The CLA question +becomes irreversible the moment an external PR merges. Coding agents (me included) are +in a gray zone — derivative work made at the user's direction is the user's, but if the +project ever wants to dual-license, the chain of authorship needs to be clean. + +**This is a strategy decision the user must make consciously; I cannot ratify it +automatically.** The action item is: address G2 before merging any external PR (and +ideally before the project's identity pivots — P1 — gets committed to positioning that +might attract contributors). + +--- + +## Findings now folded into the pivot itself + +These findings were *ratified* by ADR-0007 / ADR-0008 — the docs now carry the position +the review advocated. No further action beyond recording the alignment: + +- **S1** (wedge emphasis on determinism is the contestable leg): ARCHITECTURE.md's + "Biggest technical risk" rewrite ("no longer the SIP stack → the reflex loop itself, + which is also the differentiator") is the same correction the review raised. Pivot + ratifies S1. +- **S3** (compliance paradox — builders, not regulated, are day-1): ADR-0007's "PSTN + data-ownership dilutes in layer 1; it's a graduation" matches the review's "regulated + is the eventual prize reached via the builder persona, not the beachhead." README + already re-scoped wedge bullet 2 honestly. +- **S5** (long pole = integration/hardening, AI helps least): ADR-0007 retires the + named schedule risk (SIP-trunk rewrite) and redirects the solo-years to the + differentiation surface. The remaining long pole (reflex loop hardening) is also the + differentiator, so it's not undifferentiated plumbing. S5 substantially dissolves. +- **S6** (the self-improvement loop is the actual moat, not determinism): ADR-0008's + FOB admission criteria make the data/eval loop FOB-by-doctrine (it's differentiating); + ARCHITECTURE's risk rewrite makes the same correction. + +--- + +## Findings that remain open + actionable + +- **S2** (differentiation valley — pull rung-2 forward): **sequencing work, not doc + work.** ADR-0007 shortened the valley, but the core thesis (spearhead-end = Vapi + parity; differentiation lives in capability ladder + spend-gate) stands. When + scheduling rung-2 (human takeover via audiohook), weigh against the spearhead's + "brutally thin" mandate. +- **S4** (turn-taking ownership at the tap): **slice-3 spec decision.** Unchanged by + the pivot; ADR-0008 clarifies it. Slice-3 spec must make "who owns turn detection" + an explicit decision defaulting to **core-authoritative, brain VAD off**, with the + integration cost budgeted. Deferred until slice-2 lands green. +- **G2** (CLA posture): **needs active user decision.** See section above. +- **P1** (identity: substrate, not contact-center engine): **positioning decision.** + README still says "AI-era contact center." The pivot sharpens the alternative framing + ("self-hostable voice-agent substrate") without endorsing it. User's call. +- **P2** (beachhead: outbound + compliance, gated by R1): **sequencing decision.** + ADR-0007 makes outbound easy (rented CPaaS call-control API), but R1's guardrails + (consented enterprise outbound, sell-through-contract, AUP) still gate the beachhead. + +--- + +## Findings unchanged (recorded for completeness) + +G1, G3, G4, G5, G6, P3, P4, R1 — see the GTM and vision-sanity-check docs for their +original rationale. None of these were materially moved by the pivot; their original +verdicts and decision-implied items stand. + +--- + +## Reference: the load-bearing pivot docs + +- [ADR-0007](../adr/0007-trunk-rented-transport.md) — rent the trunk transport; no + first-party SIP stack. Supersedes ADR-0003. +- [ADR-0008](../adr/0008-fob-and-green-zone.md) — the FOB / green-zone build-vs-reuse + doctrine. +- [Vision sanity-check](2026-06-28-vision-sanity-check.md) — the original review + (S1–S6). +- [GTM / positioning / pivots](2026-06-28-gtm-path.md) — the original review + (G1–G6, P1–P4, R1).