docs: add ADR-305 and nightly research writeup for mincut-partitioned memory consolidation

Documents the rejected hypothesis (worst-cluster recall gain 0.00pp
against a pre-declared 15pp threshold, at both n=4000 and n=500, and
across a bounded floor_min sweep), the partial positive signal (4/6
clusters individually gained 15-23pp, overall recall +6.8pp), and the two
independently reproducible ruvector-mincut defects discovered along the
way (partition()/min_cut_value() inconsistency + nondeterminism;
GraphPartitioner vertex loss/fabrication + severe latency). Raw benchmark
output preserved under evidence/.

Co-Authored-By: claude-flow <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01KrAfJLv2U99vvEvQqJ1Zoo
This commit is contained in:
Claude 2026-08-17 08:16:57 +00:00
parent 5e4d1881db
commit 3bec308ed1
No known key found for this signature in database
7 changed files with 1016 additions and 0 deletions

View file

@ -0,0 +1,256 @@
# ADR-305: Mincut-Partitioned Agent-Memory Consolidation
## Status
**Proposed, hypothesis REJECTED by measurement.** Experimental crate
(`ruvector-partition-memory`), not wired into any production compaction
path. Retained for its evidence, its two documented `ruvector-mincut`
defects, and its from-scratch correct min-cut implementation
(`mincut_exact.rs`), which is a candidate for reuse in future partitioning
work regardless of this ADR's own outcome.
## Context
Nightly 2026-06-14 (`crates/ruvector-agent-memory`) introduced
`CoherencePolicy`: a global top-score compaction rule scoring every stored
memory by `α·recency + β·frequency + γ·coherence(context)` and keeping the
top `target_size`. It measured 100% recall after 50% compaction on its
test corpus and remains the best-performing policy in the ecosystem.
`CoherencePolicy` is, by construction, a single global ranking scored
against one context window (in production, the agent's most recent working
context). That is also its structural risk: a memory topic unrelated to
the current context competes on the same scale as everything else, so a
minority topic can be evicted **in full** during a single consolidation
event, at a compaction ratio the aggregate recall number reports as
favorable. Nightly 2026-06-14 did not measure this — it reports mean
recall and LRU/LFU comparisons, not worst-topic behavior.
`ruvector-mincut` provides a subpolynomial dynamic minimum-cut engine
(JinSunThorup) and graph-partitioning utilities
(`GraphPartitioner`, `RuVectorGraphAnalyzer`) that had not previously been
applied to agent memory. This ADR's premise: partitioning the memory
similarity graph before applying a retention budget, with a
per-partition floor, should protect a topic from being evicted in full
even when it loses on the global score — because that topic's competition
for its floor allocation is only the rest of its own partition, not the
whole corpus.
## Hypothesis
```text
Given a 4,000-memory corpus with 6 semantic clusters of unequal size
(1400/1000/720/600/200/80 — the two smallest are 5% and 2% of the corpus),
scored against a recency-biased context drawn from the largest cluster
(the realistic "what the agent was just working on" scenario),
when a partition-aware retention policy (floor + proportional budget per
graph partition) is used instead of CoherencePolicy's global top-score
ranking, at 50% compaction,
then the best candidate's worst-cluster recall@10 should exceed the
baseline's by >= 15 percentage points,
subject to: no candidate's overall recall@10 regressing more than 5pp
below baseline; partition+retention wall time staying under 30s per
candidate at this n; and the partition witness chain verifying.
```
Declared before the accepted run (see the research doc's Pass 2/3 and
calibration section); not modified afterward.
## Decision
Implement two partitioning strategies and compare both against the
`CoherencePolicy` baseline, using **the same scorer** in every retention
step so the only independent variable is budget allocation, not scoring:
- **Candidate A — `MincutFixedK`**: wraps the existing
`ruvector_mincut::GraphPartitioner` (unweighted, edge-count recursive
bisection, fixed `K`).
- **Candidate B — `MincutAdaptive`**: a new adaptive-depth recursive
bisection that stops splitting a component once its cut is dense
relative to its internal edge weight (no caller-chosen `K`).
- **Retention**: floor + largest-remainder proportional budget per
partition (`retention.rs`), each partition ranked internally by
`ruvector_agent_memory::CoherencePolicy` — reused as a library
dependency, not re-implemented.
## Evidence
### A defect discovered before the hypothesis could be tested
`DynamicMinCut::partition()` (and the `GraphPartitioner` /
`RuVectorGraphAnalyzer` path built on it) was found, during this
candidate's own development, to return vertex splits **inconsistent with
its own `min_cut_value()`**, and nondeterministically so:
- 6-vertex repro (two triangles joined by one weak `0.05`-weight bridge;
true min cut is uniquely `{0,1,2}` vs `{3,4,5}` at value `0.05`): of
three runs, two returned the correct split, one returned a degenerate
`{single vertex}` vs `{rest}` split — while `min_cut_value()` reported
`0.05` correctly on **every** run.
- 100-vertex version (two 50-cliques, one `0.01`-weight bridge): every run
returned the degenerate split; `min_cut_value()` still correctly
reported `0.01`.
- `GraphPartitioner` was separately found to (a) drop vertices outright at
n=100 (returned partitions covering only 50 of 100 vertices) and (b)
fabricate vertex ids that were never in the input graph at all, when the
id space is non-contiguous.
- `GraphPartitioner` was also measured to be severely slow: **8.4s at
n=500**, and **did not finish in 5m42s at n=4000** (killed).
Full repro commands are in the research doc. This crate works around the
correctness defects with a from-scratch, tested StoerWagner
implementation (`mincut_exact.rs`) used as the sole source of partition
vertex sets; `ruvector_mincut`'s `min_cut_value()` is still queried as an
independent cross-check (its *value* output, as opposed to its
*partition*, was never observed wrong). It works around the performance
defect by scale-gating candidate A (`fixed_k_max_n`, default 600) rather
than hanging the benchmark or silently omitting the comparison.
### The accepted hypothesis run (n=4000, `coherence_ratio=0.35`, `floor_min=3`)
```text
variant overall_recall worst_cluster_recall coverage
GlobalTopScore 0.4193 0.1520 1.000
MincutAdaptive 0.4873 0.1520 1.000
per_cluster_recall GlobalTopScore = [0.996, 0.152, 0.216, 0.316, 0.396, 0.440]
per_cluster_recall MincutAdaptive = [0.792, 0.380, 0.504, 0.152, 0.556, 0.540]
worst_cluster_gain_pp = -0.00 (threshold: +15.00)
ACCEPTANCE_RESULT: REJECT
```
Overall recall improved (+6.8pp) and 4 of 6 clusters gained materially
(+15 to +23pp each), but the specific cluster that was *worst* under the
baseline (cluster 3, 600 members / 15% of the corpus) is **also** worst
under `MincutAdaptive`, at the identical value — because the partitioner
left cluster 3 merged with the 1400-member majority cluster (the 2000-size
partition in `sizes=[200, 2000, 1000, 720, 80]`), so its retention budget
was decided by the same global-style competition the hypothesis set out
to avoid. The `coherence_ratio=0.35` stopping rule, calibrated before this
run against the corpus's true global min cut (see the research doc), does
correctly find and isolate the genuinely weak seams — but cluster 0/3's
separation was not one of them at this threshold.
At n=500, both candidates were run (`fixed_k_max_n=600` admits n=500):
`MincutFixedK` reached `worst_cluster_gain_pp=8.00`, `MincutAdaptive`
reached a *worse* worst-cluster recall than baseline (`0.0` vs `0.10`,
because the true 10-member minority cluster is below `min_cluster_size`
(20) and can never be isolated on its own). Both REJECT.
A bounded, pre-declared-fitness sweep of `floor_min` over `{1,3,8,15}` at
n=4000, holding the same partition fixed, left `worst_cluster_recall`
essentially flat (`0.152`/`0.148` across all four values) — confirming
the bottleneck is the **partition step**, not the **retention-budget
step**: no floor value can protect a cluster the partitioner never
separated from the majority in the first place.
## Consequences
- **Do not promote** `MincutAdaptive`/`MincutFixedK` retention to
production. The pre-registered hypothesis (worst-cluster recall
protection) is rejected by direct measurement.
- `mincut_exact.rs`'s correct, tested StoerWagner implementation is a
reusable asset independent of this ADR's outcome — any future graph-cut
work in this ecosystem needing a trustworthy partition should use it, or
a fixed `ruvector-mincut`, in preference to `DynamicMinCut::partition()`
as it stands today.
- The `ruvector-mincut` defects (partition/value inconsistency,
nondeterminism, vertex loss/fabrication, severe `GraphPartitioner`
latency) should be filed and fixed upstream in that crate; they affect
every existing consumer of `DynamicMinCut::partition()` /
`GraphPartitioner`, not just this experiment.
- A follow-up hypothesis worth testing (not implemented here): a
**per-branch, not global**, stopping criterion — e.g. always attempt at
least one more level of recursion on the largest remaining partition
before accepting `coherence_ratio`'s verdict, or size-weight the
threshold — might separate cluster 0/3 where the flat threshold did
not. This is a new hypothesis, not a retroactive change to the one
tested above.
## Alternatives
- **Ship `CoherencePolicy` unchanged.** Current state; the measured
overall-recall improvement here (+6.8pp) does not offset a rejected
primary hypothesis and a partitioner with two unresolved upstream
correctness defects and a severe latency defect.
- **Global top-score with a per-cluster-label floor** (using a cheap
clustering method like k-means on embeddings instead of graph min-cut)
was considered but not implemented; it would sidestep `ruvector-mincut`
entirely and is a reasonable next candidate.
## Implementation plan
Not applicable — hypothesis rejected; no production migration.
## API shape
`ruvector-partition-memory` (experimental, workspace member, not
re-exported by any production crate): `corpus`, `graph`, `mincut_exact`,
`partition`, `retention`, `metrics`, `witness`, `search` modules; see
`src/lib.rs` for the full surface.
## Feature flags
None; the crate is not on any production feature-gated path.
## Benchmark evidence
`docs/research/nightly/2026-08-17_mincut-partitioned-memory-consolidation/evidence/`
— raw, unedited command output: `bench_n4000.txt`, `bench_n500_with_fixedk.txt`,
`darwin_sweep.txt`, `calibration.txt`.
## Security
No new attack surface: the crate is a standalone research binary/library
operating on synthetic data, not wired into any request path. The witness
chain (`witness.rs`) is a correctness/audit mechanism, not an access
control mechanism, and makes no such claim.
## Governance
None of this crate's code should be treated as validated production
guidance for `ruvector-mincut` usage beyond the specific defects
documented above; those defects should be independently verified by
whoever owns that crate before any fix lands.
## Failure modes
- `DynamicMinCut::partition()` / `GraphPartitioner` defects: see Evidence.
- `AdaptiveConfig::min_cluster_size` (default 20) structurally prevents
isolating any true topic smaller than that absolute count — observed
directly at n=500 (10-member cluster, worst_cluster_recall=0.0).
- A coarse, single-threshold stopping rule can leave two clusters merged
even when one is a minority worth protecting, if their graph-structural
separation is weaker than the threshold demands elsewhere in the same
corpus (observed at n=4000, clusters 0/3).
## Migration
None.
## Rollback
None — nothing shipped to a production path.
## Rejection criteria
Met: worst-cluster recall gain (0.00pp, both n=4000 and n=500) fell short
of the pre-declared 15pp threshold in every configuration tested,
including a bounded post-hoc sweep of the one parameter (`floor_min`)
that could plausibly have rescued it without changing the hypothesis
itself.
## Open questions
- Would a per-branch/size-weighted stopping criterion (see Consequences)
cross the threshold? Untested — a genuinely new hypothesis for a future
nightly, not this one.
- Do the two `ruvector-mincut` defects reproduce on that crate's own
existing test suite, or does no existing test exercise
`DynamicMinCut::partition()` / `GraphPartitioner::partition()`'s output
against ground truth? Not investigated here; worth checking before
filing upstream.

