Commit Graph

29 Commits

Author SHA1 Message Date
opencode controller
568934177d test(tap): golden-fixture wire-byte locks for slice-3 protocol events (spec §7.4)
slice-3 §7.4: external golden JSON fixtures + a new integration-test
file locking the exact on-wire bytes produced by each slice-3 encode_*
function. The inline round-trip tests in protocol.rs use String::contains
(substring) which cannot catch field-order drift, stray fields, or number
formatting changes — all of which the Envelope Serialize impl promises to
keep stable (protocol.rs:115-118: "field order is pinned to make the wire
bytes deterministic"). These fixtures make that promise load-bearing.

5 fixtures cover the 5 new event kinds (spec §3.2-3.3):
  speech_started, speech_stopped, function_call,
  function_call_output, tools.update.

Each fixture asserts (a) encode_* output equals the fixture bytes exactly
(string equality, not serde_json::Value equality — the latter would
erase the field-order pinning the serializer guarantees), and (b) the
fixture decodes back to the expected DecodedPayload variant with the
expected fields.

Plus a forwards-compat re-assertion: unknown wire type -> DecodedPayload::Unknown
(via FrameKind's #[serde(other)]), not an error — the foundation of the
slice-2 §3.4 "drop + observe" posture that lets a future-protocol brain
not crash an older core.

On first run the golden test caught real drift: serde_json (without
preserve_order) alphabetizes nested object keys, so tools.update's tools
array serializes description-before-name, not input order. Fixture
corrected to lock the actual contract.

11 new tests, all green. cargo fmt + clippy -D warnings clean.
2026-07-01 01:47:22 -04:00
opencode controller
ab1ab8192c feat(brain-realtime): MockRealtimeBrain + binary (spec §5.3 + §4.2)
slice-3 §5.3 dev mode: an in-process fake OpenAI Realtime WS server
(MockRealtimeBrain) behind --features=mock. Asserts session.update has
turn_detection: null (S4 — spec §4.3) on handshake; exports a typed
OpenAI-shaped error on violation so the brain fails fast + visibly.
On each input_audio_buffer.append replies with a canned
response.audio.delta carrying 480 zeroed samples as base64 LE i16
(same wire shape as slice-2's audio_out). Powers the offline dev loop
(cargo run -p rutster-brain-realtime --features=mock) + the integration
test (no real OpenAI credentials, no network calls).

Also adds the binary main.rs (spec §4.2 + §5.3): binds the tap-side WS
server on RUTSTER_TAP_BIND (default 127.0.0.1:8082), accepts per-call
tap connections, handles the hello handshake, splits the WS into sink +
stream, dials the OpenAI side (real wss://api.openai.com in default
mode; MockRealtimeBrain url in mock mode), and bridges via
run_openai_pump + two mpsc::channel<String> pairs.

Integration test brain_mock_round_trip drives MockRealtimeBrain +
run_openai_pump end-to-end: pushes a tap audio_in frame through the
pump, observes the canned response.audio.delta arrive + get translated
back as a tap audio_out frame on the pump's outbound mpsc. Verifies
the dev-loop 'actually start + interop on a basic audio round-trip'
contract the directive's Task 4 calls out, without requiring a real
WebRTC peer (same constraint as slice-1/2's browser-driven e2e).

Files: crates/rutster-brain-realtime/src/{lib,mock,main}.rs,
crates/rutster-brain-realtime/tests/brain_mock_round_trip.rs.
NOT touched: loop_driver.rs, rtc_session.rs (seam §7.5 #6).

Gates: cargo fmt OK. cargo clippy --all --tests --features=mock
-D warnings OK. cargo test --all --features=mock OK (56 tests).
cargo deny check has pre-existing environmental failure (CVSS 4.0
unsupported in advisory-db; same on main).

Dev loop verified: cargo run -p rutster-brain-realtime --features=mock
starts the brain (mock on ephemeral port, tap server on :8082).
./target/release/rutster with RUTSTER_TAP_URL=ws://127.0.0.1:8082/realtime
starts the core on :8080. POST /v1/sessions succeeds. (Full audio
round-trip requires a browser-driven WebRTC peer, same constraint as
slice-1/2 — covered by the integration test instead.)
2026-07-01 01:47:22 -04:00
opencode controller
52a915c1cb feat(tap): tool-call side-channel wiring (spec §5.2)
slice-3 §5.2 + §6: the binary's poll task now drains the brain's
function_call proposals from rx_function_call, dispatches through the
per-channel ToolRegistry (HangupTool wired at spawn_tap_engine time),
and writes function_call_output replies back through tx_function_call_output
which run_tap_client forwards as tap WS frames to the brain.

TapClient: handle_brain_frame now forwards function_call events to a
new tx_function_call mpsc side-channel instead of dropping them.
run_tap_client adds a select! arm draining rx_function_call_output +
sending each as a tap frame. Advisory events (speech_started/stopped,
tools.update) still log + count (slice-3 deferred-action posture).

TapEngine: spawn_tap_engine now takes AppState + constructs a per-channel
ToolRegistry (spec §6.2) with HangupTool pre-registered (§6.3). TapConn
gains rx_function_call, tx_function_call_output, tool_registry fields.

session_map: drive_all_sessions calls drain_function_calls in the same
cycle as the slice-2 §5.3 step 4 flush drain (one extra channel, same
cycle); the helper spawns each dispatch as its own task so the 750 ms
hangup teardown bound (AppState::close) can't stall the 10 ms poll
cadence.

files touched: crates/rutster-tap/src/{lib,tap_client}.rs,
crates/rutster/src/{session_map,tap_engine}.rs,
crates/rutster/tests/tap_integration.rs ( AppState arg ),
crates/rutster-brain-realtime/src/translator.rs (clippy needless_borrow ).

NOT touched: loop_driver.rs, rtc_session.rs (seam test §7.5 #6).

gates: cargo fmt --check OK. cargo clippy --all --tests -D warnings OK.
cargo test --all OK. cargo deny check has pre-existing environmental
failure (CVSS 4.0 unsupported in advisory-db; same on main).
2026-07-01 01:47:22 -04:00
opencode controller
d8554c3594 feat(binary): tool_registry + hangup tool (spec §6)
FOB-boundary dispatch for brain-proposed tool calls (ADR-0007: 'rutster
mediates both the provider call-control API and the brain tap, so the
brain never holds the wire'). Slice-3's only wired tool is hangup
(fires AppState::close → existing slice-2 teardown); other tool names
reply not_implemented.

TDD: 5 tests (dispatch known + unknown tool, catalog listing, hangup
schema shape, hangup_call fires AppState::close).
2026-07-01 01:47:22 -04:00
opencode controller
b61a22ed56 feat(brain-realtime): OpenAI wss client pump (spec §4)
Builds session.update with turn_detection: null on handshake (S4,
encoded in the translator's build_openai_session_update; Task 4).
Runs a select! loop over tap-side input (audio_in → append,
function_call_output → conversation.item.create) and OpenAI-side
input (response.audio.delta → tap audio_out, speech_started/stopped
→ tap speech_started/stopped, function_call_arguments.done → tap
function_call). 401 surfaces as OpenAiClientError::AuthFailed.

URL + headers shape tested directly. Full pump loop tested via
MockRealtimeBrain in Task 10 — neither side of this pump is a real
network endpoint in unit tests.
2026-07-01 01:47:22 -04:00
opencode controller
e791d422ed feat(brain-realtime): translator — tap ⇄ OpenAI Realtime event mapping (spec §4)
Pure functions, no I/O, no call state. The OpenAI-side WS client
(Task 5) and the tap-side WS server (Task 9) call these per event.

S4 turn-ownership decision (spec §4.3, load-bearing per ADR-0008)
encoded in build_openai_session_update: turn_detection: null. OpenAI's
server-side VAD is disabled; the FOB reflex loop (step 4) owns
turn-taking; tap playout stays core-authoritative (slice-2 §4.1).

S4 test verifies the load-bearing assertion
(v['session']['turn_detection'] == Null).

Audio is pass-through — OpenAI's PCM base64 (LE i16 24 kHz mono) is
indentical to slice-2's tap PCM wire shape (spec §3.5); decode +
re-encode as a PcmFrame so the playout ring stays byte-aligned for
slice-2's playout-buffer invariants (samples: 480).
2026-07-01 01:47:22 -04:00
opencode controller
405cb7ef83 feat(brain-realtime): API-key loader + env-var/file-path posture (spec §5.3)
OPENAI_API_KEY default + OPENAI_API_KEY_FILE override (file wins —
k8s-secret pattern). Trims trailing whitespace (some k8s mounts add
newlines). KMS/Vault integration is step-6; the file-path override
makes secret-manager injection trivial later.

TDD: 5 tests (env-set, file-overrides-env, trim-newline, missing-file,
neither-set); verified deliberately-introduced-bug failure (upper()
breaks the env-var assertion).
2026-07-01 01:47:22 -04:00
opencode controller
d1f4a9fcf4 feat(tap): additive v1 protocol extensions (spec §3) — speech_started/stopped, function_call, function_call_output, tools.update
Five new event types in slice-2's v1 protocol. Forwards-compatible
per slice-2 §3.4: FrameKind's #[serde(other)] Unknown absorbs the
new types in old brains (they log + count + drop). No wire-format
break, no version bump.

New kid on the dispatch: tools.update — brain declares its catalog
on hello so the core's tool registry can validate function_call
events by name (Task 6). The 'function_call'/'function_call_output'
pair is the FOB-boundary dispatch contract the brain's translator
(Task 4) wires to OpenAI Realtime's
response.function_call_arguments.done / conversation.item.create.

TDD: 6 tests fail-red on missing impl, pass-green after this task.
Slice-2's existing protocol tests stay green — the additions are
purely additive.
2026-07-01 01:47:22 -04:00
opencode controller
b56f57bf7f feat(slice-3): +rutster-brain-realtime crate skeleton + async-trait dep (spec §1.1)
Workspace member 8 of 8. Library + binary; mirrored slice-2's
rutster-tap-echo shape. Three stub modules (api_key, translator,
openai_client) filled in by Tasks 3-5. No behavioral code yet —
this task only locks the crate boundary + the new async-trait dep
(the Tool trait in Task 6 needs it for async-fns-in-trait-objects).
2026-07-01 01:47:22 -04:00
opencode controller
f7fc94680d docs(rutster-media): complete rutster-trunk rename in rtc_session doc
The RtcSession module doc cited the dropped rutster-signaling-sip crate
and a 'future SIP SDP path' that ADR-0007 superseded. Re-anchor on
ADR-0007: no first-party SIP/SDP — the trunk is rented (CPaaS media-leg)
or out-of-tree SBC, so rutster's SDP is WebRTC-only via str0m. Final
straggler of the rutster-signaling-sip → rutster-trunk rename.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KhhqKG4cra7d1PBoVbj9UJ
2026-07-01 01:47:22 -04:00
opencode controller
2bc9877105 fmt: apply rustfmt across slice-2 use-statement drift
Some checks failed
CI / fmt (pull_request) Successful in 56s
CI / clippy (pull_request) Failing after 28s
CI / test (1.85) (pull_request) Failing after 27s
CI / test (stable) (pull_request) Failing after 28s
CI / deny (pull_request) Failing after 27s
2026-07-01 00:18:04 -04:00
opencode controller
3c3197e57a refactor(workspace): rutster-signaling-sip → rutster-trunk; edition 2024; repo URL = git.adlee.work
ADR-0007 lands in code:
- Rename crates/rutster-signaling-sip → crates/rutster-trunk. The crate was
  a stub anyway (lib.rs with a doc-comment + crate_compiles() test); the
  rename locks the new boundary shape — the future rented-transport
  ingress (CPaaS media-leg fork / out-of-tree SBC glue, **no SIP stack**)
  lands here at spearhead step 5.
- rutster-trunk/src/lib.rs doc-comment now describes the ADR-0007 split:
  CPaaS media-leg adapter as primary, out-of-tree SBC for on-prem
  graduation. Cross-refs ADR-0007 + ADR-0008 (FOB vs. green zone: trunk
  is green zone; SIP lives outside the trust boundary).
- rutster-spend/src/lib.rs doc-comment updated to quote ADR-0007's
  'rutster mediates both the provider call-control API and the brain tap'
  framing — the spend gate sits in that boundary, structurally preventing
  a runaway brain from exceeding spend/pacing. Pulling spend out into a
  service re-introduces the 3-vendor structural hole.

Cargo bumps:
- edition = '2024' (slice-1's pinned Rust 1.85 + edition-2024 floor
  already requires this; rutster-media's let-else pattern in
  OpusDecoder::decode · Slice-2's let-else in AcceptOffer's Uuid
  parse_str · all rely on edition 2024's stabilized let-chains / let-else).
- repository = 'https://git.adlee.work/alee/rutster' (the self-hosted
  Gitea remote — matches 'git remote -v' origin). The github.com/anomalyco
  URL was stale from the pre-pivot copy.
- Cargo.lock regenerated by cargo for the rename.

No behavioral code changes — the trunk crate's body is still the stub
crate_compiles() test. FOB membership (per ADR-0008) is unchanged:
rutster-trunk will be FOB-internal at step 5 because it's the media-leg
ingress (hot path); spend-spend stays FOB because spend is
security-constitutive.
2026-06-29 20:26:37 -04:00
opencode controller
b2aaf149b0 fix(slice-2): widen outer close-bound + strengthen reconnect-test assertion
Some checks failed
CI / clippy (pull_request) Failing after 43s
CI / test (1.85) (pull_request) Failing after 39s
CI / test (stable) (pull_request) Failing after 42s
CI / deny (pull_request) Failing after 39s
CI / fmt (pull_request) Failing after 10m45s
- session_map.rs: AppState::close outer timeout bumped 500ms → 750ms
  so the inner close-arm bound (500ms in tap_client.rs) has room to
  finish `ws.close(None).await` cleanly before the abort fallback
  fires. Outer > inner per final-fixes re-review Important #1.
- tap_integration.rs: reconnect-path test now asserts the resumed
  `next_pcm_frame()` returns the FRESH marker (samples[0] == 9, not
  the stale samples[0] == 7), actively witnessing the §5.3 step 4
  "no stale bleed-through" contract. Pre-kill seeding strengthened
  to ≥2 frames so step 7's silence-after-flush assertion is non-
  vacuous. Per final-fixes re-review Important #2.

Spec ref: 2026-06-28-slice-2-agent-tap-design.md §5.1 step 5, §5.3 step 4.
2026-06-28 18:21:52 -04:00
opencode controller
0d666b5ba7 fix(slice-2): §5.1 teardown seq + §5.3 playout flush + §8.4 reconnect test + §7.2 index.html
Code-review findings from the slice-2 final whole-branch review:

- tap_client.rs: close arm sends session_end (not bye) per §5.2, awaits
  brain bye with bounded 500ms timeout, then ws.close(). Promotes the
  Task-7-documented Minor to Important's required spec compliance.
- session_map.rs: AppState::close() now clears channel.tap = None before
  Closing->Closed, sends close signal, bounded-awaits engine task for the
  teardown handshake (500ms cap), aborts as fallback. Per §5.1 step 5.
- pcm.rs + tap_audio_pipe.rs: AudioPipe trait gains clear_playout_ring()
  (default no-op for EchoAudioPipe); TapAudioPipe clears the VecDeque.
  RtcSession::clear_playout_ring delegates. Per §5.3 step 4 playout-on-
  disconnect-flush contract.
- tap_engine.rs: spawn_tap_engine creates + shares a flush_tx mpsc
  channel; run_engine_loop signals flush after each failed pump loop
  (before backoff); TapConn.flush_rx is drained by drive_all_sessions
  to call RtcSession::clear_playout_ring.
- tests/tap_integration.rs: new reconnect-path test asserts Channel
  stays Connected, metrics.reconnect_attempts increments, playout
  returns None during outage, playout resumes after EchoServer
  restart with fresh frames (no stale bleed-through). Per §8.4.
- static/index.html: one static line acknowledging external-brain
  routing per §7.2 (defer real-time tap status to step-3 GUI).

Spec ref: 2026-06-28-slice-2-agent-tap-design.md §5.1 step 5, §5.2, §5.3 step 4, §7.2, §8.4.
2026-06-28 18:17:31 -04:00
opencode controller
411276bfde test(slice-2): integration test + Python echo brain + LEARNING pointers
- Integration test: smoke test against in-process EchoServer (hello
  handshake round-trip). Full end-to-end + reconnect-path tests adapt
  to slice-1's existing integration-test harness.
- examples/echo_brain/: Python reference brain (~80 lines, websockets lib).
  README documents runtime posture + why a Python brain complements the
  Rust one (language-agnosticism vs wire-types-reusability).
- LEARNING.md: 5 new pointers (mpsc/oneshot, VecDeque ring, async WS,
  Box<dyn Trait> field widening, zero-sized marker newtype).

Spec ref: 2026-06-28-slice-2-agent-tap-design.md §8.4, §8.5 #7.
2026-06-28 17:52:16 -04:00
opencode controller
84fc30591e fix(binary): spec-exact backoff staircase + remove dead Closing branch
- tap_engine.rs: Backoff::next_delay now matches spec §4.3 exactly:
  250ms -> 500ms -> 1s -> 2s -> 5s cap (no intermediate 4s step from
  naive doubling). Test asserts the spec enumeration including the
  "stays at 5s forever" cap-after-step-5 invariant.
- session_map.rs: removed the dead 'Closing && tap.is_some()' branch
  from drive_all_sessions. AppState::close already handles teardown
  inline (fires close_tx, aborts task, clears field, advances
  state, removes entry). The branch was never exercised and
  pre-paved a "future peer-initiated close" path -- both AGENTS.md
  anti-patterns ("don't pre-pave the wrong pattern").

Spec ref: 2026-06-28-slice-2-agent-tap-design.md §4.3 step 4 + §5.1 step 3.
2026-06-28 17:46:50 -04:00
opencode controller
6bc6f6426a feat(binary): wire TapEngine into session lifecycle (spec §5.1, §7)
- tap_engine.rs: spawn_tap_engine(session_id, tap_url) — dials the brain,
  runs TapClient with bounded exponential backoff (250ms→5s cap, infinite
  retries). Cold-path network I/O on tokio's pool, NOT a timing thread
  (spec §4.3 deviation boundary).
- session_map.rs: +tap_registry (DashMap<ChannelId, TapConn>); +default_tap_url.
  drive_all_sessions observes Connected+tap.is_none() → spawn TapEngine, set
  channel.tap = Some(TapHandle). Observes Closing+tap.is_some() → fire close
  oneshot, abort task, clear tap BEFORE state advances to Closed.
- routes.rs: POST /v1/sessions accepts optional {"tap_url":...} body;
  resolve_tap_url validates ws:// 127.0.0.1/localhost, rejects wss:// + non-
  loopback ws:// + bad schemes with 400 Bad Request (fail-fast per spec §4.4).
- main.rs: reads RUTSTER_TAP_URL env (default ws://127.0.0.1:8081/echo).
- rtc_session.rs (rutster-media): pipe field changed from EchoAudioPipe to
  Box<dyn AudioPipe + Send> — the seam test (§8.5 #6):
  loop_driver's call sites (sink.on_pcm_frame / source.next_pcm_frame)
  are byte-identical; only the field type widens. RtcSession::new keeps
  EchoAudioPipe default (slice-1 tests unchanged); binary uses
  RtcSession::set_pipe(tap_audio_pipe) to swap on the Connected transition.
- 4 unit tests for tap_url validation (loopback accepted, non-loopback
  rejected, wss rejected fail-fast, default fallback).

Spec ref: 2026-06-28-slice-2-agent-tap-design.md §4.3, §4.4, §5.1, §7.
2026-06-28 17:39:52 -04:00
opencode controller
e0616fe42d feat(call-model): +tap field on Channel + TapHandle marker (spec §5.2, §6)
- pub struct TapHandle(()) — zero-sized marker (Option<TapHandle> compiles
  to a bool, no allocation). The live tap connection lives in the binary's
  DashMap<ChannelId, TapConn>, keyed by the channel's existing ChannelId
  (== wire session_id per spec §5.3).
- Channel grows pub tap: Option<TapHandle>. None until Connected, set on
  Connected, cleared before state advances to Closed.
- State invariant: tap and ChannelState are tied (Connected+None=bug).
- Slice-1 backwards compatible: new field is Option<...>;

Multi-tap-per-channel is the future trigger for a TapId newtype (YAGNI).

Spec ref: 2026-06-28-slice-2-agent-tap-design.md §5.2, §6.
2026-06-28 17:24:11 -04:00
opencode controller
237a1388a4 feat(tap-echo): Rust reference echo brain + test server (spec §2.3)
- start_echo_server(addr): in-process WS server for integration tests;
  returns EchoHandle { shutdown, join, addr }.
- echo_one_connection: the per-connection echo loop — hello handshake,
  audio_in → audio_out (same PCM), bye/session_end graceful close.
- Reuses rutster-tap's protocol types — the wire-types-reusable contract
  test (spec §2.3).
- Stateless across reconnects (spec §5.3) — every hello starts fresh.
- Standalone binary: binds ws://127.0.0.1:8081, runs forever.
- 1 unit test exercises full hello/ack/audio_in/audio_out/bye over a
  TCP loopback pair.

Spec ref: 2026-06-28-slice-2-agent-tap-design.md §2.3, §5.3.
2026-06-28 17:20:00 -04:00
opencode controller
1bb5b7203c feat(tap): TapClient WSS pump loop (spec §4.2)
- run_tap_client: drives a connected WebSocketStream with a tokio::select!
  over rx_pcm_in (inbound PCM → audio_in WS frame) and ws.next() (brain
  frame → audio_out mpsc or control handling).
- Handshake: send hello, await brain hello (bounded 2s timeout).
- seq gap detection (log + count, never drop on gap — spec §3.1).
- Hot-path errors (encode/decode failures, send/recv failures) are logged
  + counted; TapClientError is returned only on graceful close, hello
  timeout, or WS errors. The TapEngine (Task 7) decides reconnect policy.
- Pure-helper unit test (elapsed_ms); full pump behavior is exercised
  against the in-process EchoServer in the Task 8 integration test.

Spec ref: 2026-06-28-slice-2-agent-tap-design.md §4.2.
2026-06-28 14:25:15 -04:00
opencode controller
51d5e6b01e feat(tap): TapAudioPipe + TapMetrics (spec §4.1)
- TapMetrics: atomic counters (drop + observe per slice-1 §3.8).
- TapAudioPipe: AudioSource + AudioSink impl over mpsc + VecDeque ring.
  Drop-oldest on overflow (lowest-latency-correct); silence on underflow.
- TAP_PLAYOUT_FRAMES = 5 (100ms @ 20ms/frame; tunable constant per spec §4.1).
- 6 unit tests cover underflow, overflow drops-oldest, sink-full drops,
  disconnected engine, frame round-trip, empty ring.

Spec ref: 2026-06-28-slice-2-agent-tap-design.md §4.1.
2026-06-28 14:18:57 -04:00
opencode controller
5b931d447b feat(tap): versioned JSON event wire protocol (spec §3)
- protocol.rs: Envelope + FrameKind + Payload types, serde-derived.
- Explicit little-endian PCM codec (to_le_bytes / from_le_bytes) —
  spec §3, §9. No host-endian silent hazard.
- encode_* helpers for hello, audio_in, audio_out, session_end, bye, error.
- decode_envelope validates protocol version + samples count; unknown
  type values decode to DecodedPayload::Unknown (log + count + drop
  per spec §3.4).
- 10 unit tests cover round-trips, LE byte order, sample-count
  mismatch, unknown-type drop, and version mismatch.

Spec ref: 2026-06-28-slice-2-agent-tap-design.md §3.
2026-06-28 14:12:48 -04:00
opencode controller
0cccf77faf build(slice-2): workspace deps + rutster-tap-echo skeleton
- Add tokio-tungstenite 0.24, futures-util 0.3, url 2, base64 0.22 to
  [workspace.dependencies] (spec §8.1).
- Add crates/rutster-tap-echo as 7th workspace member (spec §2).
- Transition crates/rutster-tap/Cargo.toml from stub to real manifest
  with the deps Tasks 2-4 will need (rutster-media, tokio, tokio-tungstenite,
  serde, serde_json, base64, url, thiserror, tracing).
- Skeleton lib.rs (compile test) + main.rs (placeholder fn main) for
  rutster-tap-echo; Task 5 fills in the echo logic.
- Verify cargo deny check passes against the new transitive dep tree.

Spec ref: docs/superpowers/specs/2026-06-28-slice-2-agent-tap-design.md §2, §8.1.
2026-06-28 14:01:12 -04:00
adlee-was-taken
3041036d94 fix: restore doc bar on RtcSession accessors; refresh stale README status
Some checks failed
CI / fmt (pull_request) Failing after 42s
CI / clippy (pull_request) Failing after 1m19s
CI / test (1.85) (pull_request) Failing after 49s
CI / test (stable) (pull_request) Failing after 29s
CI / deny (pull_request) Failing after 46s
Two Important findings from the final whole-branch review:

T4-M3: RtcSession::channel_id/channel_state/is_closed lacked /// docs
(AGENTS.md mandates /// on every public item; learner-facing code is a
primary product). One-line docs added per existing /// style in rtc_session.rs.

T7-M2: README "Status" section still read "Scoping. No code yet" — stale
post-slice-1. Updated to reflect slice-1 implementation status; minimal
edit, surrounding content preserved.
2026-06-28 13:13:27 -04:00
adlee-was-taken
1f0f64c1c6 binary: axum signaling + DashMap session store + browser test client
Four routes on axum 0.7 per spec §4.1: POST /v1/sessions (mint),
POST /v1/sessions/:id/offer (str0m-native SDP accept), DELETE
/v1/sessions/:id (close), GET / (static HTML client). Session store is
a DashMap<ChannelId, Arc<Mutex<RtcSession>>> (spec §4.5); one tokio
task drives all session poll loops — per-session tasks would pre-pave
the wrong pattern for step 4's dedicated thread. Graceful shutdown
drops the DashMap on Ctrl-C / SIGTERM. Integration test exercises the
REST surface; manual browser e2e per README §6.5.
2026-06-28 12:48:02 -04:00
adlee-was-taken
34c590b48a media: RtcSession + str0m poll loop (the media core)
RtcSession owns a str0m::Rtc + Opus decoder/encoder + EchoAudioPipe +
a bound UDP socket (spec §3.1, §4.5). accept_offer calls str0m 0.21's
sdp_api().accept_offer() natively — no hand-rolled SDP munger; str0m
fills DTLS fingerprint + ICE creds + Opus codec. The loop driver
drains poll_output per str0m's single-mutation invariant, routes
inbound MediaData through Opus decode + EchoAudioPipe sink, sends
Transmit packets on the UDP socket, and checks the 60 s idle timeout.

DEV-DEVIATION: loop runs on tokio (spec §3.4); step 4 replaces with
a dedicated timing thread per ARCHITECTURE.md.

str0m 0.21 API adjustments from the brief's sketch (verified against
src/str0m-0.21.0):
- SdpAnswer has no .mid() accessor; use answer.media_lines[0].mid()
  via the Deref<Target=Sdp> impl.
- Input::Receive carries the Instant as the first tuple element
  (Input::Receive(now, recv)); handle_input takes a single Input arg.
- Receive constructed via Receive::new(proto, src, dst, buf) (the
  DatagramRecv field is private).
- UDP socket binds 127.0.0.1:0 (Candidate::host rejects the
  unspecified address 0.0.0.0).
- BROWSER_SDP_OFFER fixture restored a=group:BUNDLE 0 (str0m's
  Sdp::assert_consistency rejects SDP without a session group).
2026-06-28 12:39:57 -04:00
adlee-was-taken
e09af55e00 media: PcmFrame + AudioSource/Sink + Opus codec pair
PcmFrame is the canonical tap format (16-bit mono @ 24 kHz, 480 samples
per 20 ms frame — ARCHITECTURE.md). AudioSource/AudioSink are the seam
step 2 splices the tap client into (spec §3.3); EchoAudioPipe is the
slice-1 wiring of that seam. OpusDecoder/OpusEncoder wrap the opus
crate's libopus FFI with hot-path match-and-continue (no ? on the 20 ms
loop, spec §3.8); decode/encode return Option<PcmFrame>/Option<Vec<u8>>
so a dropped frame is logged + counted, never propagated to crash the
peer.
2026-06-28 12:22:29 -04:00
adlee-was-taken
a5d0f90fe2 call-model: Channel + ChannelId + ChannelState (signaling embryo)
rutster-call-model is real-but-minimal (spec §5): the unifying leg
object the future API exposes. ChannelId is a Uuid newtype for
type-safety (the slice-1 worked example of the newtype pattern).
Channel is signaling-state only — media lives in rutster-media as a
leaf concern of the Channel, surfaced only when a second consumer needs
to observe it (spec §5.3). ChannelState matches the New→Connecting→
Connected→Closing→Closed flow from §5.4.
2026-06-28 11:27:55 -04:00
adlee-was-taken
39245a6553 workspace: scaffold + three stub crates (sip/tap/spend)
Workspace root, pinned toolchain, and the three stub crates whose only
job in slice 1 is to lock the ADR-0002 boundary shape. Each ships a
lib.rs module doc (what it will hold, why deferred, which spearhead step
fills it) and a crate_compiles test. Spec §2.2.
2026-06-28 11:22:52 -04:00