@@ -12,8 +12,10 @@ use tracing::warn;
use sophia_core ::{
Engram , EngramDetail , EngramId , EngramSnapshot , EngramState , GalaxyId , GalaxyInfo , GalaxyShape ,
Manifest , PositionFrame , SimEven t, Slate , SynapseDto , Vec3 ,
GalaxySnapshot , Introspection , Manifes t, Memory , MemoryKind , PositionFrame , QueryId ,
QueryStatus , SimEvent , Slate , SynapseDto , Vec3 , MAX_MEMORIES ,
} ;
use std ::collections ::{ HashMap , HashSet , VecDeque } ;
/// One item to ingest into the sim. The server pre-computes the slate via the
/// LM Studio embedding endpoint and hands the sim a fully-formed payload.
@@ -70,6 +72,23 @@ const GRAVITY_MAX_ACC: f32 = 30.0;
/// than the gravity threshold so weak co-residence doesn't link everyone.
const SYNAPSE_THRESHOLD : f32 = 0.62 ;
// ---- Stage 5: query broadcast tuning ----
/// Minimum cosine similarity for an engram to qualify as a Phase A responder.
/// Tuned for `nomic-embed-text-v1.5`: directly relevant text typically scores
/// 0.55+, weakly related 0.35– 0.50, unrelated 0.30 or below.
const QUERY_PHASE_A_THRESHOLD : f32 = 0.45 ;
/// Synapse weight cutoff for Phase B 1-hop expansion. A synapse weaker than
/// this isn't strong enough evidence to drag its endpoint into the conversation.
const QUERY_PHASE_B_SYNAPSE_THRESHOLD : f32 = 0.55 ;
/// Fraction of `max_responders` reserved for Phase A (spatial). Phase B fills
/// whatever's left so synaptic expansion always gets at least *some* slots.
const QUERY_PHASE_A_FRAC : f32 = 0.75 ;
/// Per-responder duration of the "lit up" Conversing visual on the WS bus.
/// The orchestrator does NOT explicitly turn the responder back to Idle —
/// instead it schedules a deferred event after this delay, keeping the sim
/// authoritative for the transition (no client-side timers required).
const RESPONDER_LIGHTUP_MS : u64 = 4_000 ;
#[ derive(Debug, Error) ]
pub enum SimError {
#[ error( " simulation has shut down " ) ]
@@ -104,6 +123,12 @@ enum SimCmd {
engram : EngramId ,
reply : oneshot ::Sender < Result < Option < EngramDetail > , SimError > > ,
} ,
UpdateIntrospection {
galaxy : GalaxyId ,
engram : EngramId ,
introspection : Introspection ,
reply : oneshot ::Sender < Result < ( ) , SimError > > ,
} ,
Subscribe {
galaxy : GalaxyId ,
reply : oneshot ::Sender < Result < SubscribeReply , SimError > > ,
@@ -113,6 +138,107 @@ enum SimCmd {
shape : GalaxyShape ,
reply : oneshot ::Sender < Result < GalaxyInfo , SimError > > ,
} ,
/// Stage 5: materialize a pinned Query-Engram at the galaxy center.
/// Returns the new engram's id so the orchestrator can refer to it.
StartQueryEngram {
galaxy : GalaxyId ,
query : QueryId ,
text : String ,
slate : Slate ,
reply : oneshot ::Sender < Result < EngramId , SimError > > ,
} ,
/// Stage 5: run Phase A (cosine scan) + Phase B (1-hop synaptic) and
/// return the chosen responders sorted by score (high→low). Capped at
/// `max_responders`.
BroadcastQuery {
galaxy : GalaxyId ,
query_engram : EngramId ,
max_responders : usize ,
reply : oneshot ::Sender < Result < Vec < ( EngramId , f32 ) > , SimError > > ,
} ,
/// Stage 5: fetch the source text for a responder so the orchestrator
/// can build the per-responder LLM prompt. Returns `None` for synthetic
/// engrams (no manifest).
GetEngramText {
galaxy : GalaxyId ,
engram : EngramId ,
reply : oneshot ::Sender < Result < Option < String > , SimError > > ,
} ,
/// Stage 5: a responder produced a snippet for a query. Append a memory
/// on the responder, briefly flip its state to `Conversing` (auto-reverts
/// after `RESPONDER_LIGHTUP_MS`).
RecordQueryParticipation {
galaxy : GalaxyId ,
query : QueryId ,
engram : EngramId ,
snippet : String ,
reply : oneshot ::Sender < Result < ( ) , SimError > > ,
} ,
/// Stage 5: publish a new running answer over the WS bus. The orchestrator
/// also pushes to its per-query SSE channel separately — this event is
/// only for clients that want to subscribe over the global galaxy bus.
PublishQueryAnswer {
galaxy : GalaxyId ,
query : QueryId ,
version : u32 ,
status : QueryStatus ,
answer : String ,
reply : oneshot ::Sender < Result < ( ) , SimError > > ,
} ,
/// Stage 5: query is done. Transition the Query-Engram to `Memorize`,
/// store the final answer in its memory log, emit `QueryFinished`.
FinishQuery {
galaxy : GalaxyId ,
query : QueryId ,
query_engram : EngramId ,
final_answer : String ,
responder_count : u32 ,
reply : oneshot ::Sender < Result < ( ) , SimError > > ,
} ,
/// Stage 6: snapshot every galaxy to a serializable form. Used by the
/// periodic snapshot task in the bin to write to the sled store. Pinned
/// (query) engrams are excluded — they're ephemeral.
SnapshotAll {
reply : oneshot ::Sender < Vec < GalaxySnapshot > > ,
} ,
/// Stage 6: re-create a galaxy in the world from a stored snapshot.
/// Re-uses every engram's stored `instance_idx` so the dense slot table
/// is identical to pre-shutdown. Re-schedules `BroadcastFrame`,
/// `RebuildIndex`, and one `EngramTick` per (non-pinned) engram so motion
/// resumes immediately. Returns `InvalidShape` if the snapshot's shape
/// fails validation.
HydrateGalaxy {
snapshot : GalaxySnapshot ,
reply : oneshot ::Sender < Result < GalaxyInfo , SimError > > ,
} ,
/// Stage 6: remove a galaxy from the world. Caller is responsible for
/// also removing its on-disk snapshot (see `Store::delete_galaxy`).
DeleteGalaxy {
galaxy : GalaxyId ,
reply : oneshot ::Sender < Result < ( ) , SimError > > ,
} ,
/// Stage 7: cheap stats snapshot for the HUD. Returns total events
/// processed per galaxy in the last second + total engram count.
/// Polled at 1 Hz from the browser.
GetStats {
reply : oneshot ::Sender < SimStats > ,
} ,
}
/// Lightweight stats payload returned by `SimHandle::stats`. Used by the
/// HUD's "events/sec" + "engrams" counters. Numbers are best-effort
/// snapshots, not exact (the sim counts as it processes; the read happens
/// asynchronously).
#[ derive(Debug, Clone, Default) ]
pub struct SimStats {
/// Sum of ticks across all galaxies in the last 1-second window.
pub ticks_per_sec : u32 ,
/// Sum of all non-pinned engrams across galaxies.
pub engrams_total : u32 ,
/// Number of live galaxies.
pub galaxies : u32 ,
/// Sum of all synapses across galaxies.
pub synapses_total : u32 ,
}
#[ derive(Clone) ]
@@ -165,6 +291,22 @@ impl SimHandle {
rx . await . map_err ( | _ | SimError ::Shutdown ) ?
}
/// Apply async-LLM-generated introspection to an existing engram. Called
/// from the server after a background introspection task completes.
pub async fn update_introspection (
& self ,
galaxy : GalaxyId ,
engram : EngramId ,
introspection : Introspection ,
) -> Result < ( ) , SimError > {
let ( reply , rx ) = oneshot ::channel ( ) ;
self . tx
. send ( SimCmd ::UpdateIntrospection { galaxy , engram , introspection , reply } )
. await
. map_err ( | _ | SimError ::Shutdown ) ? ;
rx . await . map_err ( | _ | SimError ::Shutdown ) ?
}
/// Subscribe to a galaxy's event stream. Returns initial state plus the
/// live receiver so the WS client can replay then follow.
pub async fn subscribe (
@@ -190,6 +332,151 @@ impl SimHandle {
. map_err ( | _ | SimError ::Shutdown ) ? ;
rx . await . map_err ( | _ | SimError ::Shutdown ) ?
}
/// Stage 5: materialize a Query-Engram at the galaxy center.
pub async fn start_query_engram (
& self ,
galaxy : GalaxyId ,
query : QueryId ,
text : String ,
slate : Slate ,
) -> Result < EngramId , SimError > {
let ( reply , rx ) = oneshot ::channel ( ) ;
self . tx
. send ( SimCmd ::StartQueryEngram { galaxy , query , text , slate , reply } )
. await
. map_err ( | _ | SimError ::Shutdown ) ? ;
rx . await . map_err ( | _ | SimError ::Shutdown ) ?
}
/// Stage 5: select up to `max_responders` engrams whose slates resonate
/// with the query, optionally extending via 1-hop synaptic neighbours.
pub async fn broadcast_query (
& self ,
galaxy : GalaxyId ,
query_engram : EngramId ,
max_responders : usize ,
) -> Result < Vec < ( EngramId , f32 ) > , SimError > {
let ( reply , rx ) = oneshot ::channel ( ) ;
self . tx
. send ( SimCmd ::BroadcastQuery { galaxy , query_engram , max_responders , reply } )
. await
. map_err ( | _ | SimError ::Shutdown ) ? ;
rx . await . map_err ( | _ | SimError ::Shutdown ) ?
}
/// Stage 5: fetch the source text of an engram (for building the
/// per-responder LLM prompt). Returns `None` for synthetic engrams.
pub async fn get_engram_text (
& self ,
galaxy : GalaxyId ,
engram : EngramId ,
) -> Result < Option < String > , SimError > {
let ( reply , rx ) = oneshot ::channel ( ) ;
self . tx
. send ( SimCmd ::GetEngramText { galaxy , engram , reply } )
. await
. map_err ( | _ | SimError ::Shutdown ) ? ;
rx . await . map_err ( | _ | SimError ::Shutdown ) ?
}
/// Stage 5: record a responder's contribution + light it up.
pub async fn record_query_participation (
& self ,
galaxy : GalaxyId ,
query : QueryId ,
engram : EngramId ,
snippet : String ,
) -> Result < ( ) , SimError > {
let ( reply , rx ) = oneshot ::channel ( ) ;
self . tx
. send ( SimCmd ::RecordQueryParticipation { galaxy , query , engram , snippet , reply } )
. await
. map_err ( | _ | SimError ::Shutdown ) ? ;
rx . await . map_err ( | _ | SimError ::Shutdown ) ?
}
/// Stage 5: publish an updated running answer to WS subscribers.
pub async fn publish_query_answer (
& self ,
galaxy : GalaxyId ,
query : QueryId ,
version : u32 ,
status : QueryStatus ,
answer : String ,
) -> Result < ( ) , SimError > {
let ( reply , rx ) = oneshot ::channel ( ) ;
self . tx
. send ( SimCmd ::PublishQueryAnswer { galaxy , query , version , status , answer , reply } )
. await
. map_err ( | _ | SimError ::Shutdown ) ? ;
rx . await . map_err ( | _ | SimError ::Shutdown ) ?
}
/// Stage 5: terminal step — store the final answer in the Query-Engram's
/// memory log, transition to Memorize, emit QueryFinished.
pub async fn finish_query (
& self ,
galaxy : GalaxyId ,
query : QueryId ,
query_engram : EngramId ,
final_answer : String ,
responder_count : u32 ,
) -> Result < ( ) , SimError > {
let ( reply , rx ) = oneshot ::channel ( ) ;
self . tx
. send ( SimCmd ::FinishQuery {
galaxy ,
query ,
query_engram ,
final_answer ,
responder_count ,
reply ,
} )
. await
. map_err ( | _ | SimError ::Shutdown ) ? ;
rx . await . map_err ( | _ | SimError ::Shutdown ) ?
}
/// Stage 6: snapshot every galaxy. Used by the periodic snapshot task.
pub async fn snapshot_all ( & self ) -> Result < Vec < GalaxySnapshot > , SimError > {
let ( reply , rx ) = oneshot ::channel ( ) ;
self . tx
. send ( SimCmd ::SnapshotAll { reply } )
. await
. map_err ( | _ | SimError ::Shutdown ) ? ;
rx . await . map_err ( | _ | SimError ::Shutdown )
}
/// Stage 6: re-create a galaxy from a stored snapshot.
pub async fn hydrate_galaxy ( & self , snapshot : GalaxySnapshot ) -> Result < GalaxyInfo , SimError > {
let ( reply , rx ) = oneshot ::channel ( ) ;
self . tx
. send ( SimCmd ::HydrateGalaxy { snapshot , reply } )
. await
. map_err ( | _ | SimError ::Shutdown ) ? ;
rx . await . map_err ( | _ | SimError ::Shutdown ) ?
}
/// Stage 6: drop a galaxy from the world.
pub async fn delete_galaxy ( & self , galaxy : GalaxyId ) -> Result < ( ) , SimError > {
let ( reply , rx ) = oneshot ::channel ( ) ;
self . tx
. send ( SimCmd ::DeleteGalaxy { galaxy , reply } )
. await
. map_err ( | _ | SimError ::Shutdown ) ? ;
rx . await . map_err ( | _ | SimError ::Shutdown ) ?
}
/// Stage 7: snapshot of recent activity for the HUD. Cheap — never
/// blocks the sim; falls back to defaults if the sim is shutting down.
pub async fn stats ( & self ) -> SimStats {
let ( reply , rx ) = oneshot ::channel ( ) ;
if self . tx . send ( SimCmd ::GetStats { reply } ) . await . is_err ( ) {
return SimStats ::default ( ) ;
}
rx . await . unwrap_or_default ( )
}
}
/// Spawn the simulation task. `default_shape` is used for any new galaxy
@@ -203,10 +490,22 @@ pub fn spawn_sim(default_shape: GalaxyShape) -> SimHandle {
let mut indexes : std ::collections ::HashMap < GalaxyId , KiddoIndex > = Default ::default ( ) ;
let mut rng = SmallRng ::seed_from_u64 ( 0xC0DE_5071 ) ;
let started = Instant ::now ( ) ;
// Stage 7 stats: rolling 1-second tick counter. We bump
// `tick_window_count` for every event we drain, and roll it into
// `ticks_per_sec` once a second has elapsed since `tick_window_start`.
let mut tick_window_start = Instant ::now ( ) ;
let mut tick_window_count : u32 = 0 ;
let mut ticks_per_sec : u32 = 0 ;
loop {
// Pick whichever happens first: a new command or the next due event.
let now = Instant ::now ( ) ;
// Roll the tick window if a second has elapsed.
if now . duration_since ( tick_window_start ) > = Duration ::from_secs ( 1 ) {
ticks_per_sec = tick_window_count ;
tick_window_count = 0 ;
tick_window_start = now ;
}
let next_at = scheduler . next_at ( ) ;
let timeout = match next_at {
Some ( at ) = > at . saturating_duration_since ( now ) ,
@@ -215,13 +514,17 @@ pub fn spawn_sim(default_shape: GalaxyShape) -> SimHandle {
tokio ::select! {
cmd = rx . recv ( ) = > {
let Some ( cmd ) = cmd else { break } ; // all handles dropped
handle_cmd ( cmd , & mut world , & mut scheduler , & mut indexes , & mut rng , default_shape ) ;
handle_cmd (
cmd , & mut world , & mut scheduler , & mut indexes ,
& mut rng , default_shape , started , ticks_per_sec ,
) ;
}
_ = tokio ::time ::sleep ( timeout ) = > {
// Drain all due events.
let now = Instant ::now ( ) ;
while let Some ( event ) = scheduler . pop_due ( now ) {
handle_event ( event , & mut world , & mut scheduler , & mut indexes , & mut rng , started ) ;
tick_window_count = tick_window_count . saturating_add ( 1 ) ;
}
}
}
@@ -230,6 +533,7 @@ pub fn spawn_sim(default_shape: GalaxyShape) -> SimHandle {
SimHandle { tx }
}
#[ allow(clippy::too_many_arguments) ]
fn handle_cmd (
cmd : SimCmd ,
world : & mut World ,
@@ -237,6 +541,8 @@ fn handle_cmd(
indexes : & mut std ::collections ::HashMap < GalaxyId , KiddoIndex > ,
_rng : & mut SmallRng ,
default_shape : GalaxyShape ,
started : Instant ,
ticks_per_sec : u32 ,
) {
match cmd {
SimCmd ::CreateGalaxy { name , reply } = > {
@@ -280,6 +586,9 @@ fn handle_cmd(
manifest : e . manifest . clone ( ) ,
slate_dim : e . slate . as_ref ( ) . map ( | s | s . dim ( ) ) ,
slate_norm : e . slate . as_ref ( ) . map ( | s | s . norm ( ) ) ,
introspection : e . introspection . clone ( ) ,
memories : e . memories . iter ( ) . cloned ( ) . collect ( ) ,
pinned : e . pinned ,
} )
} ) ;
match detail {
@@ -291,6 +600,19 @@ fn handle_cmd(
}
}
}
SimCmd ::UpdateIntrospection { galaxy , engram , introspection , reply } = > {
let res = world
. galaxies
. get_mut ( & galaxy )
. ok_or ( SimError ::UnknownGalaxy )
. and_then ( | g | {
let e = g . engrams . get_mut ( & engram ) . ok_or ( SimError ::UnknownGalaxy ) ? ;
e . introspection = introspection ;
push_memory ( & mut e . memories , ms_since ( started ) , MemoryKind ::Introspected ) ;
Ok ( ( ) )
} ) ;
let _ = reply . send ( res ) ;
}
SimCmd ::Subscribe { galaxy , reply } = > {
let res = world
. galaxies
@@ -308,6 +630,186 @@ fn handle_cmd(
} ;
let _ = reply . send ( res ) ;
}
SimCmd ::StartQueryEngram { galaxy , query , text , slate , reply } = > {
let res = world
. galaxies
. get_mut ( & galaxy )
. ok_or ( SimError ::UnknownGalaxy )
. map ( | g | materialize_query_engram ( g , galaxy , query , text , slate , started ) ) ;
let _ = reply . send ( res ) ;
}
SimCmd ::BroadcastQuery { galaxy , query_engram , max_responders , reply } = > {
let res = world
. galaxies
. get ( & galaxy )
. ok_or ( SimError ::UnknownGalaxy )
. map ( | g | compute_query_responders ( g , query_engram , max_responders ) ) ;
let _ = reply . send ( res ) ;
}
SimCmd ::GetEngramText { galaxy , engram , reply } = > {
let res = world
. galaxies
. get ( & galaxy )
. ok_or ( SimError ::UnknownGalaxy )
. map ( | g | {
g . engrams . get ( & engram ) . and_then ( | e | {
e . manifest
. as_ref ( )
. map ( | Manifest ::Text { content } | content . clone ( ) )
} )
} ) ;
let _ = reply . send ( res ) ;
}
SimCmd ::RecordQueryParticipation { galaxy , query , engram , snippet , reply } = > {
let res = world
. galaxies
. get_mut ( & galaxy )
. ok_or ( SimError ::UnknownGalaxy )
. map ( | g | {
if let Some ( e ) = g . engrams . get_mut ( & engram ) {
push_memory (
& mut e . memories ,
ms_since ( started ) ,
MemoryKind ::QueryParticipated { query , snippet } ,
) ;
e . state = EngramState ::Conversing ;
g . emit ( SimEvent ::EngramStateChanged {
id : engram ,
state : EngramState ::Conversing ,
} ) ;
// Schedule the auto-revert. If the engram gets re-lit
// for a different query before this fires, the
// `expected` guard will skip the revert.
scheduler . schedule (
Instant ::now ( ) + Duration ::from_millis ( RESPONDER_LIGHTUP_MS ) ,
Event ::RevertState {
galaxy ,
engram ,
expected : EngramState ::Conversing ,
revert_to : EngramState ::Idle ,
} ,
) ;
}
} ) ;
let _ = reply . send ( res ) ;
}
SimCmd ::PublishQueryAnswer { galaxy , query , version , status , answer , reply } = > {
let res = world
. galaxies
. get ( & galaxy )
. ok_or ( SimError ::UnknownGalaxy )
. map ( | g | {
g . emit ( SimEvent ::QueryAnswerUpdated { query , version , status , answer } ) ;
} ) ;
let _ = reply . send ( res ) ;
}
SimCmd ::FinishQuery {
galaxy , query , query_engram , final_answer , responder_count , reply ,
} = > {
let res = world
. galaxies
. get_mut ( & galaxy )
. ok_or ( SimError ::UnknownGalaxy )
. map ( | g | {
if let Some ( e ) = g . engrams . get_mut ( & query_engram ) {
e . state = EngramState ::Memorize ;
push_memory (
& mut e . memories ,
ms_since ( started ) ,
MemoryKind ::QueryAnswered {
answer : final_answer ,
responder_count ,
} ,
) ;
g . emit ( SimEvent ::EngramStateChanged {
id : query_engram ,
state : EngramState ::Memorize ,
} ) ;
}
g . emit ( SimEvent ::QueryFinished {
query ,
status : QueryStatus ::Done ,
responder_count ,
} ) ;
} ) ;
let _ = reply . send ( res ) ;
}
SimCmd ::SnapshotAll { reply } = > {
let snaps = world
. galaxies
. values ( )
. map ( | g | g . to_snapshot ( ) )
. collect ( ) ;
let _ = reply . send ( snaps ) ;
}
SimCmd ::HydrateGalaxy { snapshot , reply } = > {
let res = match snapshot . galaxy . shape . validate ( ) {
Err ( msg ) = > Err ( SimError ::InvalidShape ( msg ) ) ,
Ok ( ( ) ) = > {
let id = snapshot . galaxy . id ;
let state = GalaxyState ::from_snapshot ( snapshot ) ;
let info = state . info ( ) ;
state . emit ( state . torus_event ( ) ) ;
// Re-schedule the periodic galaxy events first so they
// start firing at their normal cadence.
let now = Instant ::now ( ) ;
scheduler . schedule ( now + FRAME_INTERVAL , Event ::BroadcastFrame { galaxy : id } ) ;
scheduler . schedule ( now + REBUILD_INTERVAL , Event ::RebuildIndex { galaxy : id } ) ;
// One EngramTick per non-pinned engram. Stagger them
// across one tick interval so they don't all fire
// simultaneously and clobber the scheduler heap on
// boot — also gives the spatial index time to rebuild
// before gravity kicks in.
let n = state . engrams . len ( ) . max ( 1 ) as u64 ;
let stagger_step = TICK_INTERVAL . as_micros ( ) as u64 / n . max ( 1 ) ;
for ( i , engram_id ) in state
. engrams
. keys ( )
. copied ( )
. enumerate ( )
{
let offset = Duration ::from_micros ( stagger_step * i as u64 ) ;
scheduler . schedule (
now + TICK_INTERVAL + offset ,
Event ::EngramTick { galaxy : id , engram : engram_id } ,
) ;
}
indexes . insert ( id , KiddoIndex ::empty ( ) ) ;
world . galaxies . insert ( id , state ) ;
Ok ( info )
}
} ;
let _ = reply . send ( res ) ;
}
SimCmd ::DeleteGalaxy { galaxy , reply } = > {
let res = if world . galaxies . remove ( & galaxy ) . is_some ( ) {
indexes . remove ( & galaxy ) ;
Ok ( ( ) )
} else {
Err ( SimError ::UnknownGalaxy )
} ;
let _ = reply . send ( res ) ;
}
SimCmd ::GetStats { reply } = > {
let mut engrams_total : u32 = 0 ;
let mut synapses_total : u32 = 0 ;
for g in world . galaxies . values ( ) {
// Pinned (query) engrams are excluded from the public count
// so the HUD doesn't blink during queries.
engrams_total + = g
. engrams
. values ( )
. filter ( | e | ! e . pinned )
. count ( ) as u32 ;
synapses_total + = g . synapses . len ( ) as u32 ;
}
let _ = reply . send ( SimStats {
ticks_per_sec ,
engrams_total ,
galaxies : world . galaxies . len ( ) as u32 ,
synapses_total ,
} ) ;
}
}
}
@@ -387,6 +889,56 @@ fn ingest_galaxy(
Ok ( ids )
}
/// Materialize a Query-Engram (Stage 5): pinned at the galaxy center with the
/// question text + slate, state `Searching`. Returns the new engram's id. No
/// `EngramTick` is scheduled — pinned engrams don't move and don't form
/// spontaneous synapses; their behaviour is driven entirely by the orchestrator.
fn materialize_query_engram (
g : & mut GalaxyState ,
_galaxy : GalaxyId ,
_query : QueryId ,
text : String ,
slate : Slate ,
started : Instant ,
) -> EngramId {
let id = EngramId ::new ( ) ;
let instance_idx = g . slot_to_id . len ( ) as u32 ;
let position = g . galaxy . center ;
let mut memories = VecDeque ::new ( ) ;
push_memory ( & mut memories , ms_since ( started ) , MemoryKind ::Born ) ;
let engram = Engram {
id ,
instance_idx ,
position ,
velocity : Vec3 ::ZERO ,
size : 1.0 ,
state : EngramState ::Searching ,
age : 0 ,
manifest : Some ( Manifest ::Text { content : text . clone ( ) } ) ,
slate : Some ( slate ) ,
introspection : Introspection ::default ( ) ,
memories ,
pinned : true ,
} ;
g . slot_to_id . push ( id ) ;
g . engrams . insert ( id , engram . clone ( ) ) ;
let snapshot = EngramSnapshot {
id ,
instance_idx ,
position : position . to_array ( ) ,
size : engram . size ,
state : engram . state ,
} ;
g . emit ( SimEvent ::EngramCreated { snapshot } ) ;
g . emit ( SimEvent ::QueryStarted {
query : _query ,
engram : id ,
position : position . to_array ( ) ,
text ,
} ) ;
id
}
/// Materialize one engram with the pre-allocated id at the galaxy's birth
/// point with a random initial velocity, emit `EngramCreated`, and schedule
/// its first `EngramTick`.
@@ -397,6 +949,7 @@ fn materialize_engram(
payload : SpawnPayload ,
scheduler : & mut Scheduler ,
rng : & mut SmallRng ,
started : Instant ,
) {
let instance_idx = g . slot_to_id . len ( ) as u32 ;
let birth = g . galaxy . shape . birth_point ( g . galaxy . center ) ;
@@ -406,6 +959,8 @@ fn materialize_engram(
SpawnPayload ::Synthetic = > ( None , None ) ,
SpawnPayload ::Manifested { manifest , slate } = > ( Some ( manifest ) , Some ( slate ) ) ,
} ;
let mut memories = VecDeque ::new ( ) ;
push_memory ( & mut memories , ms_since ( started ) , MemoryKind ::Born ) ;
let engram = Engram {
id ,
instance_idx ,
@@ -416,6 +971,9 @@ fn materialize_engram(
age : 0 ,
manifest ,
slate ,
introspection : Introspection ::default ( ) ,
memories ,
pinned : false ,
} ;
g . slot_to_id . push ( id ) ;
g . engrams . insert ( id , engram . clone ( ) ) ;
@@ -433,6 +991,19 @@ fn materialize_engram(
) ;
}
/// Append a memory to the front-bounded VecDeque, evicting the oldest entry
/// once `MAX_MEMORIES` is reached.
fn push_memory ( memories : & mut VecDeque < Memory > , at_ms : u32 , kind : MemoryKind ) {
while memories . len ( ) > = MAX_MEMORIES {
memories . pop_front ( ) ;
}
memories . push_back ( Memory { at_ms , kind } ) ;
}
fn ms_since ( started : Instant ) -> u32 {
started . elapsed ( ) . as_millis ( ) as u32
}
/// Walk the kiddo index for `engram_id`'s neighbours and return:
/// - `gravity_acc`: cosine-weighted attraction toward similar peers (only
/// contributes inside the tube; physics gates this further by position).
@@ -492,6 +1063,97 @@ fn compute_gravity_and_candidates(
( acc , candidates )
}
/// Stage 5 broadcast retrieval. Two passes:
///
/// **Phase A — global cosine scan.** Score every non-pinned engram's slate
/// against the query, keep those above `QUERY_PHASE_A_THRESHOLD`, take the
/// top `QUERY_PHASE_A_FRAC * max_responders` by score.
///
/// **Phase B — 1-hop synaptic expansion.** For each Phase A pick, walk its
/// synapses and add any neighbour we haven't already chosen. The neighbour's
/// effective score is `cosine_to_query * synapse_weight` (synaptic
/// attenuation per the system-analysis doc). Fill the remaining
/// `max_responders - phase_a` slots in score-sorted order.
///
/// We do NOT use the spatial kiddo index here. Query-Engrams sit at the
/// galaxy center while normal engrams cluster in the tube ~major_radius
/// away — a single radius doesn't cover both. A linear scan over engrams
/// is fine at MVP scale (a few thousand) and avoids per-query index
/// rebuilds. The kiddo index stays where it earns its keep: per-tick
/// gravity for engrams already in the tube.
fn compute_query_responders (
g : & GalaxyState ,
query_engram : EngramId ,
max_responders : usize ,
) -> Vec < ( EngramId , f32 ) > {
if max_responders = = 0 {
return Vec ::new ( ) ;
}
let Some ( q ) = g . engrams . get ( & query_engram ) else { return Vec ::new ( ) ; } ;
let Some ( q_slate ) = q . slate . as_ref ( ) else { return Vec ::new ( ) ; } ;
// Phase A.
let mut scored : Vec < ( EngramId , f32 ) > = g
. engrams
. values ( )
. filter ( | e | e . id ! = query_engram & & ! e . pinned )
. filter_map ( | e | {
let s = e . slate . as_ref ( ) ? ;
let cos = q_slate . cosine ( s ) ;
( cos > = QUERY_PHASE_A_THRESHOLD ) . then_some ( ( e . id , cos ) )
} )
. collect ( ) ;
scored . sort_by ( | a , b | {
b . 1. partial_cmp ( & a . 1 ) . unwrap_or ( std ::cmp ::Ordering ::Equal )
} ) ;
let phase_a_budget = ( ( max_responders as f32 ) * QUERY_PHASE_A_FRAC ) . ceil ( ) as usize ;
let phase_a_budget = phase_a_budget . min ( max_responders ) . max ( 1 ) ;
let mut chosen : Vec < ( EngramId , f32 ) > = scored . into_iter ( ) . take ( phase_a_budget ) . collect ( ) ;
let mut chosen_set : HashSet < EngramId > =
chosen . iter ( ) . map ( | ( id , _ ) | * id ) . collect ( ) ;
// Always exclude the query-engram itself from synaptic expansion just in
// case some legacy synapse pointed at it.
chosen_set . insert ( query_engram ) ;
// Phase B: build adjacency once, then walk.
let phase_b_budget = max_responders . saturating_sub ( chosen . len ( ) ) ;
if phase_b_budget = = 0 | | g . synapses . is_empty ( ) {
return chosen ;
}
let mut adjacency : HashMap < EngramId , Vec < ( EngramId , f32 ) > > =
HashMap ::with_capacity ( g . engrams . len ( ) ) ;
for syn in g . synapses . values ( ) {
if syn . weight < QUERY_PHASE_B_SYNAPSE_THRESHOLD {
continue ;
}
adjacency . entry ( syn . a ) . or_default ( ) . push ( ( syn . b , syn . weight ) ) ;
adjacency . entry ( syn . b ) . or_default ( ) . push ( ( syn . a , syn . weight ) ) ;
}
let mut additions : Vec < ( EngramId , f32 ) > = Vec ::new ( ) ;
for ( seed_id , _ ) in chosen . iter ( ) {
if let Some ( neighbours ) = adjacency . get ( seed_id ) {
for ( neighbour , weight ) in neighbours {
if ! chosen_set . insert ( * neighbour ) {
continue ;
}
let Some ( n ) = g . engrams . get ( neighbour ) else { continue ; } ;
if n . pinned {
continue ;
}
let Some ( n_slate ) = n . slate . as_ref ( ) else { continue ; } ;
let effective = q_slate . cosine ( n_slate ) * weight ;
additions . push ( ( * neighbour , effective ) ) ;
}
}
}
// Take the strongest Phase B additions to fill remaining slots.
additions . sort_by ( | a , b | {
b . 1. partial_cmp ( & a . 1 ) . unwrap_or ( std ::cmp ::Ordering ::Equal )
} ) ;
chosen . extend ( additions . into_iter ( ) . take ( phase_b_budget ) ) ;
chosen
}
/// Closest point on the torus centerline to `p` in xy-plane (mirrors the
/// math in `physics::spine_point` but doesn't pull `physics` in here).
fn nearest_spine_xy ( p : Vec3 , major_r : f32 ) -> Vec3 {
@@ -515,7 +1177,7 @@ fn handle_event(
match event {
Event ::Spawn { galaxy , id , payload } = > {
let Some ( g ) = world . galaxies . get_mut ( & galaxy ) else { return } ;
materialize_engram ( g , galaxy , id , payload , scheduler , rng ) ;
materialize_engram ( g , galaxy , id , payload , scheduler , rng , started );
}
Event ::EngramTick { galaxy , engram } = > {
let Some ( g ) = world . galaxies . get_mut ( & galaxy ) else { return } ;
@@ -535,11 +1197,27 @@ fn handle_event(
return ;
}
// After the tick, materialise any qualifying synapses and emit
// events for the newly-formed ones.
// After the tick, materialise any qualifying synapses, emit
// events for the newly-formed ones, and append a memory entry
// on both endpoints so the inspector can show them.
for ( peer , weight ) in synapse_candidates {
if let Some ( syn ) = g . try_form_synapse ( engram , peer , weight ) {
g . emit ( SimEvent ::SynapseCreated { synapse : SynapseDto ::from ( & syn ) } ) ;
let now_ms = ms_since ( started ) ;
if let Some ( e_a ) = g . engrams . get_mut ( & syn . a ) {
push_memory (
& mut e_a . memories ,
now_ms ,
MemoryKind ::SynapseFormed { with : syn . b , weight } ,
) ;
}
if let Some ( e_b ) = g . engrams . get_mut ( & syn . b ) {
push_memory (
& mut e_b . memories ,
now_ms ,
MemoryKind ::SynapseFormed { with : syn . a , weight } ,
) ;
}
}
}
@@ -568,11 +1246,14 @@ fn handle_event(
}
Event ::RebuildIndex { galaxy } = > {
// Index rebuild only — no bbox recompute (the torus is fixed-size,
// resized only via SimCmd::Resize).
// resized only via SimCmd::Resize). Pinned (query) engrams are
// skipped: the index drives gravity + per-tick neighbour search,
// and queries don't participate in either.
if let Some ( g ) = world . galaxies . get ( & galaxy ) {
let points : Vec < ( EngramId , [ f32 ; 3 ] ) > = g
. engrams
. values ( )
. filter ( | e | ! e . pinned )
. map ( | e | ( e . id , e . position . to_array ( ) ) )
. collect ( ) ;
if let Some ( idx ) = indexes . get_mut ( & galaxy ) {
@@ -584,5 +1265,15 @@ fn handle_event(
Event ::RebuildIndex { galaxy } ,
) ;
}
Event ::RevertState { galaxy , engram , expected , revert_to } = > {
if let Some ( g ) = world . galaxies . get_mut ( & galaxy ) {
if let Some ( e ) = g . engrams . get_mut ( & engram ) {
if e . state = = expected {
e . state = revert_to ;
g . emit ( SimEvent ::EngramStateChanged { id : engram , state : revert_to } ) ;
}
}
}
}
}
}