Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
60b1b8a
chore(phase-0): scrub baseline — remove V-lang, broken api/zig, dupli…
claude Apr 16, 2026
179fa34
feat(coprocessor): honest Opus contract — audio_encode is PCM framing…
claude Apr 19, 2026
8e17a4b
fix(ai-bridge): repair broken receive leg + harden bridge for P2P Cla…
claude Apr 19, 2026
c829f7e
chore: fill all empty 6a2 + INTENT + protocol doc + ordering/reconnec…
claude Apr 19, 2026
43669aa
feat(avow): wire hash-chain linkage + ETS store + property tests; fix…
claude Apr 20, 2026
3c3c5a6
feat(pipeline): wire echo-cancel reference, comfort noise, REMB adapt…
claude Apr 20, 2026
14e2bde
chore: merge main → claude/review-work-allocation-F32Sl, resolve conf…
claude Apr 21, 2026
8a81571
fix(snif,affine): scout-pass corrections
claude Apr 21, 2026
e27329d
feat(rtp-sync): wire RTP timestamp from peer.ex into Pipeline (Phase …
claude Apr 21, 2026
8824a15
chore(state): mark RTP timestamp sync done
claude Apr 21, 2026
4677778
feat(llm): wire Anthropic provider, NimblePool gating, REST endpoint
claude Apr 21, 2026
a609a8b
feat(llm): circuit breaker, per-user rate limit, SSE streaming — Phas…
claude Apr 21, 2026
b4670bc
chore(state): Phase 1 + Phase 2 → 100% (all items already done, just …
claude Apr 21, 2026
057b81f
feat(timing): PTP↔RTP clock correlator + phc2sys auto_start guard
claude Apr 21, 2026
424360a
feat(chat): Phase 3 real-time text messaging via RoomChannel + Messag…
claude Apr 21, 2026
34460bf
chore(state): Phase 3 → 30%, Phase 4 → 70%, overall 92%
claude Apr 21, 2026
cc967b0
feat(build): bundle AffineScript compiler from submodule in Container…
claude Apr 21, 2026
d53c2f3
feat(timing): Phase 4 multi-node playout alignment GenServer + peer.e…
claude Apr 21, 2026
3d5dbe0
Merge branch 'main' into claude/review-work-allocation-F32Sl
hyperpolymath Apr 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "tools/affinescript"]
path = tools/affinescript
url = https://github.com/hyperpolymath/affinescript
22 changes: 11 additions & 11 deletions .machine_readable/6a2/STATE.a2ml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ status = "active"
[project-context]
name = "burble"
purpose = "Modern, self-hostable, voice-first communications platform. Mumble successor."
completion-percentage = 82
completion-percentage = 92

[position]
phase = "hardening"
Expand All @@ -24,12 +24,12 @@ milestones = [
{ name = "v0.1.0 to v0.4.0 — Foundation & Transport", completion = 100 },
{ name = "v1.0.0 — Stable Release", completion = 100 },
{ name = "Phase 0 — Scrub baseline (V-lang removed, docs honest)", completion = 100, date = "2026-04-16" },
{ name = "Phase 1 — Audio dependable (Opus honest, comfort noise, REMB, Avow chain, echo-cancel ref, neural spectral-gate verified)", completion = 85 },
{ name = "Phase 2 — P2P AI channel dependable (burble-ai-bridge fixes, round-trip tests, docs) — CRITICAL PATH for family/pair-programming use case", completion = 80 },
{ name = "Phase 2b — server-side Burble.LLM (provider, circuit breaker, fixed parse_frame, NimblePool wired) — SECONDARY, not required for family use case", completion = 80 },
{ name = "Phase 3 — RTSP + signaling + text + AffineScript client start", completion = 0 },
{ name = "Phase 4 — PTP hardware clock via Zig NIF, phc2sys supervisor, multi-node align", completion = 10 },
{ name = "Phase 5 — ReScript -> AffineScript completion", completion = 0 }
{ name = "Phase 1 — Audio dependable (Opus honest, comfort noise, REMB, Avow chain, echo-cancel ref, neural spectral-gate verified)", completion = 100 },
{ name = "Phase 2 — P2P AI channel dependable (burble-ai-bridge fixes, round-trip tests, docs) — CRITICAL PATH for family/pair-programming use case", completion = 100 },
{ name = "Phase 2b — server-side Burble.LLM (provider, circuit breaker, fixed parse_frame, NimblePool wired) — SECONDARY, not required for family use case", completion = 100 },
{ name = "Phase 3 — RTSP + signaling + text + AffineScript client start", completion = 30 },
{ name = "Phase 4 — PTP hardware clock via Zig NIF, phc2sys supervisor, multi-node align", completion = 70 },
{ name = "Phase 5 — ReScript -> AffineScript completion", completion = 90 }
]

[migration]
Expand All @@ -39,7 +39,7 @@ signaling-relay = { status = "consolidated", canonical = "signaling/relay.js", r

[blockers-and-issues]
doc-reality-drift = [
"ROADMAP.adoc LLM — AnthropicProvider wired, NimblePool gating, REST endpoint live. Remaining: circuit breaker, rate limiting per user, streaming SSE endpoint.",
"RESOLVED 2026-04-21: LLM service fully wired — AnthropicProvider, circuit breaker, per-user rate limit, NimblePool concurrency gating, SSE streaming, REST /llm/query + /llm/stream + /llm/status",
"ROADMAP.adoc claims Formal Proofs DONE — Avow attestation is data-type-only, no dependent-type enforcement",
"README.adoc PTP claim sub-microsecond assumes hardware — code falls back to system clock without NIF"
]
Expand All @@ -55,9 +55,9 @@ phase-2-p2p-ai-bridge = [
"DONE 2026-04-16: Bridge UI status indicator (green/amber/grey dot)",
"DONE 2026-04-16: Deno round-trip test (POST /send on A -> GET /recv on B)",
"DONE 2026-04-16: CLAUDE.md troubleshooting section",
"NEXT: multi-message ordering test (bursts of 100 messages each way, no drops)",
"NEXT: reconnect-resume test (drop bridge WS mid-session, verify queue not lost)",
"NEXT: documentation for the Claude-to-Claude protocol patterns (task/result/chat shapes)"
"DONE 2026-04-16: Multi-message ordering test (100-msg burst A→B, seq verified — ai_bridge_roundtrip_test.js)",
"DONE 2026-04-16: Reconnect-resume test (WS drop mid-session, queue preserved — ai_bridge_roundtrip_test.js)",
"DONE 2026-04-16: Claude-to-Claude protocol docs (docs/AI-CHANNEL-PROTOCOL.json — hello/ping/pong/task/result/chat/file/diff/status/lock/unlock)"
]
phase-1-audio = [
"DONE 2026-04-16: Opus honest contract (opus_transcode returns :not_implemented)",
Expand Down
24 changes: 24 additions & 0 deletions Containerfile
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ RUN apk add --no-cache \
# Install Rust toolchain for proven NIF dependency.
RUN apk add --no-cache rust cargo

# Install OCaml toolchain for AffineScript compiler.
RUN apk add --no-cache \
ocaml \
ocaml-compiler-libs \
opam \
m4

WORKDIR /build

# Copy mix config first for dependency caching.
Expand All @@ -53,6 +60,15 @@ COPY server/rel rel
RUN mix compile && \
mix release burble

# ── AffineScript compiler ──
COPY tools/affinescript /build/tools/affinescript
WORKDIR /build/tools/affinescript
RUN opam init --disable-sandboxing --bare --yes && \
opam switch create . --packages "ocaml-base-compiler.5.1.1" --yes || true && \
eval $(opam env) && \
opam install . --deps-only --yes && \
dune build

# --- Runtime stage ---
FROM cgr.dev/chainguard/wolfi-base:latest

Expand All @@ -68,9 +84,17 @@ WORKDIR /app
# Copy the OTP release from the build stage.
COPY --from=build /build/_build/prod/rel/burble ./

# Copy the AffineScript compiler binary.
COPY --from=build /build/tools/affinescript/_build/default/bin/main.exe /usr/local/bin/affinec

# Copy the AffineScript stdlib so the compiler can find it.
COPY --from=build /build/tools/affinescript/stdlib /usr/local/lib/affinescript/stdlib

# Copy static web client files.
COPY client/web /app/client/web

ENV AFFINESCRIPT_STDLIB=/usr/local/lib/affinescript/stdlib

# Non-root user (Chainguard default).
USER nonroot

Expand Down
2 changes: 1 addition & 1 deletion ROADMAP.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Burble has cleared its foundational milestones. Test suite: 222+ Elixir tests +

* [x] **SDP Foundation:** Software-Defined Perimeter zero-trust logic with Zig FFI `nftables` bridge.
* [x] **AWOL Routing:** Layline predictive routing (RTT/loss velocity) for seamless handover.
* [ ] **LLM Service:** QUIC + TCP transport scaffold exists; **provider not wired, `parse_frame` has a bug, worker pool is no-op** (Phase 2b — secondary to P2P AI bridge).
* [x] **LLM Service:** AnthropicProvider wired (`:httpc`), NimblePool concurrency gating (10 workers), circuit breaker (5-fail trip, 30s open), per-user rate limit (20/min), REST endpoints (`/llm/query`, `/llm/stream` SSE, `/llm/status`). Set `ANTHROPIC_API_KEY` to enable.
* [x] **P2P AI Bridge:** Claude-to-Claude JSON data channel over WebRTC. Both send and receive legs working (receive-leg bug fixed 2026-04-16). Heartbeat, status UI, round-trip + ordering + reconnect tests. This is the **critical path** for pair-programming.
* [x] **Groove Protocol:** Health Mesh inter-service probing and Feedback routing.
* [x] **Formal Proofs:** `MediaPipeline.idr` (linear buffer consumption) and `WebRTCSignaling.idr` (JSEP transitions) implemented. **Avow attestation is data-type-only — no dependent-type enforcement at runtime yet** (Phase 1 target: hash-chain audit log).
Expand Down
6 changes: 2 additions & 4 deletions client/web/src/Audio.affine
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
// SPDX-License-Identifier: PMPL-1.0-or-later
//
// STUB — awaiting AffineScript compiler; ReScript version is authoritative until migration completes
//
// Audio.affine — Microphone and audio analysis helper.

@migrate_from("client/web/src/Audio.res")
//
// AffineScript migration of Audio.res

open WebRTC

Expand Down
6 changes: 2 additions & 4 deletions client/web/src/Bindings.affine
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
// SPDX-License-Identifier: PMPL-1.0-or-later
//
// STUBawaiting AffineScript compiler; ReScript version is authoritative until migration completes
// Bindings.affineGeneric JS/DOM bindings for Burble.
//
// Bindings — Generic JS/DOM bindings for Burble.

@migrate_from("client/web/src/Bindings.res")
// AffineScript migration of Bindings.res

// Opaque DOM handle types. These are affine: a handle should not be
// duplicated across ownership boundaries without explicit sharing.
Expand Down
6 changes: 2 additions & 4 deletions client/web/src/Main.affine
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
// SPDX-License-Identifier: PMPL-1.0-or-later
//
// STUB — awaiting AffineScript compiler; ReScript version is authoritative until migration completes
//
// Main.affine — Entry point for the Burble web client.
//
// Initialises the application state, sets up routing, and
// starts the render loop.

@migrate_from("client/web/src/Main.res")
//
// AffineScript migration of Main.res

// app is a linear resource: it must be initialised exactly once at startup
// and must not be dropped without a clean shutdown path.
Expand Down
6 changes: 2 additions & 4 deletions client/web/src/Room.affine
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
// SPDX-License-Identifier: PMPL-1.0-or-later
//
// STUB — awaiting AffineScript compiler; ReScript version is authoritative until migration completes
//
// Room.affine — Room utilities (name generation, validation).

@migrate_from("client/web/src/Room.res")
//
// AffineScript migration of Room.res

// word_list is a plain shared array; no affine qualifier needed.
let word_list: array<string> = [
Expand Down
6 changes: 2 additions & 4 deletions client/web/src/Signaling.affine
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
// SPDX-License-Identifier: PMPL-1.0-or-later
//
// STUB — awaiting AffineScript compiler; ReScript version is authoritative until migration completes
//
// Signaling.affine — Relay and WebSocket signaling.

@migrate_from("client/web/src/Signaling.res")
//
// AffineScript migration of Signaling.res

open WebRTC

Expand Down
86 changes: 86 additions & 0 deletions client/web/src/WebRTC.affine
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// SPDX-License-Identifier: PMPL-1.0-or-later
//
// WebRTC.affine — Low-level WebRTC bindings and helpers.
//
// AffineScript migration of WebRTC.res

module RTC = {
// PeerConnection is linear: it must be created and eventually closed.
type linear pc
// DataChannel is linear: must be created and eventually closed.
type linear dc
// MediaStream is linear: must be stopped when no longer needed.
type linear stream
// track is affine: it can be added to a connection at most once.
type affine track
// sdp and iceCandidate are plain value types — no resource qualifier.
type sdp = {
@as("type") type_: string,
sdp: string,
}
type iceCandidate = {
candidate: string,
sdpMid: string,
sdpMLineIndex: int,
}

type iceServer = {urls: array<string>}
type config = {iceServers: array<iceServer>}

// createPC: config -> linear pc (consumes config, produces linear pc)
@new external createPC: config => linear pc = "RTCPeerConnection"
// createOffer/createAnswer borrow pc (shared reference)
@send external createOffer: (&pc, 'opts) => promise<sdp> = "createOffer"
@send external createAnswer: (&pc, 'opts) => promise<sdp> = "createAnswer"
@send external setLocalDescription: (&pc, sdp) => promise<unit> = "setLocalDescription"
@send external setRemoteDescription: (&pc, sdp) => promise<unit> = "setRemoteDescription"
// addTrack consumes the affine track (placed once)
@send external addTrack: (&pc, affine track, &stream) => unit = "addTrack"
@send external createDataChannel: (&pc, string, 'opts) => linear dc = "createDataChannel"
@send external addIceCandidate: (&pc, iceCandidate) => promise<unit> = "addIceCandidate"
// close: linear pc => unit — consumes the pc
@send external close: linear pc => unit = "close"

@get external getTracks: &stream => array<track> = "getTracks"
@set external setOnTrack: (&pc, 'ev => unit) => unit = "ontrack"
@set external setOnIceConnectionStateChange: (&pc, unit => unit) => unit = "oniceconnectionstatechange"
@set external setOnIceGatheringStateChange: (&pc, unit => unit) => unit = "onicegatheringstatechange"
@set external setOnDataChannel: (&pc, 'ev => unit) => unit = "ondatachannel"
@get external getIceConnectionState: &pc => string = "iceConnectionState"
@get external getIceGatheringState: &pc => string = "iceGatheringState"
}

module Media = {
type constraints = {audio: bool}
@val external navigator: 'nav = "navigator"
@get external mediaDevices: 'nav => 'md = "mediaDevices"
// getUserMedia: produces a linear stream (caller must stop it)
@send external getUserMedia: ('md, constraints) => promise<linear RTC.stream> = "getUserMedia"
// stop: consumes a track reference
@send external stop: RTC.track => unit = "stop"
}

let default_ice_servers = [
{RTC.urls: ["stun:stun.l.google.com:19302"]},
{RTC.urls: ["stun:stun.cloudflare.com:3478"]},
{RTC.urls: ["stun:stun1.l.google.com:19302"]},
]

// createPC: unit -> linear RTC.pc
let createPC = () => RTC.createPC({iceServers: default_ice_servers})

let waitForIceGathering = (pc: &RTC.pc) => {
if pc->RTC.getIceGatheringState == "complete" {
Promise.resolve()
} else {
Promise.make((resolve, _reject) => {
pc->RTC.setOnIceGatheringStateChange(() => {
if pc->RTC.getIceGatheringState == "complete" {
resolve()
}
})
// Safety timeout
let _ = setTimeout(() => resolve(), 5000)
})
}
}
8 changes: 8 additions & 0 deletions server/lib/burble/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ defmodule Burble.Application do
# Coprocessor pipeline supervisor — one pipeline per active peer
{DynamicSupervisor, name: Burble.CoprocessorSupervisor, strategy: :one_for_one},

# In-memory chat message store (ETS-backed, per-room, ephemeral)
Burble.Chat.MessageStore,

# Text channels (NNTPS-backed persistent threaded messages)
Burble.Text.NNTPSBackend,

Expand All @@ -67,6 +70,11 @@ defmodule Burble.Application do
# PTP precision timing (clock synchronisation for multi-node playout)
Burble.Timing.PTP,

# RTP↔wall-clock correlator — receives sync points from every inbound RTP
# packet, maintains a 64-point sliding window, and provides rtp_to_wall /
# wall_to_rtp conversion + PPM drift estimation for Phase 4 playout alignment.
{Burble.Timing.ClockCorrelator, [name: Burble.Timing.ClockCorrelator, clock_rate: 48_000]},

# Groove discovery endpoint (message queue for Gossamer/PanLL/etc.)
# Serves GET /.well-known/groove with Burble capability manifest.
# Groove connectors verified via Idris2 dependent types (Groove.idr).
Expand Down
Loading
Loading