View file

@ -0,0 +1,585 @@
# Mincut-Partitioned Agent-Memory Consolidation
**Date**: 2026-08-17
**Crate**: `ruvector-partition-memory` (`crates/ruvector-partition-memory`)
**Status**: PoC complete — **hypothesis REJECTED by measurement**, plus two documented defects discovered in `ruvector-mincut`
**ADR**: [ADR-305](../../../adr/ADR-305-mincut-partitioned-memory-consolidation.md)
---
## Summary of Outcome
The hypothesis — that partitioning the agent-memory similarity graph
before applying a retention budget protects a minority topic from being
evicted in full by a global top-score compactor — is **rejected** on the
pre-declared metric (worst-cluster recall@10 gain ≥ 15pp) at every scale
tested:
| Run | Best candidate | Worst-cluster gain | Threshold | Verdict |
|---|---|---|---|---|
| n=4000, coherence_ratio=0.35 | MincutAdaptive | **0.00pp** | 15pp | REJECT |
| n=500, coherence_ratio=0.35 | MincutFixedK | 8.00pp | 15pp | REJECT |
| n=4000, floor_min sweep {1,3,8,15} | (all) | 0.00pp (flat) | 15pp | REJECT |
The mechanism is not worthless — 4 of 6 clusters gained 1523pp recall
each and overall recall improved +6.8pp at n=4000 — but the specific
cluster the hypothesis exists to protect (the one a global score would
otherwise starve) was, in the accepted run, left merged with the majority
cluster by the partitioner, so it received no protection at all. A bounded
sweep of the retention floor confirmed this is a **partitioning**
shortfall, not a **retention-budget** shortfall: no floor value moved the
worst-cluster number.
Along the way, developing this candidate against `ruvector-mincut`
surfaced two independent, reproducible defects in that crate (not
previously known to this nightly process — see below), which this run
worked around rather than silently absorbed.
---
## Abstract
`ruvector-agent-memory` (nightly 2026-06-14) scores every stored memory
against a global importance formula — `α·recency + β·frequency +
γ·coherence(context)` — and keeps the top-N at compaction time. It
measured excellent aggregate recall, but a global ranking is, by
construction, blind to topic diversity: a memory topic the agent is not
currently working on competes on the same scale as everything else, and
can be evicted **in full**.
This nightly asks whether partitioning the memory similarity graph first
— using `ruvector-mincut`, previously unused for agent memory — and
retaining a guaranteed floor per partition, fixes that. It does not, at
least not with the threshold-based partitioner tested here; the write-up
below explains why, with per-cluster evidence.
---
## Hypothesis
```text
Given a 4,000-memory corpus with 6 semantic clusters of unequal size
(1400/1000/720/600/200/80 — two minorities at 5% and 2% of the corpus),
scored against a recency-biased context drawn from the largest cluster,
when a partition-aware retention policy (floor + proportional budget per
graph partition) replaces CoherencePolicy's global top-score ranking,
at 50% compaction,
then the best candidate's worst-cluster recall@10 should exceed the
baseline's by >= 15 percentage points,
subject to: no candidate's overall recall@10 regressing more than 5pp
below baseline; partition+retention wall time under 30s per candidate at
this n; and the partition witness chain verifying.
```
This threshold, and the corpus/graph calibration below, were fixed
**before** the accepted run in `evidence/bench_n4000.txt`. They were not
adjusted afterward.
---
## Why This Matters for RuVector
RuVector is a Rust-native substrate for agent memory, not just a vector
store. Long-running agents accumulate memories across many unrelated
topics; a compaction policy that silently loses whole topics degrades
retrieval quality in a way aggregate recall numbers hide. This nightly
connects:
| Component | Role |
|---|---|
| `ruvector-agent-memory` | Reused directly as a library dependency — the baseline scorer, and the within-partition scorer for both candidates. Not re-implemented. |
| `ruvector-mincut` | Source of the graph-partitioning primitives this crate builds on (`GraphPartitioner`) and cross-checks against (`DynamicMinCut::min_cut_value()`). |
| `ruvector-retrieval-receipt` (2026-08-13) | Precedent this crate follows for `witness.rs`'s SHA-256 hash-chain design — tamper-evident commitments over a decision, not a signature over correctness. |
| ruFlo | A real production path for this class of policy (if a future variant is accepted) would run as a scheduled memory-consolidation workflow, not inline on the write path. |
| MCP | A future accepted policy's natural interface is a narrow `memory_consolidate(target_pct)` tool, mirroring 2026-06-14's suggested `memory_compact`. |
---
## Architecture
```mermaid
flowchart TD
A[Memory corpus<br/>4000 records, 6 clusters] --> B[k-NN similarity graph<br/>graph.rs, k=10, cosine weights]
B --> C1[GlobalTopScore baseline<br/>ruvector_agent_memory::CoherencePolicy]
B --> C2[MincutFixedK candidate A<br/>ruvector_mincut::GraphPartitioner]
B --> C3[MincutAdaptive candidate B<br/>mincut_exact.rs Stoer-Wagner]
C3 --> W[PartitionWitnessChain<br/>witness.rs — SHA-256 hash chain]
C1 --> R1[retain_global_top_score]
C2 --> R2[retain_partitioned<br/>floor + proportional budget]
C3 --> R2
R1 --> M[metrics.rs<br/>overall + per-cluster + worst-cluster recall@10]
R2 --> M
M --> ACC[Pre-declared acceptance gate<br/>main.rs]
```
`mincut_exact.rs` exists because `ruvector_mincut::DynamicMinCut::partition()`
was found, during development, to disagree with its own `min_cut_value()`
— see **Defects Discovered** below. Candidate B's splits are materialized
by a from-scratch, tested StoerWagner implementation instead;
`ruvector_mincut`'s value is still queried as an independent cross-check
and logged.
---
## Implementation
Three variants, one shared scorer:
- **`GlobalTopScore`** (baseline): `ruvector_agent_memory::CoherencePolicy::default()`
applied to the whole corpus.
- **`MincutFixedK`** (candidate A): `ruvector_mincut::GraphPartitioner`
(existing tool, unweighted edge-count recursive bisection to a
caller-chosen `K`), then `retain_partitioned`.
- **`MincutAdaptive`** (candidate B): a new recursive bisection
(`partition.rs::recurse`) using `mincut_exact::global_min_cut` at each
level, stopping once a component's cut is dense relative to its
internal edge weight (`coherence_ratio`, calibrated below), then
`retain_partitioned`.
`retain_partitioned` (`retention.rs`) allocates the retention budget
per-partition via a floor (`floor_min`, default 3) plus largest-remainder
proportional split of the remainder, then ranks each partition internally
with the same `CoherencePolicy` the baseline uses — isolating the
independent variable to *budget allocation*, not *scoring*.
The corpus (`corpus.rs`) is a deterministic, seeded synthetic generator:
6 clusters on the unit sphere (rejection-sampled to cosine separation
≤ 0.35), Gaussian noise (`noise_std`), decoupled recency/frequency
signals, and a recency-biased "focus cluster" standing in for what the
agent was just working on — the realistic scenario in which
`CoherencePolicy`'s context window is biased away from other topics.
Ground truth is brute-force top-k cosine search against the full,
uncompacted corpus, computed once at generation time.
### Calibration (before the accepted run)
At the originally-planned `noise_std=0.35`, the corpus's true global min
cut degenerately isolated a single outlier vertex
(`normalized_cut≈0.76` — no real topic boundary was the graph's weakest
seam). At `noise_std=0.25`, the min cut cleanly isolated one whole
semantic cluster (`normalized_cut≈0.07`), confirming a graph structure
the hypothesis could actually be tested against. `noise_std=0.25` and
`coherence_ratio=0.35` were fixed from this calibration pass, before the
accepted run — see `evidence/calibration.txt`.
---
## Defects Discovered in `ruvector-mincut`
Two independent, reproducible issues, found while building candidate B,
neither previously known to this nightly process:
### 1. `DynamicMinCut::partition()` is inconsistent with its own `min_cut_value()`, and nondeterministic
Minimal repro: two triangles `{0,1,2}` and `{3,4,5}`, joined by one
`weight=0.05` bridge edge. The true global minimum cut is unique — value
`0.05`, split `{0,1,2}`/`{3,4,5}` (isolating any single triangle vertex
costs ≥ `1.0`).
```rust
let mincut = MinCutBuilder::new().exact().with_edges(edges).build().unwrap();
mincut.min_cut_value() // always 0.05, every run — correct
mincut.partition() // sometimes {0,1,2}/{3,4,5} (correct),
// sometimes {single vertex}/{rest} (wrong: that
// split's actual crossing weight is >= 1.0, not 0.05)
```
Of three runs: two returned the correct split, one returned the
degenerate split — same code, same input, different process invocations.
At 100 vertices (two 50-cliques, one `0.01` bridge), **every** run
returned the degenerate split, while `min_cut_value()` still correctly
reported `0.01` every time. `cut_edges()` (derived from `.partition()`)
was cross-checked to independently confirm the mismatch: for the
degenerate split, summed crossing-edge weight was `2.0`, not the reported
`0.05`.
### 2. `GraphPartitioner` / `RuVectorGraphAnalyzer`: vertex loss, vertex fabrication, and severe latency
- At n=100 (two 50-cliques + weak bridge), `GraphPartitioner::partition()`
returned partitions covering only 50 of the 100 input vertices.
- With a non-contiguous vertex-id space (`{1,2,3,11,12,13}`),
`RuVectorGraphAnalyzer::partition()` returned a side containing ids
(`4,5,6,7,8,9,10`) that were never in the input graph.
- **Latency**: `GraphPartitioner::partition()` (K=10) measured **8.4s at
n=500**, and had not finished after **5m42s at n=4000** (process
killed). This crate's own `mincut_exact::global_min_cut` measured
**167ms at n=500** and **~11.1s for the full adaptive recursion at
n=4000** — the same order of magnitude for *one* global min cut,
suggesting `GraphPartitioner`'s recursive re-wrapping (`RuVectorGraphAnalyzer::new`
per subgraph, itself built on the fully-dynamic `MinCutWrapper`) pays a
large, likely superlinear, overhead for what is fundamentally a
one-shot static computation at each level.
**Workaround used in this crate**: `mincut_exact.rs` — a from-scratch,
tested, deterministic weighted StoerWagner implementation — is the sole
source of partition vertex sets for candidate B.
`ruvector_mincut::DynamicMinCut::min_cut_value()` is still called as an
independent cross-check (`partition.rs`), logged via a `debug_assert!` on
disagreement; it was never observed wrong in this crate's testing, only
its *partition* output was. `fixed_k_partition` (candidate A) filters
`GraphPartitioner`'s output against the known-valid vertex set and
appends any uncovered vertex as a fallback group, so it cannot silently
drop or fabricate a memory — and is scale-gated (`fixed_k_max_n`, default
600) so a benchmark run cannot hang on it.
**Not filed upstream as part of this nightly** (no `ruvector-mincut`
maintainer sign-off in scope here) — recorded as an open question in
ADR-305 for whoever owns that crate to verify and file.
---
## Benchmark Methodology
- Release build (`cargo build --release`), `rustc 1.94.1`, `cargo 1.94.1`.
- Hardware: x86-64, 4 logical CPUs, 15GiB RAM, Linux 6.18.5.
- Deterministic seed (`seed=42`) for corpus generation; ground truth
computed once per corpus via brute-force cosine search, not resampled
per variant.
- 150 out-of-sample queries (25 per cluster), recall@10 against the full
uncompacted corpus.
- Single run per configuration (no repeated-trial variance reporting —
see Limitations).
- Exact commands and raw, unedited output: `evidence/*.txt`.
```bash
cargo run --release -p ruvector-partition-memory --bin benchmark -- 4000 3 0.35 10 600
cargo run --release -p ruvector-partition-memory --bin benchmark -- 500 3 0.35 10 600
cargo run --release -p ruvector-partition-memory --example darwin_sweep
cargo run --release -p ruvector-partition-memory --example calibrate
```
## Benchmark Results
### n=4000 (accepted run)
```text
variant retained overall_recall worst_cluster_recall coverage partition_us retention_us
GlobalTopScore 2000 0.4193 0.1520 1.000 0 12948
MincutAdaptive 2000 0.4873 0.1520 1.000 11164029 13482
per_cluster_recall GlobalTopScore = [0.996, 0.152, 0.216, 0.316, 0.396, 0.440]
per_cluster_recall MincutAdaptive = [0.792, 0.380, 0.504, 0.152, 0.556, 0.540]
MincutFixedK: SKIPPED (n=4000 exceeds fixed_k_max_n=600; see Defects Discovered)
MincutAdaptive partitions: 5, sizes=[200, 2000, 1000, 720, 80]
^^^^ cluster0(1400)+cluster3(600) stayed merged
worst_cluster_gain_pp = -0.00 (threshold 15.00) ACCEPTANCE_RESULT: REJECT
```
Full raw output: `evidence/bench_n4000.txt`.
### n=500 (both candidates)
```text
variant overall_recall worst_cluster_recall coverage
GlobalTopScore 0.3440 0.1000 1.000
MincutFixedK 0.4467 0.1800 1.000
MincutAdaptive 0.3500 0.0000 0.833 <- 10-member cluster below min_cluster_size(20)
worst_cluster_gain_pp (best=MincutFixedK) = 8.00 (threshold 15.00) ACCEPTANCE_RESULT: REJECT
```
Full raw output: `evidence/bench_n500_with_fixedk.txt`.
### Bounded Darwin-style sweep (n=4000, partition fixed, `floor_min` varied)
```text
floor_min=1 overall_recall=0.4880 worst_cluster_recall=0.1520 fitness=0.4224
floor_min=3 overall_recall=0.4873 worst_cluster_recall=0.1520 fitness=0.4222
floor_min=8 overall_recall=0.5000 worst_cluster_recall=0.1480 fitness=0.4260
floor_min=15 overall_recall=0.5067 worst_cluster_recall=0.1480 fitness=0.4260
winner: floor_min=15 DARWIN_RESULT: PROMOTE (composite fitness only — see Darwin section)
```
`worst_cluster_recall` is flat (within noise) across every `floor_min`
tested — direct evidence the shortfall is structural (partitioning), not
a retention-budget tuning problem. Full raw output:
`evidence/darwin_sweep.txt`.
---
## Memory Math
At n=4000, d=64: corpus embeddings are `4000 × 64 × 4 bytes ≈ 1.0MB`.
The k-NN graph (k=10, deduplicated undirected) holds ~31,000 edges;
stored as `(u64, u64, f64)` triples, `~744KB`. `mincut_exact`'s working
set during a single `global_min_cut` call is `O(V)` `HashMap`s of degree
`~2k`; peak additional memory is a small multiple of the edge list, not
separately measured in this run (see Limitations).
## Performance Math
`MincutAdaptive`'s ~11.1s at n=4000 is dominated by the top-level
`global_min_cut` call over the full ~4000-vertex, ~31000-edge graph
(subsequent recursion levels operate on rapidly shrinking subgraphs).
This is consistent with the `O(V·E·log V)`-ish binary-heap StoerWagner
formulation used here (not the theoretically tighter but more complex
`O(VE + V² log V)` NagamochiIbaraki-style variant) — acceptable for a
one-time nightly consolidation event, not for an inline write-path
operation at this scale without further optimization.
## Failure Modes
- Partitioner leaves the true worst cluster merged with the majority
(this run's actual failure mode — see per-cluster evidence above).
- `min_cluster_size` floor structurally prevents isolating any topic
smaller than that absolute count (n=500 run).
- `ruvector-mincut` defects (see above) — worked around, not fixed.
## Rejected Alternatives
- **K-means-based partitioning** instead of graph min-cut: not
implemented; a reasonable next candidate that sidesteps
`ruvector-mincut` entirely (see ADR-305 Alternatives).
- **Forcing `GraphPartitioner` to be candidate A at full scale**: rejected
after direct measurement (5m42s, unfinished) — reported honestly as a
scale-gated skip rather than silently hidden or waited out indefinitely.
---
## Security
No new attack surface. This crate is a standalone research binary/library
over synthetic data; nothing in it is wired into a request-serving path.
`witness.rs` (SHA-256 hash chain over partition decisions) is a
tamper-evidence mechanism for *auditing a partition decision after the
fact* — it proves a step's recorded cut value and vertex-set hashes were
not edited post-hoc — it is **not** a correctness proof of the underlying
min cut and makes no access-control claim, matching the threat-model
framing `ruvector-retrieval-receipt` (2026-08-13) established for reads.
## Governance
Hypothesis rejected; no promotion, no production migration, no rollback
needed. The two `ruvector-mincut` defects are recorded as an open
question in ADR-305, not filed upstream from within this nightly run —
that requires the owning maintainer's verification.
## MCP Implications
None planned — the underlying policy is rejected. Had it been accepted,
the natural interface would mirror the 2026-06-14 nightly's suggested
`memory_compact(context, target_pct)` tool, narrowly scoped, read/write
on the agent's own memory store only.
## WASM / Edge Implications
Not evaluated. `mincut_exact.rs` has zero non-`ruvector_mincut` type
dependencies beyond `std` collections and would very likely compile to
WASM (no unsafe, no platform-specific code) if this policy is revisited,
but binary-size and edge-memory impact were not measured in this run —
no deployment claim is made.
## RVF Implications
A future accepted consolidation policy's output (retained memory ids +
partition witness chain) is a natural fit for an RVF portable snapshot:
the witness chain already produces the kind of signed-lineage evidence
RVF snapshots want. Not implemented — analysis only, per the mandatory
(implementation optional) requirement for RVF fit.
## RVM Implications
No RVM fit identified: this policy does not need isolated execution,
capability boundaries, or proof-gated mutation beyond what its own
witness chain already provides for its one internal decision (the
partition). Not forced.
## ruFlo Implications
If a future variant of this hypothesis is accepted, ruFlo's natural role
is a scheduled memory-consolidation workflow (analogous to the "memory
maintenance" workflow class in the harness's own role list) — triggered
on a cadence or storage-pressure signal, not run inline on the write
path, given the measured ~11s latency at n=4000.
---
## Practical Applications
1. **Long-running coding agents** — memory: prior debugging sessions
across unrelated modules; problem: a burst of work on module A can
starve retained memory of module B at consolidation time; RuVector
capability: (if a future variant is accepted) partition-aware
retention; ecosystem integration: ruFlo scheduled consolidation;
business value: fewer "the agent forgot X" regressions; main risk:
this run shows the naive version does not reliably deliver that
protection; time horizon: near-term, pending a revised hypothesis.
2. **Customer-support agent memory** — user: support bot; problem: a busy
week on one product line can evict memory of a rarely-escalated
product line; capability: same as above; risk: same; horizon: near-term.
3. **Multi-project assistant memory** — user: an assistant used across
several unrelated user projects; problem: intense work on project A
crowds out project B's memory; horizon: near-term.
4. **Scientific literature agents** — user: research assistant tracking
several research threads; problem: an active thread's queries bias
consolidation away from a dormant-but-still-relevant thread; horizon:
medium-term.
5. **Enterprise Graph RAG** — user: internal knowledge agent; problem:
department-specific knowledge clusters compete unevenly for retention
budget; horizon: medium-term.
6. **Robotics/edge agent memory** — user: an embedded agent with a hard
memory cap; problem: same starvation risk, higher stakes given no
"just don't compact" fallback; horizon: long-term, pending edge
feasibility work not done here.
7. **Security/anomaly-memory agents** — user: a SOC assistant; problem:
a high-volume alert category can crowd out memory of a rare-but-severe
category; horizon: medium-term.
8. **Local-first personal assistants** — user: a device-resident
assistant; problem: identical starvation risk under a tight local
memory budget; horizon: long-term.
## Long Horizon Applications
1. **Self-healing graph memory** — thesis: agent memory graphs that
detect and repair their own topic-starvation without a human noticing;
requires: a stopping criterion that reliably finds every weak seam, not
just some of them (this run's central gap); RuVector role: the
substrate the repair loop runs against; why this experiment matters:
it is the first measured evidence of *where* a naive version of this
idea fails; primary uncertainty: whether any single global threshold
can ever reliably separate every minority topic, or whether a
per-branch/adaptive criterion is required; falsification: repeat this
benchmark with a per-branch stopping rule and measure worst-cluster
gain again.
2. **Synthetic nervous systems for agent fleets** — thesis: fleets of
agents sharing a partitioned memory substrate, each fleet member
effectively "owning" a partition; requires: partition stability under
concurrent writes, not evaluated here; RuVector role: shared substrate;
uncertainty: whether partition boundaries stay stable as memory grows;
falsification: a delete/insert-churn variant of this benchmark.
3. **Agent operating systems** — thesis: memory partitioning as a kernel
primitive analogous to process isolation; requires: much stronger
correctness guarantees than this run's underlying library currently
provides (see Defects Discovered); uncertainty: whether the two
documented `ruvector-mincut` defects are fixable without an API
change; falsification: the fix either lands and this crate's
`mincut_exact.rs` workaround becomes redundant, or it doesn't.
4. **Swarm memory** — thesis: partition-aware consolidation as the memory
layer for multi-agent swarms; requires: partitioning at swarm scale
(this run only reached n=4000 at ~11s per full run); uncertainty:
scaling behavior beyond n=4000, not measured; falsification: repeat at
n=40,000 and check wall time stays sub-linear-ish.
5. **Dynamic world models** — thesis: topic partitions as a proxy for
distinct "world model" facets an agent maintains; requires: partition
labels that are stable and interpretable over time, not evaluated;
uncertainty: whether graph min-cut partitions correspond to anything a
human would call a coherent "facet"; falsification: qualitative review
of partition contents against human-labeled topics.
6. **Proof-gated autonomous infrastructure** — thesis: the witness chain
here generalizes to a general "prove this maintenance decision wasn't
silently gamed" primitive for autonomous infra; requires: extending
`witness.rs`'s pattern beyond partition decisions; uncertainty:
whether the pattern holds up under adversarial (not just accidental)
tampering; falsification: an explicit red-team pass against the
witness chain, not performed in this run.
7. **RVM coherence domains** — thesis: partitions as RVM coherence-domain
boundaries; requires: the RVM fit analysis above to change from "not
identified" to "identified," which would need a concrete isolation
requirement this policy does not currently have; uncertainty: high;
falsification: N/A until a concrete requirement exists.
8. **Robotics memory** — thesis: partition-aware retention for
resource-constrained robot memory; requires: the edge/WASM
measurements this run explicitly did not make; uncertainty: whether
`mincut_exact.rs`'s ~11s at n=4000 is remotely feasible on embedded
hardware; falsification: run `mincut_exact` benchmarks on target
hardware.
---
## Competitor Comparison
Not materially applicable — no public vector database documents a
graph-partition-aware memory *compaction* policy comparable to this
experiment's scope (agent-memory lifecycle management, not ANN indexing).
`documented_external_capability`: none found for this specific mechanism
in Milvus/Qdrant/Weaviate/Pinecone/LanceDB/FAISS/pgvector/Chroma/Vespa.
`directly_measured_capability`: N/A (nothing external to measure against).
`unknown`: whether any of these systems' internal (undocumented)
compaction logic does something structurally similar.
---
## Evolution Results (Darwin)
- **Executed**: yes, bounded (generations=1, candidates_per_generation=4,
matching the harness's default budget), over `floor_min ∈ {1,3,8,15}`,
partition held fixed (only retention depends on `floor_min`).
- **Fitness** (declared before running): `0.5·worst_cluster_recall +
0.3·overall_recall + 0.2·correctness`.
- **Winner**: `floor_min=15`, `fitness=0.4260` vs parent
(`floor_min=3`) `fitness=0.4222``DARWIN_RESULT: PROMOTE` **on this
composite fitness metric only**. `worst_cluster_recall` itself did not
improve (0.148 vs 0.152 — marginally *worse*); the promotion is driven
by `floor_min=15`'s better overall recall. This is reported precisely
so it is not mistaken for the primary ACCEPTANCE_RESULT, which remains
REJECT.
- **Parent retained**: yes — this Darwin promotion is not wired into
`main.rs`'s defaults; ADR-305 does not recommend shipping it.
## Witness Evidence
`MincutAdaptive`'s partition witness chain: 9 split steps at n=4000,
`chain_verify=true`, head
`a15b77949d3d26928fc84cd89b0dcb749c4b16359b3caa08320967a8bffa8469`
(`evidence/bench_n4000.txt`). `witness.rs` unit tests additionally verify
the chain detects post-hoc tampering of a recorded step
(`chain_breaks_when_a_field_is_edited_after_the_fact`).
## Production Path
None — hypothesis rejected. See ADR-305 Consequences for the specific
follow-up direction (per-branch stopping criterion) that would need to be
tested as a new hypothesis before any production consideration.
## Falsification Criteria
Met, per the pre-declared acceptance gate: worst-cluster recall gain did
not reach +15pp in any tested configuration, including a bounded sweep of
the one parameter most likely to rescue it.
## Limitations
- **Single run per configuration** — no repeated-trial variance reporting
(Step 13's "prefer multiple repetitions" was not followed here, given
~11s per n=4000 run and the time budget for one nightly cycle). The
measured numbers should be read as point estimates, not
variance-characterized results.
- **One corpus generator, one seed family** — results are specific to
this synthetic corpus's cluster-separation and noise characteristics;
not validated against a real agent-memory trace.
- **`ruvector-mincut` defects not filed upstream** from within this run —
recorded as an open question, not resolved.
- **No WASM/edge measurement**, despite the mandatory-analysis
requirement being satisfied by the qualitative section above.
- **`mincut_exact.rs` is not asymptotically optimal** StoerWagner
(a NagamochiIbaraki-style formulation would be faster); it was
sufficient for this run's n=4000 but was not tuned for larger scale.
## Next Research
1. Test a per-branch/size-weighted adaptive stopping criterion against
the same corpus and acceptance gate, as a genuinely new hypothesis.
2. Test a k-means-based (non-graph) partition baseline, sidestepping
`ruvector-mincut` entirely, as a cheaper alternative worth comparing.
3. Verify the two `ruvector-mincut` defects against that crate's own test
suite and, if confirmed absent from existing coverage, file them
upstream with the repros in this doc.
4. Repeat this benchmark with repeated trials and variance reporting if
a revised hypothesis clears the first-pass bar above.
## References
- Nightly 2026-06-14, `crates/ruvector-agent-memory``CoherencePolicy`,
reused directly here.
- Nightly 2026-08-13, `crates/ruvector-retrieval-receipt`, ADR-304 —
witness-chain design precedent for `witness.rs`.
- Jin, Sun, Thorup, "Fully Dynamic Exact Minimum Cut in Subpolynomial
Time" (SODA 2024) — the algorithm `ruvector-mincut`'s `witness` module
cites; not itself re-verified in this run.
- Stoer, Wagner, "A Simple Min-Cut Algorithm" (1997) — the algorithm
implemented from scratch in `mincut_exact.rs`.

View file

@ -0,0 +1,18 @@
=== ruvector-partition-memory nightly benchmark ===
n=4000 dims=64 cluster_sizes=[1400, 1000, 720, 600, 200, 80] focus_cluster=0 target_size=2000 (50% retention) queries=150
params: floor_min=3 coherence_ratio=0.35 fixed_k=10 fixed_k_max_n=600
corpus_gen_us=119649 knn_graph_us=2691799 knn_edges=31061
MincutFixedK: SKIPPED (n=4000 exceeds fixed_k_max_n=600; ruvector_mincut::GraphPartitioner measured at 8.4s for n=500 and did not finish in 5m42s for n=4000 during this nightly's development — see the research doc for the repro)
MincutAdaptive partitions: 5 (coherence_ratio=0.35) sizes=[200, 2000, 1000, 720, 80]
MincutAdaptive witness: 9 split steps, chain_verify=true, head=a15b77949d3d26928fc84cd89b0dcb749c4b16359b3caa08320967a8bffa8469
variant retained overall_recall worst_cluster_recall coverage partition_us retention_us
GlobalTopScore retained=2000 overall_recall=0.4193 worst_cluster_recall=0.1520 coverage=1.000 partition_us=0 retention_us=12948
MincutAdaptive retained=2000 overall_recall=0.4873 worst_cluster_recall=0.1520 coverage=1.000 partition_us=11164029 retention_us=13482
per_cluster_recall GlobalTopScore = [0.996, 0.15200000000000005, 0.21599999999999997, 0.31600000000000006, 0.39599999999999996, 0.44000000000000006]
per_cluster_recall MincutAdaptive = [0.7919999999999999, 0.38, 0.5040000000000001, 0.15200000000000002, 0.5559999999999999, 0.54]
acceptance: best_candidate=MincutAdaptive worst_cluster_gain_pp=-0.00 (threshold_pp=15.00) gain_ok=false overall_ok=true latency_ok=true witness_ok=true
ACCEPTANCE_RESULT: REJECT

View file

@ -0,0 +1,20 @@
=== ruvector-partition-memory nightly benchmark ===
n=500 dims=64 cluster_sizes=[175, 125, 90, 75, 25, 10] focus_cluster=0 target_size=250 (50% retention) queries=150
params: floor_min=3 coherence_ratio=0.35 fixed_k=10 fixed_k_max_n=600
corpus_gen_us=12962 knn_graph_us=35113 knn_edges=3572
MincutFixedK partitions: 3 (K=10 requested) sizes=[1, 346, 153]
MincutAdaptive partitions: 4 (coherence_ratio=0.35) sizes=[90, 75, 125, 210]
MincutAdaptive witness: 7 split steps, chain_verify=true, head=11910bea6063621a4290554636dc218909bc4baa4812350f4ea4de56238a7a70
variant retained overall_recall worst_cluster_recall coverage partition_us retention_us
GlobalTopScore retained=250 overall_recall=0.3440 worst_cluster_recall=0.1000 coverage=1.000 partition_us=0 retention_us=1533
MincutFixedK retained=250 overall_recall=0.4467 worst_cluster_recall=0.1800 coverage=1.000 partition_us=642943 retention_us=1681
MincutAdaptive retained=250 overall_recall=0.3500 worst_cluster_recall=0.0000 coverage=0.833 partition_us=516739 retention_us=2514
per_cluster_recall GlobalTopScore = [0.988, 0.2, 0.188, 0.3, 0.2879999999999999, 0.10000000000000003]
per_cluster_recall MincutFixedK = [0.848, 0.18, 0.34, 0.4640000000000001, 0.5479999999999999, 0.2999999999999999]
per_cluster_recall MincutAdaptive = [0.6039999999999999, 0.49199999999999994, 0.4040000000000001, 0.46, 0.14000000000000004, 0.0]
acceptance: best_candidate=MincutFixedK worst_cluster_gain_pp=8.00 (threshold_pp=15.00) gain_ok=false overall_ok=true latency_ok=true witness_ok=true
ACCEPTANCE_RESULT: REJECT

View file

@ -0,0 +1,6 @@
n=500 ratio=0.5 partitions=4 sizes=[90, 75, 125, 210] purities=["1.00", "1.00", "1.00", "0.83"] us=480432
n=500 ratio=0.35 partitions=4 sizes=[90, 75, 125, 210] purities=["1.00", "1.00", "1.00", "0.83"] us=524855
n=500 ratio=0.2 partitions=2 sizes=[90, 410] purities=["1.00", "0.43"] us=315318
n=4000 ratio=0.5 partitions=5 sizes=[200, 2000, 1000, 720, 80] purities=["1.00", "0.70", "1.00", "1.00", "1.00"] us=10784652
n=4000 ratio=0.35 partitions=5 sizes=[200, 2000, 1000, 720, 80] purities=["1.00", "0.70", "1.00", "1.00", "1.00"] us=11097579
n=4000 ratio=0.2 partitions=4 sizes=[2200, 1000, 720, 80] purities=["0.64", "1.00", "1.00", "1.00"] us=6667169

View file

@ -0,0 +1,8 @@
parent partition (fixed for the whole sweep): 5 partitions, sizes=[200, 2000, 1000, 720, 80], correctness=1
gen=1 candidates_per_generation=4
floor_min=1 retained=2000 overall_recall=0.4880 worst_cluster_recall=0.1520 fitness=0.4224
floor_min=3 retained=2000 overall_recall=0.4873 worst_cluster_recall=0.1520 fitness=0.4222
floor_min=8 retained=2000 overall_recall=0.5000 worst_cluster_recall=0.1480 fitness=0.4240
floor_min=15 retained=2000 overall_recall=0.5067 worst_cluster_recall=0.1480 fitness=0.4260
winner: floor_min=15 fitness=0.4260 beats_parent(floor_min=3)=true
DARWIN_RESULT: PROMOTE

View file

@ -0,0 +1,123 @@
# Partitioning agent memory before compaction: a negative result, and a bug it uncovered
## Problem
Agent memory systems that compact by a single global importance score
(recency + frequency + relevance to current context) can evict an entire
topic in one pass, even at a compaction ratio that looks fine on average.
A topic the agent isn't currently working on has no defense against a
score built for the topic it is working on.
## Hypothesis
Partition the memory similarity graph into topic clusters first, then
give each partition a guaranteed minimum retention share, so a topic only
competes with itself for its floor allocation instead of the whole
corpus. Tested on a synthetic 4,000-memory corpus with 6 unequal-size
semantic clusters (down to 2% of the corpus), against
`ruvector-agent-memory`'s existing `CoherencePolicy` baseline, at 50%
compaction.
Pre-declared bar: the best partition-aware candidate's **worst-cluster**
recall@10 must beat the baseline's by ≥15 percentage points.
## What happened
It didn't clear the bar. Overall recall improved (+6.8pp) and 4 of 6
clusters individually gained 1523pp — the mechanism clearly does
something. But the specific cluster that was worst under the baseline was
*also* worst under the partitioned candidate, at the identical recall
value, because the partitioner left it merged with the majority cluster
instead of separating it out. A follow-up sweep of the retention floor
(1, 3, 8, 15) left the worst-cluster number flat across every value —
proof the gap is in *where the graph gets cut*, not *how the budget gets
split afterward*.
```text
per_cluster_recall GlobalTopScore = [0.996, 0.152, 0.216, 0.316, 0.396, 0.440]
per_cluster_recall MincutAdaptive = [0.792, 0.380, 0.504, 0.152, 0.556, 0.540]
^^^^^ improved a lot ^^^^^ untouched
```
## The bug along the way
Before any of the above could be measured, `ruvector_mincut::DynamicMinCut::partition()`
turned out to be untrustworthy. Minimal repro: two triangles joined by a
single weak-weight bridge edge — a graph whose true minimum cut is
unique and easy to verify by hand.
```rust
let mincut = MinCutBuilder::new().exact().with_edges(edges).build().unwrap();
mincut.min_cut_value() // 0.05, every single run — correct
mincut.partition() // sometimes the correct split, sometimes a
// degenerate "isolate one vertex" split whose
// actual crossing weight is 40x the reported value
```
At 100 vertices the degenerate split happened on every run, not just
some. `GraphPartitioner` (built on the same machinery) separately dropped
vertices outright, fabricated vertex ids for non-contiguous id spaces,
and took 8.4 seconds to partition 500 vertices — with no sign of
finishing at 4,000 after nearly six minutes.
None of that is this crate's algorithm — it's the *value* computation
that was correct, only the *partition materialization* that wasn't. The
workaround was a from-scratch, tested, deterministic weighted
StoerWagner implementation (`mincut_exact.rs`, ~250 lines, zero
non-`ruvector_mincut`-type dependencies), used as the sole source of
partition vertex sets, with `ruvector_mincut`'s value still queried
purely as an independent cross-check.
## Why report a rejected hypothesis
Because the measurement is real and the mechanism partially works. A
future variant with a smarter stopping rule — one that doesn't let a
single global threshold decide every split — is a legitimate next
experiment, and now has a concrete, per-cluster reason to exist instead
of a hunch. And because the correctness bug this candidate ran into would
have silently produced wrong partitions for anyone else building on
`GraphPartitioner` or `DynamicMinCut::partition()` today, whether or not
this particular hypothesis had panned out.
## Limitations
Single run per configuration, one synthetic corpus, one seed family — no
variance characterization. The `ruvector-mincut` defects are documented
with repros but not filed upstream from within this run; that needs the
owning maintainer's independent verification.
## Production relevance
None yet — this is a rejected hypothesis. If a per-branch stopping
criterion clears the bar in a follow-up run, the natural production path
is a scheduled ruFlo memory-consolidation workflow, not an inline
write-path operation (the measured ~11s partitioning time at n=4000 rules
that out regardless).
## RuVector ecosystem implications
`mincut_exact.rs` is a reusable, correctness-tested min-cut
implementation independent of this ADR's own rejected hypothesis — a
better foundation for any future RuVector graph-partitioning work than
`DynamicMinCut::partition()` as it stands today.
## Future direction
Test a per-branch/size-weighted stopping criterion (attempt at least one
more split on the largest remaining partition before accepting a global
threshold's verdict) against the same corpus and the same 15pp bar, as a
new, separately pre-declared hypothesis.
## References
- Nightly 2026-06-14, `ruvector-agent-memory``CoherencePolicy`, the
baseline this experiment measured against and reused as a dependency.
- Nightly 2026-08-13, `ruvector-retrieval-receipt` — witness-chain design
precedent.
- Stoer & Wagner, "A Simple Min-Cut Algorithm" (1997).
- Jin, Sun & Thorup, "Fully Dynamic Exact Minimum Cut in Subpolynomial
Time" (SODA 2024) — the algorithm `ruvector-mincut` implements.
Full write-up, ADR, and raw benchmark output:
`docs/research/nightly/2026-08-17_mincut-partitioned-memory-consolidation/`
in [ruvnet/ruvector](https://github.com/ruvnet/ruvector).