diff --git a/Cargo.lock b/Cargo.lock index 895e642572..072d59f005 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10708,6 +10708,13 @@ dependencies = [ "rand_distr 0.4.3", ] +[[package]] +name = "ruvector-structural-memory-merge" +version = "0.1.0" +dependencies = [ + "emergent-time", +] + [[package]] name = "ruvector-temporal-coherence" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index b4381e6431..a8bc4febb4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -302,6 +302,8 @@ members = [ "crates/ruvector-streaming-qng", # Entropy-adaptive ANN beam search: live Shannon entropy gates beam width (ADR-303) "crates/ruvector-entropy-ann", + # Structural-time + coherence conflict resolution for concurrent multi-agent memory writes (ADR-305) + "crates/ruvector-structural-memory-merge", ] resolver = "2" diff --git a/crates/ruvector-structural-memory-merge/Cargo.toml b/crates/ruvector-structural-memory-merge/Cargo.toml new file mode 100644 index 0000000000..9c9ba5f999 --- /dev/null +++ b/crates/ruvector-structural-memory-merge/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "ruvector-structural-memory-merge" +version = "0.1.0" +edition = "2021" +description = "Conflict resolution for concurrent multi-agent shared memory writes using emergent-time structural proper time plus coherence, instead of wall-clock or vector-clock last-write-wins" +authors = ["ruvnet", "claude-flow"] +license = "MIT OR Apache-2.0" +repository = "https://github.com/ruvnet/ruvector" +keywords = ["agent-memory", "crdt", "structural-time", "coherence", "ruvector"] +categories = ["algorithms", "data-structures"] + +[[bin]] +name = "structural-memory-merge-bench" +path = "src/main.rs" + +[dependencies] +emergent-time = { path = "../emergent-time" } + +[dev-dependencies] diff --git a/crates/ruvector-structural-memory-merge/src/lib.rs b/crates/ruvector-structural-memory-merge/src/lib.rs new file mode 100644 index 0000000000..b92e19358d --- /dev/null +++ b/crates/ruvector-structural-memory-merge/src/lib.rs @@ -0,0 +1,296 @@ +//! Conflict resolution for concurrent multi-agent shared memory. +//! +//! When several autonomous agents write to the same semantic memory slot +//! without a synchronized wall clock or a single sequencer, something has to +//! decide which write survives. The standard answer — last-write-wins (LWW) +//! by wall-clock timestamp — is vulnerable to clock skew between agents and +//! is semantically blind: it cannot tell a high-value memory update from a +//! low-value one. A vector-clock LWW fixes the skew problem for *causally +//! related* writes but still has to arbitrarily tie-break writes that are +//! genuinely concurrent (neither happened-before the other). +//! +//! This crate adds a third policy, [`StructuralCoherenceMerge`], that never +//! overrides real causal order (a happens-before write is always superseded +//! by what it happened before) but, for genuinely concurrent conflicts, +//! breaks the tie using `emergent-time`'s +//! [`StructuralProperTime`](emergent_time::structural_clock::StructuralProperTime) +//! — the magnitude of structural state-change the write represents — combined +//! with a coherence score against the current shared context window. + +pub mod scenario; +pub mod vclock; + +pub use emergent_time::structural_clock::{ + Clock, StateSnapshot, StructuralMetric, StructuralProperTime, +}; +pub use vclock::{AgentId, CausalOrder, VectorClock}; + +pub type MemoryKey = u64; + +/// One agent's proposed write to a shared memory key. +#[derive(Clone, Debug)] +pub struct MemoryWrite { + pub agent_id: AgentId, + pub key: MemoryKey, + /// Local wall-clock timestamp in milliseconds (may be skewed vs. other agents). + pub wall_ts_ms: f64, + pub vclock: VectorClock, + /// The writing agent's local state immediately before this write. + pub prev_snapshot: StateSnapshot, + /// The new memory's structural state. + pub snapshot: StateSnapshot, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Winner { + A, + B, +} + +/// A merge policy's decision on one conflicting pair, with enough detail to +/// audit *why* — analogous to a witness log entry +/// (see `ruvector-proof-gate` / `ruvector-retrieval-receipt` for the +/// cryptographic hardening of this idea in the write/read paths). +#[derive(Clone, Debug)] +pub struct Decision { + pub winner: Winner, + pub reason: &'static str, + pub tau_a: f64, + pub tau_b: f64, + pub causal_order: CausalOrder, +} + +pub trait MergePolicy { + fn name(&self) -> &'static str; + fn resolve(&self, a: &MemoryWrite, b: &MemoryWrite, context: &[Vec]) -> Decision; +} + +fn dot(a: &[f64], b: &[f64]) -> f64 { + a.iter().zip(b).map(|(x, y)| x * y).sum() +} + +fn norm(a: &[f64]) -> f64 { + dot(a, a).sqrt() +} + +/// f64 cosine coherence against a context window — the same "max similarity +/// to any context vector" signal as `ruvector-agent-memory::scoring`, lifted +/// to the f64 embeddings `StructuralProperTime` operates on. +fn coherence_score(v: &[f64], context: &[Vec]) -> f64 { + let nv = norm(v); + if nv < 1e-9 || context.is_empty() { + return 0.0; + } + context + .iter() + .map(|q| { + let nq = norm(q); + if nq < 1e-9 { + 0.0 + } else { + (dot(v, q) / (nv * nq)).clamp(-1.0, 1.0) + } + }) + .fold(f64::NEG_INFINITY, f64::max) + .max(0.0) +} + +/// Baseline: last-write-wins by raw wall-clock timestamp. Ignores causal +/// order and content entirely — the ubiquitous default in eventually +/// consistent systems. +pub struct LwwWallClock; +impl MergePolicy for LwwWallClock { + fn name(&self) -> &'static str { + "LwwWallClock" + } + fn resolve(&self, a: &MemoryWrite, b: &MemoryWrite, _context: &[Vec]) -> Decision { + let causal_order = a.vclock.compare(&b.vclock); + let winner = if a.wall_ts_ms >= b.wall_ts_ms { + Winner::A + } else { + Winner::B + }; + Decision { + winner, + reason: "later wall_ts_ms", + tau_a: 0.0, + tau_b: 0.0, + causal_order, + } + } +} + +/// Variant A: last-write-wins ordered by vector clock. Never violates causal +/// order, but ties among genuinely concurrent writes are broken by agent id +/// — a fixed, content-blind rule. +pub struct LwwVectorClock; +impl MergePolicy for LwwVectorClock { + fn name(&self) -> &'static str { + "LwwVectorClock" + } + fn resolve(&self, a: &MemoryWrite, b: &MemoryWrite, _context: &[Vec]) -> Decision { + let causal_order = a.vclock.compare(&b.vclock); + let (winner, reason) = match causal_order { + CausalOrder::Before => (Winner::B, "b causally follows a"), + CausalOrder::After => (Winner::A, "a causally follows b"), + CausalOrder::Equal => (Winner::B, "equal clocks, arbitrary"), + CausalOrder::Concurrent => { + if a.agent_id >= b.agent_id { + (Winner::A, "concurrent, higher agent_id") + } else { + (Winner::B, "concurrent, higher agent_id") + } + } + }; + Decision { + winner, + reason, + tau_a: 0.0, + tau_b: 0.0, + causal_order, + } + } +} + +/// Variant B (the candidate): respects causal order exactly like +/// [`LwwVectorClock`], but for genuinely concurrent conflicts, breaks the tie +/// using each write's structural proper-time magnitude (how much the write +/// actually moves the agent's state) weighted by its coherence with the +/// current shared context. +#[derive(Default)] +pub struct StructuralCoherenceMerge { + pub metric: StructuralMetric, +} + +impl MergePolicy for StructuralCoherenceMerge { + fn name(&self) -> &'static str { + "StructuralCoherenceMerge" + } + fn resolve(&self, a: &MemoryWrite, b: &MemoryWrite, context: &[Vec]) -> Decision { + let causal_order = a.vclock.compare(&b.vclock); + let clock = StructuralProperTime::new(self.metric); + let tau_a = clock.tick(&a.prev_snapshot, &a.snapshot); + let tau_b = clock.tick(&b.prev_snapshot, &b.snapshot); + + if let Some((winner, reason)) = match causal_order { + CausalOrder::Before => Some((Winner::B, "b causally follows a")), + CausalOrder::After => Some((Winner::A, "a causally follows b")), + _ => None, + } { + return Decision { + winner, + reason, + tau_a, + tau_b, + causal_order, + }; + } + + let coh_a = coherence_score(&a.snapshot.embedding, context); + let coh_b = coherence_score(&b.snapshot.embedding, context); + // Structural magnitude scaled by contextual relevance: a large + // structural shift only counts fully if it also lands somewhere the + // current context cares about. + let score_a = tau_a * (0.5 + 0.5 * coh_a); + let score_b = tau_b * (0.5 + 0.5 * coh_b); + let winner = if score_a >= score_b { + Winner::A + } else { + Winner::B + }; + Decision { + winner, + reason: "concurrent, higher tau*coherence", + tau_a, + tau_b, + causal_order, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use scenario::{generate, ScenarioConfig}; + + #[test] + fn vclock_and_structural_never_violate_causal_order() { + let cfg = ScenarioConfig { + num_conflicts: 20, + num_causal_controls: 300, + wall_skew_ms: 5000.0, // deliberately large, to try to trip a violation + ..Default::default() + }; + let cases = generate(&cfg); + let vc = LwwVectorClock; + let sc = StructuralCoherenceMerge::default(); + for c in cases.iter().filter(|c| !c.is_concurrent) { + let d_vc = vc.resolve(&c.a, &c.b, &c.context); + let d_sc = sc.resolve(&c.a, &c.b, &c.context); + assert_eq!( + d_vc.winner, + Winner::B, + "vclock policy must keep b (causally later)" + ); + assert_eq!( + d_sc.winner, + Winner::B, + "structural policy must keep b (causally later)" + ); + } + } + + #[test] + fn wall_clock_lww_can_violate_causal_order_under_skew() { + let cfg = ScenarioConfig { + num_conflicts: 0, + num_causal_controls: 500, + wall_skew_ms: 5000.0, + jitter_ms: 10.0, + ..Default::default() + }; + let cases = generate(&cfg); + let wc = LwwWallClock; + let violations = cases + .iter() + .filter(|c| !c.is_concurrent) + .filter(|c| wc.resolve(&c.a, &c.b, &c.context).winner == Winner::A) + .count(); + assert!( + violations > 0, + "expected wall-clock LWW to violate causal order at least once under 5000ms skew" + ); + } + + #[test] + fn structural_merge_prefers_larger_coherent_shift() { + let base = StateSnapshot::full(vec![0.0, 0.0], 1.0, 0.5, 0.0, 0.0); + let small = StateSnapshot::full(vec![0.1, 0.0], 0.9, 0.55, 0.02, 0.9); + let large = StateSnapshot::full(vec![1.0, 0.0], 0.2, 0.95, 0.4, 0.05); + let mut vc_a = VectorClock::new(); + vc_a.tick(1); + let mut vc_b = VectorClock::new(); + vc_b.tick(2); + let a = MemoryWrite { + agent_id: 1, + key: 0, + wall_ts_ms: 0.0, + vclock: vc_a, + prev_snapshot: base.clone(), + snapshot: small, + }; + let b = MemoryWrite { + agent_id: 2, + key: 0, + wall_ts_ms: 0.0, + vclock: vc_b, + prev_snapshot: base, + snapshot: large, + }; + let context = vec![vec![1.0, 0.0]]; + let sc = StructuralCoherenceMerge::default(); + let d = sc.resolve(&a, &b, &context); + assert_eq!(d.winner, Winner::B); + assert!(d.tau_b > d.tau_a); + } +} diff --git a/crates/ruvector-structural-memory-merge/src/main.rs b/crates/ruvector-structural-memory-merge/src/main.rs new file mode 100644 index 0000000000..aed02027dd --- /dev/null +++ b/crates/ruvector-structural-memory-merge/src/main.rs @@ -0,0 +1,192 @@ +//! Benchmark: does structural-time + coherence conflict resolution beat +//! wall-clock and vector-clock last-write-wins on concurrent multi-agent +//! memory writes? +//! +//! Formal hypothesis (fixed before this binary was run; see +//! `docs/research/nightly/2026-08-14-structural-time-memory-merge/README.md`): +//! +//! Given 2000 genuinely concurrent (causally-unordered) synthetic memory-write +//! conflicts across 6 agents with a hidden ground-truth quality label, +//! +//! when `StructuralCoherenceMerge` resolves each conflict instead of +//! `LwwWallClock` (400ms per-agent clock skew) or `LwwVectorClock` +//! (agent-id tiebreak), +//! +//! then its correct-resolution rate (picks the higher hidden-quality write) +//! should exceed both baselines by >= 10 percentage points, +//! +//! subject to: zero causal-order violations on a 500-case causally-ordered +//! control set (all three policies), and throughput within 5x of the +//! wall-clock baseline. + +use ruvector_structural_memory_merge::scenario::{generate, ConflictCase, ScenarioConfig}; +use ruvector_structural_memory_merge::{ + LwwVectorClock, LwwWallClock, MergePolicy, StructuralCoherenceMerge, Winner, +}; +use std::time::Instant; + +struct PolicyResult { + name: &'static str, + correct_rate: f64, + mean_quality_regret: f64, + causal_violations: usize, + causal_total: usize, + merges_per_sec: f64, +} + +fn eval_policy( + policy: &dyn MergePolicy, + conflicts: &[ConflictCase], + controls: &[ConflictCase], +) -> PolicyResult { + let mut correct = 0usize; + let mut regret_sum = 0.0f64; + for c in conflicts { + let d = policy.resolve(&c.a, &c.b, &c.context); + let (winner_q, loser_q) = match d.winner { + Winner::A => (c.quality_a, c.quality_b), + Winner::B => (c.quality_b, c.quality_a), + }; + if winner_q >= loser_q { + correct += 1; + } + regret_sum += (loser_q - winner_q).max(0.0); + } + let correct_rate = correct as f64 / conflicts.len().max(1) as f64; + let mean_quality_regret = regret_sum / conflicts.len().max(1) as f64; + + let mut causal_violations = 0usize; + for c in controls { + let d = policy.resolve(&c.a, &c.b, &c.context); + // Control pairs are constructed so `a` always causally precedes `b`; + // the only correct winner is `b`. Anything else is a causal-order + // violation, independent of quality. + if d.winner != Winner::B { + causal_violations += 1; + } + } + + let warmup = conflicts.iter().chain(controls.iter()).take(200); + for c in warmup { + std::hint::black_box(policy.resolve(&c.a, &c.b, &c.context)); + } + let reps = 20usize; + let start = Instant::now(); + for _ in 0..reps { + for c in conflicts.iter().chain(controls.iter()) { + std::hint::black_box(policy.resolve(&c.a, &c.b, &c.context)); + } + } + let elapsed = start.elapsed(); + let total_ops = reps * (conflicts.len() + controls.len()); + let merges_per_sec = total_ops as f64 / elapsed.as_secs_f64(); + + PolicyResult { + name: policy.name(), + correct_rate, + mean_quality_regret, + causal_violations, + causal_total: controls.len(), + merges_per_sec, + } +} + +fn run_at_skew(skew_ms: f64) -> Vec { + let cfg = ScenarioConfig { + num_conflicts: 2000, + num_causal_controls: 500, + dim: 16, + num_agents: 6, + wall_skew_ms: skew_ms, + jitter_ms: 30.0, + seed: 0xA6E17, + }; + let cases = generate(&cfg); + let (controls, conflicts): (Vec<_>, Vec<_>) = cases.into_iter().partition(|c| !c.is_concurrent); + + let policies: Vec> = vec![ + Box::new(LwwWallClock), + Box::new(LwwVectorClock), + Box::new(StructuralCoherenceMerge::default()), + ]; + policies + .iter() + .map(|p| eval_policy(p.as_ref(), &conflicts, &controls)) + .collect() +} + +fn print_table(title: &str, results: &[PolicyResult]) { + println!("\n### {title}\n"); + println!("| Policy | Correct-resolution rate | Mean quality regret | Causal violations | Merges/sec |"); + println!("|---|---|---|---|---|"); + for r in results { + println!( + "| {} | {:.1}% | {:.4} | {}/{} | {:.0} |", + r.name, + r.correct_rate * 100.0, + r.mean_quality_regret, + r.causal_violations, + r.causal_total, + r.merges_per_sec + ); + } +} + +fn main() { + println!("# Structural-Time Memory Merge — benchmark\n"); + println!( + "Rust {}, release build, deterministic seed 0xA6E17.", + env!("CARGO_PKG_VERSION") + ); + + let zero_skew = run_at_skew(0.0); + print_table("Skew = 0ms (best case for wall-clock LWW)", &zero_skew); + + let realistic_skew = run_at_skew(400.0); + print_table( + "Skew = 400ms per-agent bias (realistic, unsynchronized edge agents)", + &realistic_skew, + ); + + let heavy_skew = run_at_skew(2000.0); + print_table("Skew = 2000ms per-agent bias (severe drift)", &heavy_skew); + + // --- Acceptance check against the pre-registered hypothesis ----------- + let wall = &realistic_skew[0]; + let vclock = &realistic_skew[1]; + let structural = &realistic_skew[2]; + + let beats_wall = structural.correct_rate - wall.correct_rate >= 0.10; + let beats_vclock = structural.correct_rate - vclock.correct_rate >= 0.10; + let no_causal_violations = structural.causal_violations == 0 && vclock.causal_violations == 0; + let throughput_ok = structural.merges_per_sec >= wall.merges_per_sec / 5.0; + + println!("\n### Acceptance check (realistic skew = 400ms)\n"); + println!( + "- beats LwwWallClock by >=10pp: {} ({:+.1}pp)", + beats_wall, + (structural.correct_rate - wall.correct_rate) * 100.0 + ); + println!( + "- beats LwwVectorClock by >=10pp: {} ({:+.1}pp)", + beats_vclock, + (structural.correct_rate - vclock.correct_rate) * 100.0 + ); + println!( + "- zero causal-order violations (vclock & structural): {} (vclock={}, structural={})", + no_causal_violations, vclock.causal_violations, structural.causal_violations + ); + println!( + "- throughput within 5x of wall-clock baseline: {} ({:.0} vs {:.0} merges/sec)", + throughput_ok, structural.merges_per_sec, wall.merges_per_sec + ); + + let verdict = if beats_wall && beats_vclock && no_causal_violations && throughput_ok { + "ACCEPT" + } else if !no_causal_violations || !beats_wall || !beats_vclock { + "REJECT" + } else { + "INCONCLUSIVE" + }; + println!("\n### VERDICT: {verdict}\n"); +} diff --git a/crates/ruvector-structural-memory-merge/src/scenario.rs b/crates/ruvector-structural-memory-merge/src/scenario.rs new file mode 100644 index 0000000000..5c7cdd8820 --- /dev/null +++ b/crates/ruvector-structural-memory-merge/src/scenario.rs @@ -0,0 +1,329 @@ +//! Deterministic synthetic scenario generator for the merge-policy benchmark. +//! +//! Ground-truth design (kept honest — see `docs/research/nightly` write-up): +//! each write carries a hidden `alpha` (structural-shift magnitude towards a +//! drifting "ideal" target) that a policy never observes directly. `alpha` +//! drives three *independent, noisy* channels a policy CAN observe: +//! +//! 1. the structural snapshot (embedding/entropy/graph/coherence/pred_error +//! deltas consumed by `StructuralProperTime`), +//! 2. the coherence context window (a separately-noised sample of the ideal +//! point, not the same noise draw as the write embedding), and +//! 3. wall-clock timestamp, corrupted by a persistent per-agent skew. +//! +//! `true_quality = alpha + independent noise` is the evaluation label. No +//! policy ever reads `alpha` or `true_quality` — only the noisy observables. +//! This keeps the "correct resolution rate" metric from leaking its own +//! answer into the algorithm under test. + +use crate::vclock::{AgentId, VectorClock}; +use crate::{MemoryKey, MemoryWrite}; +use emergent_time::structural_clock::StateSnapshot; + +struct Rng(u64); +impl Rng { + fn new(seed: u64) -> Self { + Rng(seed | 1) + } + /// Uniform in `[0, 1)`. + fn next_unit(&mut self) -> f64 { + let mut x = self.0; + x ^= x >> 12; + x ^= x << 25; + x ^= x >> 27; + self.0 = x; + let v = x.wrapping_mul(0x2545_F491_4F6C_DD1D); + (v >> 11) as f64 / (1u64 << 53) as f64 + } + /// Uniform in `[-1, 1)`. + fn next_signed(&mut self) -> f64 { + self.next_unit() * 2.0 - 1.0 + } +} + +#[derive(Clone, Copy, Debug)] +pub struct ScenarioConfig { + pub num_conflicts: usize, + pub num_causal_controls: usize, + pub dim: usize, + pub num_agents: u32, + /// Max magnitude (ms) of each agent's fixed wall-clock skew bias. + pub wall_skew_ms: f64, + /// Intra-event jitter (ms) applied even at zero skew. + pub jitter_ms: f64, + pub seed: u64, +} + +impl Default for ScenarioConfig { + fn default() -> Self { + ScenarioConfig { + num_conflicts: 2000, + num_causal_controls: 500, + dim: 16, + num_agents: 6, + wall_skew_ms: 400.0, + jitter_ms: 30.0, + seed: 0xA6E17, + } + } +} + +pub struct ConflictCase { + pub key: MemoryKey, + pub a: MemoryWrite, + pub b: MemoryWrite, + /// Hidden ground truth — visible only to the evaluator, never a policy. + pub quality_a: f64, + pub quality_b: f64, + pub context: Vec>, + /// `false` for the causal-control group (b causally follows a). + pub is_concurrent: bool, +} + +fn rand_vec(rng: &mut Rng, dim: usize) -> Vec { + (0..dim).map(|_| rng.next_signed()).collect() +} + +fn lerp_add(base: &[f64], target: &[f64], alpha: f64, noise: &[f64], noise_scale: f64) -> Vec { + base.iter() + .zip(target) + .zip(noise) + .map(|((b, t), n)| b + alpha * (t - b) + noise_scale * n) + .collect() +} + +/// Build one write whose hidden shift magnitude towards `ideal` is `alpha`. +#[allow(clippy::too_many_arguments)] +fn make_write( + rng: &mut Rng, + agent: AgentId, + key: MemoryKey, + vclock: VectorClock, + base_ms: f64, + skew_ms: f64, + jitter_ms: f64, + ideal: &[f64], + alpha: f64, + dim: usize, +) -> (MemoryWrite, f64) { + let prev_embedding = rand_vec(rng, dim); + let struct_noise = rand_vec(rng, dim); + let new_embedding = lerp_add(&prev_embedding, ideal, alpha, &struct_noise, 0.15); + + let prev_snapshot = StateSnapshot::full(prev_embedding, 1.0, 0.5, 0.0, 0.0); + let snapshot = StateSnapshot::full( + new_embedding, + 1.0 - 0.6 * alpha + 0.05 * rng.next_signed(), // entropy drops as the write converges + (0.3 + 0.6 * alpha + 0.05 * rng.next_signed()).clamp(0.0, 1.0), + 0.4 * alpha + 0.03 * rng.next_signed().abs(), + (1.0 - alpha + 0.05 * rng.next_signed()).clamp(0.0, 1.0), + ); + + let wall_ts_ms = base_ms + jitter_ms * rng.next_signed() + skew_ms; + let write = MemoryWrite { + agent_id: agent, + key, + wall_ts_ms, + vclock, + prev_snapshot, + snapshot, + }; + let quality = (alpha + 0.20 * rng.next_signed()).clamp(0.0, 1.5); + (write, quality) +} + +/// Generate the benchmark corpus: `num_conflicts` genuinely concurrent write +/// pairs (the primary test set) followed by `num_causal_controls` +/// causally-ordered pairs (sanity/control set for causal-order preservation). +pub fn generate(cfg: &ScenarioConfig) -> Vec { + let mut rng = Rng::new(cfg.seed); + let mut skew = Vec::with_capacity(cfg.num_agents as usize); + for _ in 0..cfg.num_agents { + skew.push(cfg.wall_skew_ms * rng.next_signed()); + } + + let mut cases = Vec::with_capacity(cfg.num_conflicts + cfg.num_causal_controls); + let step_ms = 1000.0; + + for i in 0..cfg.num_conflicts { + let ideal = rand_vec(&mut rng, cfg.dim); + let ag_a = (rng.next_unit() * cfg.num_agents as f64) as u32 % cfg.num_agents; + let mut ag_b = (rng.next_unit() * cfg.num_agents as f64) as u32 % cfg.num_agents; + if ag_b == ag_a { + ag_b = (ag_b + 1) % cfg.num_agents; + } + let alpha_a = rng.next_unit(); + let alpha_b = rng.next_unit(); + let base_ms = i as f64 * step_ms; + + let mut vc_a = VectorClock::new(); + vc_a.tick(ag_a); + let mut vc_b = VectorClock::new(); + vc_b.tick(ag_b); + + let (wa, qa) = make_write( + &mut rng, + ag_a, + i as MemoryKey, + vc_a, + base_ms, + skew[ag_a as usize], + cfg.jitter_ms, + &ideal, + alpha_a, + cfg.dim, + ); + let (wb, qb) = make_write( + &mut rng, + ag_b, + i as MemoryKey, + vc_b, + base_ms, + skew[ag_b as usize], + cfg.jitter_ms, + &ideal, + alpha_b, + cfg.dim, + ); + + // Context window: independently-noised samples around the same ideal + // point (different noise draw than either write's embedding). + let context: Vec> = (0..3) + .map(|_| { + lerp_add( + &vec![0.0; cfg.dim], + &ideal, + 1.0, + &rand_vec(&mut rng, cfg.dim), + 0.35, + ) + }) + .collect(); + + cases.push(ConflictCase { + key: i as MemoryKey, + a: wa, + b: wb, + quality_a: qa, + quality_b: qb, + context, + is_concurrent: true, + }); + } + + for j in 0..cfg.num_causal_controls { + let idx = cfg.num_conflicts + j; + let ideal = rand_vec(&mut rng, cfg.dim); + let ag_a = (rng.next_unit() * cfg.num_agents as f64) as u32 % cfg.num_agents; + let mut ag_b = (rng.next_unit() * cfg.num_agents as f64) as u32 % cfg.num_agents; + if ag_b == ag_a { + ag_b = (ag_b + 1) % cfg.num_agents; + } + let base_ms = idx as f64 * step_ms; + + let mut vc_a = VectorClock::new(); + vc_a.tick(ag_a); + let alpha_a = rng.next_unit(); + let (wa, qa) = make_write( + &mut rng, + ag_a, + idx as MemoryKey, + vc_a.clone(), + base_ms, + skew[ag_a as usize], + cfg.jitter_ms, + &ideal, + alpha_a, + cfg.dim, + ); + + // b causally observes a's write, then ticks locally: b happens-after a. + let mut vc_b = vc_a.clone(); + vc_b.merge(&wa.vclock); + vc_b.tick(ag_b); + let alpha_b = rng.next_unit(); + let (wb, qb) = make_write( + &mut rng, + ag_b, + idx as MemoryKey, + vc_b, + base_ms + step_ms * 0.5, + skew[ag_b as usize], + cfg.jitter_ms, + &ideal, + alpha_b, + cfg.dim, + ); + + let context: Vec> = (0..3) + .map(|_| { + lerp_add( + &vec![0.0; cfg.dim], + &ideal, + 1.0, + &rand_vec(&mut rng, cfg.dim), + 0.35, + ) + }) + .collect(); + + cases.push(ConflictCase { + key: idx as MemoryKey, + a: wa, + b: wb, + quality_a: qa, + quality_b: qb, + context, + is_concurrent: false, + }); + } + + cases +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn concurrent_pairs_are_actually_concurrent() { + let cfg = ScenarioConfig { + num_conflicts: 50, + num_causal_controls: 10, + ..Default::default() + }; + let cases = generate(&cfg); + for c in cases.iter().filter(|c| c.is_concurrent) { + assert_eq!( + c.a.vclock.compare(&c.b.vclock), + crate::vclock::CausalOrder::Concurrent + ); + } + } + + #[test] + fn control_pairs_are_causally_ordered() { + let cfg = ScenarioConfig { + num_conflicts: 10, + num_causal_controls: 50, + ..Default::default() + }; + let cases = generate(&cfg); + for c in cases.iter().filter(|c| !c.is_concurrent) { + assert_eq!( + c.a.vclock.compare(&c.b.vclock), + crate::vclock::CausalOrder::Before + ); + } + } + + #[test] + fn deterministic_for_fixed_seed() { + let cfg = ScenarioConfig::default(); + let a = generate(&cfg); + let b = generate(&cfg); + assert_eq!(a.len(), b.len()); + assert_eq!(a[0].quality_a, b[0].quality_a); + assert_eq!(a[0].a.wall_ts_ms, b[0].a.wall_ts_ms); + } +} diff --git a/crates/ruvector-structural-memory-merge/src/vclock.rs b/crates/ruvector-structural-memory-merge/src/vclock.rs new file mode 100644 index 0000000000..17d213b95f --- /dev/null +++ b/crates/ruvector-structural-memory-merge/src/vclock.rs @@ -0,0 +1,119 @@ +//! Minimal vector clock for detecting happens-before vs. concurrent writes +//! across agents, without a synchronized wall clock. + +use std::collections::BTreeMap; + +pub type AgentId = u32; + +/// Causal relationship between two events' vector clocks. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum CausalOrder { + /// `self` happened before `other`. + Before, + /// `self` happened after `other`. + After, + /// Neither dominates: the writes are concurrent (a genuine conflict). + Concurrent, + /// Identical clocks. + Equal, +} + +/// A Lamport-style vector clock: one logical counter per known agent. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct VectorClock(BTreeMap); + +impl VectorClock { + pub fn new() -> Self { + VectorClock(BTreeMap::new()) + } + + pub fn get(&self, agent: AgentId) -> u64 { + *self.0.get(&agent).unwrap_or(&0) + } + + /// Advance this clock's own counter for `agent` by one local event. + pub fn tick(&mut self, agent: AgentId) { + let e = self.0.entry(agent).or_insert(0); + *e += 1; + } + + /// Merge in another clock's knowledge (component-wise max), as happens + /// when an agent observes a remote write. + pub fn merge(&mut self, other: &VectorClock) { + for (&agent, &v) in other.0.iter() { + let e = self.0.entry(agent).or_insert(0); + if v > *e { + *e = v; + } + } + } + + /// Compare against another clock to determine causal order. + pub fn compare(&self, other: &VectorClock) -> CausalOrder { + let agents: std::collections::BTreeSet = + self.0.keys().chain(other.0.keys()).copied().collect(); + // self_leq_other: self <= other componentwise (self dominated by other). + // other_leq_self: other <= self componentwise (other dominated by self). + let mut self_leq_other = true; + let mut other_leq_self = true; + for a in agents { + let sv = self.get(a); + let ov = other.get(a); + if sv > ov { + self_leq_other = false; + } + if ov > sv { + other_leq_self = false; + } + } + match (self_leq_other, other_leq_self) { + (true, true) => CausalOrder::Equal, + (true, false) => CausalOrder::Before, + (false, true) => CausalOrder::After, + (false, false) => CausalOrder::Concurrent, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn ticking_own_agent_is_before() { + let mut a = VectorClock::new(); + a.tick(1); + let mut b = a.clone(); + b.tick(1); + assert_eq!(a.compare(&b), CausalOrder::Before); + assert_eq!(b.compare(&a), CausalOrder::After); + } + + #[test] + fn independent_agents_are_concurrent() { + let mut a = VectorClock::new(); + a.tick(1); + let mut b = VectorClock::new(); + b.tick(2); + assert_eq!(a.compare(&b), CausalOrder::Concurrent); + } + + #[test] + fn merge_then_tick_dominates() { + let mut a = VectorClock::new(); + a.tick(1); + let mut b = VectorClock::new(); + b.tick(2); + // b observes a's write, then ticks locally: b now happens-after a. + b.merge(&a); + b.tick(2); + assert_eq!(a.compare(&b), CausalOrder::Before); + } + + #[test] + fn equal_clocks() { + let a = VectorClock::new(); + let b = VectorClock::new(); + assert_eq!(a.compare(&b), CausalOrder::Equal); + } +} diff --git a/docs/adr/ADR-305-structural-time-memory-merge.md b/docs/adr/ADR-305-structural-time-memory-merge.md new file mode 100644 index 0000000000..b6c8c54b0d --- /dev/null +++ b/docs/adr/ADR-305-structural-time-memory-merge.md @@ -0,0 +1,182 @@ +# ADR-305: Structural-Time Conflict Resolution for Concurrent Multi-Agent Memory + +**Date**: 2026-08-14 +**Status**: Accepted — PoC merged as an experimental crate; production integration requires the follow-ups in "Migration" +**Deciders**: Nightly research agent +**Tags**: agent-memory, emergent-time, coherence, crdt, multi-agent, ruvector-structural-memory-merge + +--- + +## Context + +RuVector positions itself as a substrate for autonomous, multi-agent systems (`docs/research/nightly/2026-06-13-temporal-coherence-agent-memory`, `2026-06-14-agent-memory-compaction`). Those two nightlies both address **single-agent** memory: how one agent ranks or compacts its own memory over time. + +Neither addresses what happens when **multiple agents share a memory namespace and write concurrently** — e.g. a swarm of edge agents (Cognitum Seed devices, ruFlo workers) updating a shared belief about the same entity without a synchronized wall clock or a single sequencer. RuVector has no answer to: *when two agents' writes to the same memory key conflict, which one survives?* + +The default answer in eventually-consistent systems is **last-write-wins (LWW) by wall-clock timestamp**. It has two known problems, both directly relevant to RuVector's edge/agent-OS ambitions: + +1. **Clock skew.** Unsynchronized edge devices do not share NTP-grade clocks. A device with a persistent forward clock bias wins every conflict it participates in, regardless of causal order or content. +2. **Semantic blindness.** LWW (and its safer cousin, vector-clock LWW with an arbitrary tiebreak) has no notion of *which write is more valuable* — it can only ever look at metadata, never content. + +RuVector already has `crates/emergent-time`, which defines **structural proper time**: a clock that measures how much a system's state actually moved (`τ = f(Δv, ΔS, ΔG, ΔC, ΔE)` over embedding, entropy, graph, coherence, and prediction-error channels), rather than counting wall-clock ticks. It also has `crates/ruvector-agent-memory::scoring`, which scores memories by coherence against an active context window. Neither had been connected to the multi-agent conflict-resolution problem before this nightly. + +--- + +## Hypothesis + +> Given 2000 genuinely concurrent (causally-unordered, per vector clock) synthetic memory-write conflicts across 6 agents, each write carrying a hidden ground-truth quality label never exposed to any policy, +> +> when conflicts are resolved by `StructuralCoherenceMerge` (respects causal order exactly; for truly concurrent writes, prefers the one with larger structural-proper-time magnitude weighted by coherence against the current context) instead of `LwwWallClock` (400ms per-agent clock skew) or `LwwVectorClock` (agent-id tiebreak on concurrent writes), +> +> then its correct-resolution rate (picks the write with the higher hidden quality) should exceed both baselines by ≥ 10 percentage points, +> +> subject to zero causal-order violations on a 500-case causally-ordered control set (all three policies to the extent they are logically capable of it) and merge throughput within 5× of the wall-clock baseline. + +--- + +## Decision + +**Accept.** Implement `crates/ruvector-structural-memory-merge` as a new, small (4 files, 936 lines total, none over 500) workspace crate. It depends only on `emergent-time` (already zero-dependency) and the Rust standard library. + +Three `MergePolicy` implementations were built and measured against each other on identical synthetic data: + +| Policy | Causal order | Concurrent tiebreak | +|---|---|---| +| `LwwWallClock` (baseline) | **Not respected** — picks the later `wall_ts_ms`, which clock skew can invert | later `wall_ts_ms` | +| `LwwVectorClock` (variant A) | Always respected | higher `agent_id` (content-blind) | +| `StructuralCoherenceMerge` (variant B, candidate) | Always respected | higher `τ · (0.5 + 0.5·coherence)` | + +`τ` is `emergent_time::structural_clock::StructuralProperTime::tick(prev_snapshot, snapshot)` — reused directly from the existing crate, not reimplemented. `coherence` is a local f64 cosine-similarity-to-context function, structurally identical to `ruvector-agent-memory::scoring::coherence_score` but operating on the `StateSnapshot` embeddings `StructuralProperTime` already consumes. + +--- + +## Evidence + +**Command**: `cargo run --release -p ruvector-structural-memory-merge --bin structural-memory-merge-bench` +**Hardware**: x86-64 Linux 6.18.5-fc-v20, `rustc 1.94.1`, release profile (`opt-level=3, lto=fat, codegen-units=1, strip=true`). +**Seed**: `0xA6E17` (deterministic; `scenario::tests::deterministic_for_fixed_seed` pins this). +**Corpus**: 2000 concurrent conflicts + 500 causally-ordered control pairs, 6 agents, dim 16. + +| Skew | Policy | Correct-resolution rate | Mean quality regret | Causal violations (of 500) | Merges/sec | +|---|---|---|---|---|---| +| 0ms | LwwWallClock | 50.3% | 0.1752 | 0 | 13,980,367 | +| 0ms | LwwVectorClock | 50.6% | 0.1716 | 0 | 13,623,369 | +| 0ms | **StructuralCoherenceMerge** | **86.8%** | **0.0162** | 0 | 4,324,119 | +| 400ms | LwwWallClock | 48.5% | 0.1784 | **19** | 12,098,078 | +| 400ms | LwwVectorClock | 50.6% | 0.1716 | 0 | 12,153,589 | +| 400ms | **StructuralCoherenceMerge** | **86.8%** | **0.0162** | 0 | 4,338,379 | +| 2000ms | LwwWallClock | 48.6% | 0.1796 | **171** | 12,348,027 | +| 2000ms | LwwVectorClock | 50.6% | 0.1716 | 0 | 12,218,791 | +| 2000ms | **StructuralCoherenceMerge** | **86.8%** | **0.0162** | 0 | 4,285,081 | + +Acceptance check at the pre-registered 400ms realistic-skew condition, printed by the binary itself: + +``` +- beats LwwWallClock by >=10pp: true (+38.3pp) +- beats LwwVectorClock by >=10pp: true (+36.2pp) +- zero causal-order violations (vclock & structural): true (vclock=0, structural=0) +- throughput within 5x of wall-clock baseline: true (4338379 vs 12098078 merges/sec) + +VERDICT: ACCEPT +``` + +**Honest caveat on the margin** (this is load-bearing, not decoration): both LWW baselines are *structurally incapable* of reading write content, so they sit at ≈50% (chance) by construction on a two-way choice — that is not a weak baseline being beaten unfairly, it is the actual behavior of the two most common real-world defaults (naive LWW and vector-clock LWW), but it does mean the reported +36–38pp margin should be read as "content-aware beats content-blind under this noise model," not as evidence `StructuralCoherenceMerge` is close to any information-theoretic ceiling. No ablation against coherence-only or τ-only variants was run this cycle (see "Open Questions"). + +**Wall-clock skew directly causes causal-order violations**, and the effect scales with skew magnitude: 0 at 0ms, 19/500 (3.8%) at 400ms, 171/500 (34.2%) at 2000ms. `LwwVectorClock` and `StructuralCoherenceMerge` have **zero** violations at every skew level tested, by construction (they consult the vector clock before ever considering wall time or structural score). + +**Build/test status**: `cargo build --release -p ruvector-structural-memory-merge` clean; `cargo test --release -p ruvector-structural-memory-merge` — 10/10 passed; `cargo clippy --release -p ruvector-structural-memory-merge --all-targets` — clean (no warnings after two style fixes: `derive(Default)`, collapsed `if`/`else if`). Release binary size: 390,672 bytes (stripped). + +--- + +## Consequences + +**Positive**: +- RuVector gains a second real integration point for `emergent-time` beyond its own crate (the first, self-contained, use was the drift-to-failure early-warning benchmark in `emergent-time` itself) — evidence that structural time is a reusable primitive, not a one-off. +- Establishes an honest experimental pattern (hidden ground truth, independent noise channels, a causally-ordered control group) for testing any future "smarter conflict resolution" claim in this codebase, reducing the risk of circular benchmarks. +- Directly serves the "swarm memory" and "agent operating system" long-horizon theses in `CLAUDE.md`'s ecosystem map. + +**Negative / costs**: +- `StructuralCoherenceMerge` is ~2.8–3.2× slower per resolution than either LWW baseline (still >4.2M resolutions/sec in this microbenchmark, so not a practical bottleneck at plausible RuVector agent-memory write rates, but non-zero). +- Requires every write to carry a `StateSnapshot` (5 extra scalars + an embedding) and a `VectorClock` (one `u64` per known agent) — more metadata than a bare timestamp. +- Vector clocks grow with the number of distinct agents that have ever written to a namespace; this PoC does not implement pruning/compaction of stale agent entries (a known, standard vector-clock cost, not unique to this design). + +--- + +## Alternatives + +1. **Pure vector-clock LWW with a smarter tiebreak** (e.g. random, round-robin) instead of agent-id — rejected for evaluation here because any metadata-only tiebreak is, by the nature of "genuinely concurrent," blind to content and will sit at ≈50% correct-resolution on this benchmark's two-way choice; agent-id was chosen as a representative, deterministic instance of that whole family rather than re-testing several equivalent-in-expectation variants. +2. **CRDT-style merge (keep both writes, let the reader reconcile)** — avoids picking a "loser" entirely. Legitimate for some data types (e.g. counters, sets) but not for a single scalar memory slot where the RuVector agent-memory model expects one active value per key; flagged as a real alternative worth its own nightly if RuVector's memory model moves towards multi-value registers. +3. **Coherence-only (no τ) or τ-only (no coherence) tiebreak** — not built this cycle; see "Open Questions." +4. **Full CRDT libraries (e.g. `automerge`-style)** — rejected as out of scope: RuVector's memory model is single-writer-per-key by convention, and pulling in a general CRDT engine is a much larger dependency and design commitment than this PoC's question warrants. + +--- + +## Implementation Plan / API Shape + +Already implemented in this PR (`crates/ruvector-structural-memory-merge`): + +```rust +pub trait MergePolicy { + fn name(&self) -> &'static str; + fn resolve(&self, a: &MemoryWrite, b: &MemoryWrite, context: &[Vec]) -> Decision; +} + +pub struct MemoryWrite { + pub agent_id: AgentId, + pub key: MemoryKey, + pub wall_ts_ms: f64, + pub vclock: VectorClock, + pub prev_snapshot: StateSnapshot, // emergent_time::structural_clock::StateSnapshot + pub snapshot: StateSnapshot, +} + +pub struct Decision { + pub winner: Winner, + pub reason: &'static str, + pub tau_a: f64, + pub tau_b: f64, + pub causal_order: CausalOrder, +} +``` + +No feature flags were needed — the crate has no optional functionality and no unsafe code. + +--- + +## Security / Governance + +- Pure computation over caller-supplied data; no I/O, no network, no filesystem access, no unsafe blocks. +- `Decision` records `reason` and both `τ` values, which is enough to *audit* a merge after the fact, but this PoC does **not** implement tamper-evidence (no hash chaining). Production hardening should route `Decision` records through `ruvector-proof-gate` (writes) and/or `ruvector-retrieval-receipt` (reads) rather than reimplementing witness chaining here — this ADR explicitly does not claim tamper-evidence. +- `VectorClock` growth is unbounded in agent count; a production integration must define a pruning/GC policy (standard vector-clock operational concern) before this ships on a long-lived namespace. + +--- + +## Failure Modes + +- If an agent's `StateSnapshot` is fabricated or manipulated by a malicious participant, `StructuralCoherenceMerge` can be steered to prefer that agent's writes — this PoC assumes honest-but-uncoordinated agents, not Byzantine ones. A Byzantine-robust variant is out of scope here. +- If `context` (the coherence comparison window) is empty or degenerate, `StructuralCoherenceMerge` falls back to pure `τ` comparison (`coherence_score` returns `0.0`), which is a graceful but less-informed degradation, not a crash. +- Two writes with identical `τ` and `coherence` scores fall through to whichever compares `>=` first (`Winner::A`) — a silent, deterministic tiebreak, not a panic. + +--- + +## Migration / Rollback + +- Additive only: new crate, new workspace member line, no changes to any existing crate's public API or behavior. Rollback is `git revert` of this PR with no other side effects. +- Not wired into `ruvector-agent-memory`, MCP, or ruFlo in this PR — this is a PoC-level integration, not a production migration. See "Practical Applications" in the nightly README for the concrete next step (an `ruvector-agent-memory` feature flag exposing `StructuralCoherenceMerge` as an optional multi-writer mode). + +--- + +## Rejection Criteria (for this ADR, going forward) + +This design should be reconsidered / reverted if a future nightly or production integration shows: +- The correct-resolution advantage collapses (e.g. below LwwVectorClock) on a *realistic* (not synthetic) multi-agent memory corpus. +- Vector-clock metadata growth becomes a measured production cost RuVector cannot amortize. +- A Byzantine or adversarial multi-agent setting is in scope, where this design's honest-agent assumption is violated. + +--- + +## Open Questions + +1. **Ablation**: does `τ` alone, or `coherence` alone, capture most of the win, or is the product genuinely better than either signal? Not measured this cycle. +2. **Real corpus**: this PoC's ground truth is synthetic-by-construction (a hidden `alpha` driving correlated noisy observables). A follow-up should validate against a real multi-agent memory trace (e.g. from an actual ruFlo swarm run) if one becomes available. +3. **Byzantine robustness**: what does `StructuralCoherenceMerge` do under an adversarial agent deliberately inflating its own `τ`/`coherence`? Not addressed; flagged as a hard constraint before any production use outside a trusted-agent-set. diff --git a/docs/research/nightly/2026-08-14-structural-time-memory-merge/README.md b/docs/research/nightly/2026-08-14-structural-time-memory-merge/README.md new file mode 100644 index 0000000000..0b89f6baf5 --- /dev/null +++ b/docs/research/nightly/2026-08-14-structural-time-memory-merge/README.md @@ -0,0 +1,502 @@ +# Structural-Time Conflict Resolution for Concurrent Multi-Agent Memory + +**150-char summary:** Structural-proper-time + coherence resolves concurrent multi-agent memory write conflicts at 86.8% vs ~50% for wall/vector-clock LWW, 0 causal violations. + +**Date:** 2026-08-14 +**Crate:** `crates/ruvector-structural-memory-merge` +**ADR:** [ADR-305](../../../adr/ADR-305-structural-time-memory-merge.md) +**Status:** ACCEPT (see Acceptance below) — experimental PoC, not yet wired into production `ruvector-agent-memory` + +--- + +## Abstract + +RuVector's agent-memory nightlies so far (`2026-06-13-temporal-coherence-agent-memory`, +`2026-06-14-agent-memory-compaction`) address a single agent managing its own memory over time. +None address what happens when **multiple agents share a memory namespace and write concurrently** +— the situation any RuVector-based agent swarm (ruFlo workers, Cognitum edge fleets) will hit as +soon as more than one autonomous process can update the same belief. + +The default answer in distributed systems, last-write-wins (LWW) by wall-clock timestamp, has two +known weaknesses: it is vulnerable to clock skew between unsynchronized agents, and it is +semantically blind — it can never prefer a more valuable write over a less valuable one, only a +later one. + +This nightly connects two RuVector primitives that had never been composed before: `emergent-time`'s +`StructuralProperTime` clock (state-change magnitude across embedding/entropy/graph/coherence/ +prediction-error channels) and `ruvector-agent-memory`'s coherence-scoring concept. The result, +`StructuralCoherenceMerge`, never overrides genuine causal order (a vector clock still decides +happens-before pairs) but for **genuinely concurrent** conflicts, picks the write with the larger +structural shift weighted by coherence with the current shared context — instead of an arbitrary, +content-blind tiebreak. + +**Key measured result** (`cargo run --release -p ruvector-structural-memory-merge`, seed +`0xA6E17`, 2000 concurrent conflicts + 500 causal-control pairs, 400ms realistic per-agent clock +skew): + +| Policy | Correct-resolution rate | Causal-order violations | +|---|---|---| +| `LwwWallClock` | 48.5% | 19 / 500 | +| `LwwVectorClock` | 50.6% | 0 / 500 | +| **`StructuralCoherenceMerge`** | **86.8%** | **0 / 500** | + +**Hardware:** x86-64, Linux 6.18.5-fc-v20, `rustc 1.94.1`, release build +(`opt-level=3, lto=fat, codegen-units=1, strip=true`). + +--- + +## Hypothesis + +```text +Given 2000 genuinely concurrent (causally-unordered) synthetic memory-write conflicts +across 6 agents, each write carrying a hidden ground-truth quality label never exposed +to any policy, + +when conflicts are resolved by StructuralCoherenceMerge instead of LwwWallClock (400ms +per-agent clock skew) or LwwVectorClock (agent-id tiebreak), + +then its correct-resolution rate should exceed both baselines by >= 10 percentage points, + +subject to zero causal-order violations on a 500-case causally-ordered control set, and +merge throughput within 5x of the wall-clock baseline. +``` + +This hypothesis was fixed in `src/main.rs`'s doc comment *before* the benchmark was run and was +not modified afterwards. + +--- + +## Why This Matters Now (2026) + +RuVector's own `CLAUDE.md` names "agent operating systems," "swarm memory," and "edge cognition" +as target domains. As soon as more than one agent process (ruFlo workers, a Cognitum edge fleet, +independent MCP clients) can write to the same memory namespace, *something* has to arbitrate +conflicts, and today RuVector has no answer beyond whatever the caller's key-value store happens +to do by default (almost always wall-clock LWW). This nightly gives RuVector a measured, +content-aware alternative and — just as importantly — a measured demonstration of how often the +naive default actually gets the causal order wrong under realistic clock skew (3.8% of pairs at +400ms skew, 34.2% at 2000ms). + +## Why It Could Matter in 2036 + +Multi-agent systems with no central sequencer (edge swarms, offline-first agents, satellite/robot +fleets with intermittent connectivity) will be common. A causally-correct, content-aware default +for "whose belief wins" is infrastructure those systems will need regardless of which vector +database or agent framework wraps it. + +## Why It Could Matter in 2046 + +If autonomous multi-agent systems become long-lived (systems that outlive their original +operators), the mechanism by which they resolve disagreements about shared state stops being an +implementation detail and becomes a governance question. A transparent, auditable +(`Decision.reason`, `tau_a`, `tau_b`) resolution rule is a small but genuine building block for +that future — one an operator, or another agent, can inspect after the fact. + +--- + +## RuVector Ecosystem Fit + +| Theme | Connection | +|---|---| +| `emergent-time` | `StructuralProperTime` reused directly (not reimplemented) as the concurrent-write tiebreak signal — its second real use in the workspace | +| `ruvector-agent-memory` | Coherence-scoring concept extended from single-agent ranking to multi-agent conflict resolution | +| `ruvector-proof-gate` / `ruvector-retrieval-receipt` | Named as the correct home for hardening `Decision` records into tamper-evident witness entries (not reimplemented here — see ADR-305 Security section) | +| ruFlo | Natural trigger: a ruFlo swarm-memory-maintenance workflow could call `MergePolicy::resolve` whenever a namespace write conflict is detected | +| MCP | A narrow `memory_merge_resolve(key, agent_a, agent_b)` tool is a plausible, narrow MCP surface (see "MCP Surface" below) | +| Edge / WASM | Crate has exactly one dependency (`emergent-time`, itself zero-dependency); release binary is 390,672 bytes | +| RVF | A namespace's accumulated `VectorClock` + latest `Decision` log is a natural RVF-portable artifact (see "RVF Integration") | + +### MetaHarness / Flywheel / Darwin Availability (verified, not assumed) + +- `npx metaharness --help` — **installed and available** (v0.4.5, pulled fresh this run). Its subcommands (`score`, `analyze`, `genome`, `learn`, `proxy`) are for scaffolding/scoring *new* agent harnesses, not for orchestrating this kind of in-repo Rust research cycle, so it was not invoked as a control-plane for this nightly. +- `npx ruvector harness doctor --json` / `darwin` / `flywheel` — **not resolvable** in this environment (`npm error: could not determine executable to run`). No such CLI package is installed here. Per the nightly instructions' own rule ("do not assume a package exists solely because it appears in the prompt — verify first"), the Darwin/Flywheel *steps* in this cycle were therefore executed as their manual equivalent: a documented three-arm comparison (baseline / variant A / variant B) with a pre-registered fitness rule and hard acceptance gate, directly in the Rust benchmark binary and this document, rather than through a nonexistent CLI. No Darwin generations/mutations were run because there is only one candidate design (`StructuralCoherenceMerge`) in this cycle, not a population to evolve — see "Darwin" below. + +--- + +## Architecture + +```mermaid +flowchart TD + subgraph Agents["Concurrent agents (no shared clock)"] + A1[Agent A write] + A2[Agent B write] + end + + A1 -->|VectorClock much| VC{Causal order?} + A2 -->|VectorClock much| VC + + VC -->|happens-before / after| KEEP[Keep the causally-later write] + VC -->|Concurrent| SC[StructuralCoherenceMerge] + + SC --> TAU["tau = StructuralProperTime.tick(prev_snapshot, snapshot)
(embedding, entropy, graph, coherence, pred_error deltas)"] + SC --> COH["coherence = cosine(embedding, context window)"] + TAU --> SCORE["score = tau * (0.5 + 0.5 * coherence)"] + COH --> SCORE + SCORE --> WINNER[Winner = higher score] + + KEEP --> LOG[Decision: winner, reason, tau_a, tau_b, causal_order] + WINNER --> LOG + LOG -.future hardening.-> WITNESS[ruvector-proof-gate / retrieval-receipt witness chain] +``` + +--- + +## Implementation + +Four files, 936 lines total (none over 500), one external dependency (`emergent-time`, itself +zero-dependency): + +- `src/vclock.rs` (119 lines) — minimal Lamport-style `VectorClock` with `happens-before` / + `concurrent` comparison. +- `src/scenario.rs` (329 lines) — deterministic synthetic-corpus generator. See "Benchmark + Methodology" for how it avoids leaking ground truth into the algorithm under test. +- `src/lib.rs` (296 lines) — `MemoryWrite`, `Decision`, the `MergePolicy` trait, and the three + policies (`LwwWallClock`, `LwwVectorClock`, `StructuralCoherenceMerge`). +- `src/main.rs` (192 lines) — the benchmark binary; prints the tables reproduced above and the + machine-readable `ACCEPT`/`REJECT`/`INCONCLUSIVE` verdict. + +```rust +pub trait MergePolicy { + fn name(&self) -> &'static str; + fn resolve(&self, a: &MemoryWrite, b: &MemoryWrite, context: &[Vec]) -> Decision; +} +``` + +`StructuralCoherenceMerge::resolve` first checks `a.vclock.compare(&b.vclock)`: if one write +causally precedes the other, the later one always wins — the structural/coherence score is used +**only** to break ties among writes with `CausalOrder::Concurrent` (or `Equal`). + +--- + +## Benchmark Methodology + +**Ground truth without leakage.** Each synthetic write carries a hidden `alpha` (a structural +shift magnitude towards a drifting "ideal" target) that no policy ever observes. `alpha` drives +three *independently noised* observable channels: + +1. The `StateSnapshot` (embedding/entropy/graph/coherence/pred_error deltas) that + `StructuralProperTime` consumes. +2. The coherence context window — sampled around the same ideal point with a **separate** noise + draw from the write's own embedding. +3. `wall_ts_ms`, corrupted by a persistent per-agent skew bias. + +`true_quality = alpha + independent noise` is the evaluation label, read only by the benchmark +harness, never by a `MergePolicy`. This is what keeps "correct-resolution rate" from measuring the +algorithm against its own signal (the reward-hacking failure mode the nightly process is required +to guard against): `coherence_score` and `tau` are noisy, imperfect proxies for `alpha`/`quality`, +not restatements of it. + +**Causally-ordered control set.** A separate 500-pair set is constructed so that `b`'s vector clock +provably observes-and-follows `a`'s (`vc_b = merge(vc_a); vc_b.tick(agent_b)`). The only correct +winner for these pairs is `b`, by construction, independent of any quality signal — this isolates +"does the policy ever override real causal order" from "does the policy pick the better concurrent +write," which are different failure modes and are reported separately. + +**Determinism.** A single xorshift64* PRNG seeded with `0xA6E17` drives the whole corpus; the test +`scenario::tests::deterministic_for_fixed_seed` pins two independent generations to be bit-identical. + +**Reproduce it**: +```bash +cargo test --release -p ruvector-structural-memory-merge # 10/10 tests +cargo clippy --release -p ruvector-structural-memory-merge --all-targets # clean +cargo run --release -p ruvector-structural-memory-merge --bin structural-memory-merge-bench +``` + +--- + +## Benchmark Results (full) + +| Skew | Policy | Correct-resolution rate | Mean quality regret | Causal violations (of 500) | Merges/sec | +|---|---|---|---|---|---| +| 0ms | LwwWallClock | 50.3% | 0.1752 | 0 | 13,980,367 | +| 0ms | LwwVectorClock | 50.6% | 0.1716 | 0 | 13,623,369 | +| 0ms | **StructuralCoherenceMerge** | **86.8%** | **0.0162** | 0 | 4,324,119 | +| 400ms | LwwWallClock | 48.5% | 0.1784 | **19** | 12,098,078 | +| 400ms | LwwVectorClock | 50.6% | 0.1716 | 0 | 12,153,589 | +| 400ms | **StructuralCoherenceMerge** | **86.8%** | **0.0162** | 0 | 4,338,379 | +| 2000ms | LwwWallClock | 48.6% | 0.1796 | **171** | 12,348,027 | +| 2000ms | LwwVectorClock | 50.6% | 0.1716 | 0 | 12,218,791 | +| 2000ms | **StructuralCoherenceMerge** | **86.8%** | **0.0162** | 0 | 4,285,081 | + +Two effects worth separating: + +1. **Correct-resolution rate is flat across skew** for every policy — skew doesn't change *which* + write is more valuable, only which timestamp looks larger. `StructuralCoherenceMerge` and + `LwwVectorClock` never even consult wall time for concurrent pairs, so they're unaffected by + construction; `LwwWallClock`'s correct-resolution rate hovers at chance regardless of skew + because it was already at chance at zero skew (it is content-blind, not skew-sensitive, on + *this* metric). +2. **Causal-order violations scale directly with skew** for `LwwWallClock` (0 → 19 → 171 of 500 as + skew goes 0 → 400ms → 2000ms) and are **zero at every skew level** for the other two policies — + this is the skew-sensitive failure mode, and it is a different metric from correct-resolution + rate. + +## Acceptance + +``` +- beats LwwWallClock by >=10pp: true (+38.3pp) +- beats LwwVectorClock by >=10pp: true (+36.2pp) +- zero causal-order violations (vclock & structural): true (vclock=0, structural=0) +- throughput within 5x of wall-clock baseline: true (4,338,379 vs 12,098,078 merges/sec, 2.8x) + +VERDICT: ACCEPT +``` + +--- + +## Memory Math + +Per tracked write: one `StateSnapshot` (`Vec` embedding of dimension *d*, + 4 `f64` scalars = +`8*(d+4)` bytes) and one `VectorClock` (`BTreeMap`, ~24 bytes/entry, one entry per agent +that has ever written to the namespace it participates in). At the benchmark's `d=16`, that's 160 +bytes of snapshot per write side, plus a few hundred bytes of vector-clock map at 6 agents — this +PoC does not implement vector-clock pruning; a production namespace with many short-lived agent +identities would need one (see ADR-305 Failure Modes). + +## Performance Math + +`StructuralCoherenceMerge` does two `StructuralProperTime::tick` calls (each an L2 distance over +the embedding plus four scalar diffs) and two coherence scans (each `O(context_len)` cosine +similarities) per concurrent resolution — `O(d + context_len)` vs. `O(1)` for either LWW baseline. +At `d=16`, `context_len=3` this is the measured 2.8x throughput cost. Both remain far above any +plausible RuVector agent-memory write rate (millions vs. thousands of writes/sec). + +--- + +## Failure Modes + +See ADR-305 "Failure Modes" for the full list (malicious/Byzantine snapshot fabrication, empty +context degradation, deterministic tie-break on exact score ties). None of these were exercised as +adversarial tests in this cycle — flagged as follow-up work, not resolved here. + +## Rejected Alternatives + +1. Smarter metadata-only tiebreaks (random, round-robin) instead of agent-id for + `LwwVectorClock` — not separately tested; any metadata-only rule is content-blind by + definition and should sit at ≈50% on this benchmark's two-way choice regardless of which + specific rule is used. +2. CRDT multi-value registers (keep both writes) — legitimate for some data types, rejected as + out of scope for RuVector's current single-value-per-key agent-memory model. +3. τ-only or coherence-only tiebreak (no product) — not built this cycle; see "Next Research." +4. Full external CRDT library — rejected as disproportionate to the question being asked. + +--- + +## Security + +Pure computation, no I/O, no unsafe code, no external input parsing. `VectorClock` growth is +unbounded in distinct-agent count (standard vector-clock cost, not unique to this design) and is +not addressed here. See ADR-305 "Security / Governance." + +## Governance + +`Decision.reason` + `tau_a`/`tau_b` + `causal_order` give a human-auditable trail for *why* a +write won, which is a governance-relevant property this design gets close to for free — but it is +not tamper-evident. Production use should route decisions through `ruvector-proof-gate` / +`ruvector-retrieval-receipt` rather than trusting an in-process log. + +## MCP Implications + +A narrow, read-mostly tool is plausible: + +| Field | Value | +|---|---| +| Tool name | `memory_merge_resolve` | +| Inputs | `key`, two candidate writes (or their agent ids + a lookup), context window | +| Outputs | `winner`, `reason`, `tau_a`, `tau_b`, `causal_order` | +| Authority | Read-only computation; does not itself mutate the memory store | +| Side effects | None (caller applies the decision) | +| Witness behavior | None in this PoC — recommend logging through `ruvector-proof-gate` at the call site | +| Error behavior | Malformed snapshot (`NaN`/mismatched dims) → typed error, no partial write | + +Not implemented in this PR — this is a design note for a follow-up, per Step 30 of the nightly +process ("prefer narrow tools over broad arbitrary execution"). + +## WASM / Edge Implications + +Zero unsafe code, one dependency (itself zero-dependency), 390,672-byte stripped native release +binary. Not yet built for `wasm32` in this cycle — no deployment claim is made beyond "the +dependency graph does not obviously block it," which is a lower bar than an actual measured WASM +build/run. + +## RVF Integration Analysis + +A namespace's `VectorClock` plus its rolling `Decision` log is a plausible RVF-portable unit: +moving an agent (or a whole swarm's shared-memory namespace) between devices would carry both +"what has been observed" (the clock) and "what was decided and why" (the log) — deterministic +replay of the log against the clock is exactly the kind of copy-on-write, signed-lineage artifact +RVF targets. Not implemented; this is an analysis, not a claim of integration. + +## RVM Integration Analysis + +If `StructuralCoherenceMerge` is exposed to multiple mutually-untrusted agents (not this PoC's +honest-agent assumption), RVM-style capability boundaries would be the right place to enforce that +an agent cannot fabricate an artificially large `τ`/`coherence` for its own writes — i.e. RVM would +need to attest the `StateSnapshot` came from the agent's real state transition, not a chosen one. +Not addressed here; flagged as a hard prerequisite for any adversarial deployment. + +## ruFlo Integration Analysis + +Concrete workflow: a ruFlo "shared-memory-maintenance" job subscribes to write-conflict events on +a namespace, calls `MergePolicy::resolve` (via the future MCP tool above or a direct crate +dependency), and applies the decision — replacing whatever ad hoc LWW the underlying store does by +default today. This is the most direct, low-risk path to production use of this nightly's result. + +--- + +## Competitor Comparison + +| System | Documented external capability | Directly measured here | RuVector architectural difference | +|---|---|---|---| +| Redis (LWW / CRDT modules) | LWW by default; CRDT modules available | Not measured (different system) | This PoC's structural signal is agent-memory-specific (embedding+entropy+graph+coherence), not a generic CRDT | +| Automerge / Yjs (CRDT frameworks) | Multi-value/OT-based merge, no single "winner" | Not measured | RuVector's single-value-per-key model is a deliberate simplification, not a claimed improvement | +| Milvus / Qdrant / Weaviate / Pinecone / LanceDB / FAISS / pgvector / Chroma / Vespa | None document a multi-writer conflict-resolution mechanism for the same vector key | Not applicable — this is a gap none of them fill, not a head-to-head benchmark | RuVector is not claiming to beat these systems here; this nightly addresses a problem outside their documented scope | + +No performance-victory claim is made against any of the above — the comparison set here is the +two generic distributed-systems defaults (wall-clock LWW, vector-clock LWW), which is what this +PoC actually measured against. + +--- + +## Practical Applications + +1. **Multi-agent belief reconciliation** — a swarm of research agents (ruFlo workers) updating a + shared "current best answer" memory slot; business value: fewer stale/contradictory agent + outputs; main risk: honest-agent assumption; horizon: near-term. +2. **Edge sensor fleets with intermittent connectivity** — Cognitum Seed devices reporting + overlapping observations of the same entity; value: correct reconciliation without NTP; + risk: vector-clock growth with fleet size; horizon: near-term. +3. **Federated agent memory across organizations** — no single party is a trusted sequencer; + value: causally-correct merge without a central authority; risk: requires the RVM Byzantine + hardening noted above before cross-org use; horizon: mid-term. +4. **Multi-model ensemble memory** — several LLM agents (different models) updating a shared + scratchpad; value: prefers the more contextually relevant update; risk: coherence signal + quality depends on embedding model consistency; horizon: near-term. +5. **Robotics fleet shared world-model** — multiple robots updating a shared map/belief state; + value: causal correctness matters physically (a robot must never act on a causally-stale + belief); risk: real-time latency budget vs. the 2.8x overhead; horizon: mid-term. +6. **Code-intelligence agent swarms** — multiple coding agents updating a shared "current + understanding of this module" memory; value: avoids one agent's stale summary overwriting + another's fresher, more relevant one; risk: none specific; horizon: near-term. +7. **Security/anomaly retrieval across sensors** — multiple detectors writing candidate anomaly + explanations to a shared key; value: prefers structurally significant, contextually relevant + explanations; risk: adversarial detector could game τ; horizon: mid-term (needs RVM hardening). +8. **Scientific multi-agent search** — parallel literature-search agents updating a shared + "current hypothesis" memory; value: same pattern as #1 applied to research workflows; + risk: none specific; horizon: near-term. + +## Long Horizon Applications + +1. **Swarm memory as a first-class RuVector primitive** — thesis: multi-writer conflict + resolution becomes as fundamental to RuVector as HNSW is today; required advances: production + hardening, ablations, adversarial robustness; RuVector role: reference implementation; + uncertainty: whether structural time remains the right signal at scale; falsification: a + simpler signal matches it on a real corpus. +2. **Causally-consistent world models for embodied agents** — thesis: physical multi-agent + systems need causally-correct shared state as a safety property, not just a quality one; + required advances: real-time bounds, RVM Byzantine hardening; RuVector role: the memory + substrate; uncertainty: real-time overhead at fleet scale; falsification: overhead exceeds + robotics control-loop budgets. +3. **Agent operating systems with governed shared state** — thesis: an "agent OS" needs a kernel + primitive for concurrent-write arbitration, analogous to what LWW is for key-value stores today; + required advances: MCP tool, RVM enforcement; RuVector role: providing that kernel primitive; + uncertainty: whether one arbitration policy generalizes across domains; falsification: different + domains need incompatible policies. +4. **Synthetic nervous systems** — thesis: `emergent-time`'s structural-time framing (already + used for early-warning detection in its own crate) generalizes to conflict arbitration as shown + here, suggesting one clock primitive can serve multiple agentic-infrastructure roles; + uncertainty: whether this generalizes beyond memory conflicts; falsification: other proposed + uses of structural time fail their own hypotheses. +5. **Self-healing distributed memory** — thesis: causally-correct, content-aware merge is a + building block for memory stores that repair inconsistency automatically rather than requiring + manual reconciliation; required advances: automatic conflict detection, not just resolution; + RuVector role: the resolution half of that pipeline; uncertainty: detection is unsolved here; + falsification: detection costs dominate resolution savings. +6. **Proof-gated autonomous infrastructure** — thesis: combining this nightly's `Decision` audit + trail with `ruvector-proof-gate`'s witness chains produces infrastructure where *why* a shared + belief changed is provably reconstructable; required advances: the integration itself; + RuVector role: both halves already exist separately; uncertainty: none major; falsification: + integration proves impractical at write volume. +7. **RVM coherence domains for multi-tenant agent memory** — thesis: different trust domains + sharing a memory substrate need enforcement, not just a good default policy; required advances: + RVM integration (see above); RuVector role: RVM already exists as a target; uncertainty: scope + of enforcement needed; falsification: honest-agent assumption turns out sufficient in practice. +8. **Portable, replayable agent lineage (RVF)** — thesis: an agent's contribution to shared memory + becomes a portable, replayable artifact, not just a local side effect; required advances: RVF + integration (see above); RuVector role: RVF already exists as a target; uncertainty: replay + determinism at scale; falsification: replay diverges from live execution under load. + +--- + +## Evolution Results (Darwin) + +Not run as a generational search this cycle: this nightly compared exactly three fixed policies +(one baseline, two variants), not a population to mutate. No `ruvector harness darwin` CLI is +installed in this environment (verified, see "MetaHarness / Flywheel / Darwin Availability" +above), so no evolutionary loop was executed. The natural Darwin extension — evolving the +`StructuralMetric` channel weights (`w_embedding, w_entropy, w_graph, w_coherence, w_pred_error`) +against the correct-resolution-rate fitness — is flagged as the concrete next experiment (below), +not attempted here, to keep this cycle's claim limited to what was actually measured with the +crate's `StructuralMetric::default()` weights. + +## Promotion Decision + +**Promote the crate as an experimental, non-default addition to the workspace** (this PR). +**Do not** promote `StructuralCoherenceMerge` as a default multi-writer policy inside +`ruvector-agent-memory` yet — that requires the ablation and adversarial-robustness follow-ups +listed above, consistent with ADR-305's "Rejection Criteria." + +## Witness Evidence + +- Starting commit: `74d2a6017` (branch `claude/focused-darwin-1je9ll`, `origin/main` at run time). +- Hardware/toolchain: x86-64 Linux 6.18.5-fc-v20, `rustc 1.94.1`, `cargo 1.94.1`. +- Exact benchmark command and raw output are reproduced verbatim in "Benchmark Results" above — + no numbers in this document were hand-edited after the run. +- No cryptographic witness chain was generated for this PoC (see "Governance" above for why, and + what production hardening would add). + +## Production Path + +1. Ablate `τ`-only vs. coherence-only vs. combined (this cycle's open question #1). +2. Validate against a real ruFlo multi-agent memory trace, not only synthetic data. +3. Add the RVM Byzantine-robustness hardening before any untrusted-multi-tenant deployment. +4. Wire a feature-flagged `multi_writer` mode into `ruvector-agent-memory` once 1–3 are done. +5. Route `Decision` records through `ruvector-proof-gate` for tamper-evidence. + +## Falsification Criteria + +This hypothesis would have been rejected (not merely revised) if the benchmark had shown any of: +`StructuralCoherenceMerge` failing to beat both LWW baselines by ≥10pp; any causal-order violation +by `StructuralCoherenceMerge` or `LwwVectorClock` on the causal-control set; or throughput more +than 5x slower than the wall-clock baseline. None of these occurred — see "Acceptance." + +## Limitations + +Synthetic ground truth (not a real multi-agent trace); no ablation of the two signal components; +no adversarial/Byzantine testing; no WASM build measured; no MCP/ruFlo wiring implemented, only +designed. All stated explicitly above at the relevant section, not held back to a single disclaimer. + +## Next Research + +1. Ablation of `StructuralMetric` channel weights via an actual bounded Darwin search once the + `ruvector harness darwin` tooling is available (or a hand-rolled bounded grid/random search + otherwise), fitness = correct-resolution rate subject to zero causal violations. +2. Real-trace validation against ruFlo swarm memory logs. +3. Byzantine-robustness variant with RVM-attested snapshots. + +## References + +- `crates/emergent-time/src/structural_clock.rs` — `StructuralProperTime`, reused directly. +- `crates/ruvector-agent-memory/src/scoring.rs` — the coherence-scoring pattern this nightly + extends to a multi-agent setting. +- `docs/research/nightly/2026-06-13-temporal-coherence-agent-memory/README.md` — prior, + single-agent nightly this one is explicitly distinct from (temporal decay + graph-coherence + ranking, not multi-writer conflict resolution). +- `docs/research/nightly/2026-06-14-agent-memory-compaction/README.md` — prior, single-agent + compaction nightly, likewise distinct. +- `docs/adr/ADR-227*` (`ruvector-proof-gate`) and + `docs/research/nightly/2026-08-13-retrieval-receipts/README.md` (`ruvector-retrieval-receipt`) + — the witness-chain mechanisms named as the correct home for hardening this PoC's `Decision` + audit trail. +- Lamport, "Time, Clocks, and the Ordering of Events in a Distributed System" (1978) — the vector + clock construction this crate's `LwwVectorClock` baseline and causal-order gate are built on. diff --git a/docs/research/nightly/2026-08-14-structural-time-memory-merge/gist.md b/docs/research/nightly/2026-08-14-structural-time-memory-merge/gist.md new file mode 100644 index 0000000000..d03ad283e1 --- /dev/null +++ b/docs/research/nightly/2026-08-14-structural-time-memory-merge/gist.md @@ -0,0 +1,134 @@ +# Resolving Concurrent Memory Writes Without a Shared Clock + +## Problem + +Any system where more than one autonomous process can write to the same piece of shared state +eventually has to answer: two writes conflict, which one wins? The standard answer — last-write-wins +(LWW) by wall-clock timestamp — quietly assumes synchronized clocks and throws away all information +about *what* was written, keeping only *when*. + +For a Rust-native "agent memory" substrate like RuVector, this stops being a theoretical concern the +moment more than one agent (a ruFlo worker, an edge device, an independent MCP client) can update the +same memory key. Unsynchronized edge devices routinely drift by hundreds of milliseconds to seconds; +under that drift, naive LWW doesn't just occasionally pick the "wrong" write by some fuzzy quality +measure — it can pick a write that is *provably, causally* earlier than the one it discards. + +## Hypothesis + +Two independent RuVector primitives, never previously connected, look like a plausible fix: + +- **`emergent-time`'s structural proper time** — a clock that measures how much a system's state + actually changed (embedding movement, entropy, graph topology, coherence, prediction error), + instead of counting wall-clock ticks. +- **Coherence scoring** — how well a piece of content matches the current context, already used in + `ruvector-agent-memory` to rank a single agent's own memories. + +The hypothesis: for writes that are *genuinely concurrent* (no causal relationship a vector clock +can establish), preferring the write with the larger structural shift, weighted by its coherence +with the current shared context, should recover more of the "actually more valuable" write than +either wall-clock LWW or a vector-clock LWW with an arbitrary tiebreak — while never overriding +real causal order, which the LWW-by-wall-clock approach can do under skew. + +## Technical Design + +`crates/ruvector-structural-memory-merge` implements three `MergePolicy` variants: + +```rust +pub trait MergePolicy { + fn resolve(&self, a: &MemoryWrite, b: &MemoryWrite, context: &[Vec]) -> Decision; +} +``` + +- `LwwWallClock` — winner is whichever write has the larger `wall_ts_ms`. Never checks the vector + clock. +- `LwwVectorClock` — checks the vector clock first; if one write causally precedes the other, the + later one wins unconditionally. Only for genuinely concurrent writes does it fall back to a + tiebreak (here: higher `agent_id`) — a rule that's deterministic but carries no information about + content. +- `StructuralCoherenceMerge` — same causal-order gate as `LwwVectorClock`, but the concurrent-case + tiebreak is `τ · (0.5 + 0.5·coherence)`, where `τ` is `StructuralProperTime::tick(prev, cur)` on + each write's five-channel state snapshot, and `coherence` is cosine similarity between the new + write's embedding and the current shared context window. + +The causal-order gate is the load-bearing design choice: structural time is used *only* to break +ties among writes a vector clock cannot order, never to override a real happens-before relationship. + +## Honest Evaluation Design + +The hardest part of this kind of benchmark is avoiding circularity — if "correct" is defined in +terms of the same signal the algorithm uses, the algorithm cannot lose. This PoC avoids that by +generating a hidden per-write `alpha` (a structural-shift magnitude towards a drifting, unobserved +"ideal" target) that drives three *independently noised* observable channels: the structural +snapshot, a separately-sampled coherence context window, and a skewed wall-clock timestamp. +`true_quality = alpha + independent noise` is the evaluation label; no `MergePolicy` implementation +ever sees `alpha` or `true_quality`, only the noisy, imperfect observables. A separate 500-pair +causally-ordered control set, constructed so the correct winner is deterministic by vector-clock +construction (independent of any quality signal), isolates "does this policy ever break real causal +order" from "does this policy pick the better concurrent write" — two different failure modes, +reported separately. + +## Actual Implementation and Evidence + +936 lines across four files (none over 500), one dependency (`emergent-time`, itself dependency-free). Full test +suite: 10/10 passing; `cargo clippy --all-targets`: clean. + +Measured with `cargo run --release -p ruvector-structural-memory-merge` (x86-64 Linux, `rustc +1.94.1`, release profile, seed `0xA6E17`, 2000 concurrent conflicts + 500 causal-control pairs, 6 +agents): + +| Skew | Policy | Correct-resolution rate | Causal violations (of 500) | +|---|---|---|---| +| 400ms | LwwWallClock | 48.5% | 19 | +| 400ms | LwwVectorClock | 50.6% | 0 | +| 400ms | **StructuralCoherenceMerge** | **86.8%** | **0** | + +Both LWW baselines sit at roughly chance (50%) on correct-resolution rate at every skew level +tested (0/400/2000ms) — expected, since they are structurally incapable of reading write content. +`StructuralCoherenceMerge` beats both by 36–38 percentage points while never violating causal order, +at 2.8× the per-resolution cost of either baseline (still >4.2M resolutions/sec). Wall-clock LWW's +causal-order violation count scales directly with injected skew: 0 at 0ms, 19/500 at 400ms, 171/500 +at 2000ms — a direct, measured demonstration of the clock-skew failure mode this design targets. + +Acceptance was pre-registered before the run (`src/main.rs` doc comment): beat both baselines by +≥10pp, zero causal violations, throughput within 5× — all four conditions held. **VERDICT: ACCEPT.** + +## Limitations + +The evaluation corpus is synthetic, not drawn from a real multi-agent memory trace. No ablation was +run to separate how much of the win comes from `τ` versus coherence individually. The design assumes +honest agents — an agent that fabricates its own `StateSnapshot` to inflate `τ` is not defended +against here; that requires RVM-style attestation, not addressed in this cycle. No WASM build was +measured, only argued to be plausible from the dependency graph. None of this is fixed in this PR; +all of it is listed as explicit follow-up work in ADR-305. + +## Production Relevance + +This does not ship as a default in `ruvector-agent-memory` — it ships as a standalone, opt-in crate +with a clear promotion path (ablation → real-trace validation → Byzantine hardening → feature-flagged +integration) documented in ADR-305. The most direct next step is a ruFlo workflow that calls +`MergePolicy::resolve` whenever a shared-memory namespace detects a write conflict, replacing +whatever ad hoc LWW the underlying store does today. + +## RuVector Ecosystem Implications + +This is the second real (non-benchmark-only) use of `emergent-time`'s structural time inside the +workspace, and the first to connect it to the agent-memory subsystem. It gives RuVector a measured, +non-hand-wavy answer to "how does shared agent memory behave when more than one agent can write to +it" — a gap none of the prior agent-memory nightlies addressed, and one every multi-agent RuVector +deployment (ruFlo swarms, edge fleets) will eventually need an answer to. + +## Future Direction + +1. Ablate the `τ` and coherence signal components independently. +2. Validate against a real multi-agent memory trace instead of synthetic ground truth. +3. Add RVM-attested snapshots before any untrusted-multi-tenant deployment. +4. Wire a feature-flagged multi-writer mode into `ruvector-agent-memory`. +5. Route `Decision` audit records through `ruvector-proof-gate` for tamper-evidence. + +## References + +- `crates/emergent-time/src/structural_clock.rs` +- `crates/ruvector-agent-memory/src/scoring.rs` +- Lamport, L. "Time, Clocks, and the Ordering of Events in a Distributed System." *Communications + of the ACM*, 1978. +- `docs/adr/ADR-305-structural-time-memory-merge.md` (full evidence and acceptance record)