Files
Sophia/crates/sophia-server/src/ws.rs
dtoro 8688f632bf Implement Sophia MVP scaffold (stages 0–3 + topology pivot)
Six-crate Rust workspace (core/sim/llm/store/server/bin) backing a
Three.js + WebGL frontend. Live at http://127.0.0.1:7777 via `cargo run`.

Sim
- Event-driven scheduler with min-heap, per-engram tick, staggered Spawn
  events (40 ms apart) so each engram's flight is visually readable.
- Solid-donut torus topology (replaces original spherical density-driven
  shell, see Topology Pivot in docs/system-analysis.md). Configurable
  major/minor radii in config.toml; live `POST /api/galaxy/:id/resize`.
- Physics: Verlet integration + friction; in-hole pull + galactic spin
  (CCW around +z) for spiral-ejection ejection from the donut centre;
  soft tube boundary with velocity-reflecting wall.
- Cosine-weighted gravity (kiddo k-NN within radius 25, threshold 0.50)
  and synapse formation (threshold 0.62) gated to inside-the-tube only.
- LM Studio integration via OpenAI-compatible REST: batched embeddings,
  optional Bearer auth, semaphore-bounded parallel ops per §13.5.

Server
- axum HTTP + WebSocket. Routes: /healthz, /api/galaxy CRUD, /seed,
  /ingest, /resize, /engrams/:id, /ws/galaxy/:id/events.
- Binary 12-byte-aligned position frames at ~20 Hz; JSON for sparse
  events (Hello, EngramCreated, SynapseCreated, TorusUpdated).
- Layered config: config.toml (defaults) + config.local.toml (secrets,
  gitignored) merged on startup.

Frontend
- Vite + vanilla TypeScript + three.js 0.169.
- Engrams render as additive bloom-friendly point sprites with a
  per-engram hash-driven hue rotation and breathing pulse.
- Comet-style velocity-aligned trails; additive ribbon synapses whose
  endpoints track engram positions every frame.
- UnrealBloomPass + ACES tone-mapping for the linked-particles look.
- HUD shows torus dims, engram + synapse counts, LM Studio status;
  controls for seed, ingest, and live torus resize.

Docs
- README replaced with docs/IDEA.md; system-analysis.md updated with
  the topology pivot decisions and Galaxy Ejection refinement notes
  (the implementation plan lives in ~/.claude/plans, gitignored).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 08:29:37 +02:00

109 lines
4.0 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! WebSocket bridge from the simulation to a single browser client.
//!
//! Wire protocol:
//! - `text` frames: JSON-serialized [`SimEvent`] (Hello, EngramCreated,
//! BBoxUpdated). One message per frame.
//! - `binary` frames: position frames, encoded as
//! `[tag u32 LE = 0x01][t_ms u32 LE][n u32 LE][n × (x f32 LE, y f32 LE, z f32 LE)]`
//! — 12-byte header followed by the float region. The header is encoded
//! as three little-endian u32s rather than a tighter (u8, u32, u32) so the
//! float region starts at a 4-byte-aligned offset, which lets the browser
//! wrap it as a `Float32Array` view without copying. (`Float32Array`
//! requires its byte offset to be a multiple of 4 and throws otherwise.)
//! One frame at ~20 Hz, in `instance_idx` order.
//!
//! See `docs/system-analysis.md` Risk #1 in §13 plan for why we use binary
//! frames here.
use axum::extract::ws::{Message, WebSocket};
use bytes::{BufMut, BytesMut};
use futures_util::{SinkExt, StreamExt};
use sophia_core::{GalaxyId, PositionFrame, SimEvent};
use sophia_sim::SimHandle;
use tokio::sync::broadcast;
use tracing::{debug, warn};
const POS_FRAME_TAG: u32 = 0x01;
pub async fn run_galaxy_socket(socket: WebSocket, sim: SimHandle, galaxy: GalaxyId) {
let (mut sender, mut receiver) = socket.split();
let (info, snapshots, synapses, mut bus) = match sim.subscribe(galaxy).await {
Ok(t) => t,
Err(e) => {
warn!("ws subscribe failed: {e}");
let _ = sender
.send(Message::Text(
serde_json::json!({ "type": "error", "message": e.to_string() }).to_string(),
))
.await;
return;
}
};
// Hello: tell the client about the galaxy + every existing engram + synapse.
let hello = SimEvent::Hello { galaxy: info, engrams: snapshots, synapses };
if let Err(e) = send_text(&mut sender, &hello).await {
debug!("ws hello send failed: {e}");
return;
}
loop {
tokio::select! {
// Inbound: ignore messages for now (Stage 1 has no client→server).
// Just drain so the socket stays alive and we notice closure.
msg = receiver.next() => {
match msg {
Some(Ok(Message::Close(_))) | None => break,
Some(Err(e)) => { debug!("ws recv err: {e}"); break; }
Some(Ok(_)) => {}
}
}
// Outbound: forward sim events to the client.
ev = bus.recv() => {
match ev {
Ok(SimEvent::PositionFrame(f)) => {
if let Err(e) = sender.send(Message::Binary(encode_position_frame(&f))).await {
debug!("ws send pos frame failed: {e}");
break;
}
}
Ok(other) => {
if let Err(e) = send_text(&mut sender, &other).await {
debug!("ws send text failed: {e}");
break;
}
}
Err(broadcast::error::RecvError::Closed) => break,
Err(broadcast::error::RecvError::Lagged(n)) => {
warn!("ws lagged by {n} events; client will catch up on next frame");
}
}
}
}
}
}
async fn send_text(
sender: &mut futures_util::stream::SplitSink<WebSocket, Message>,
ev: &SimEvent,
) -> anyhow::Result<()> {
let body = serde_json::to_string(ev)?;
sender.send(Message::Text(body)).await?;
Ok(())
}
fn encode_position_frame(f: &PositionFrame) -> Vec<u8> {
let n = f.positions.len();
let mut buf = BytesMut::with_capacity(12 + n * 12);
buf.put_u32_le(POS_FRAME_TAG);
buf.put_u32_le(f.t_ms);
buf.put_u32_le(n as u32);
for [x, y, z] in &f.positions {
buf.put_f32_le(*x);
buf.put_f32_le(*y);
buf.put_f32_le(*z);
}
buf.to_vec()
}