Skip to content

fix: replace greedy consolidation clustering with average-link (UPGMA) - #261

Merged
jagoff merged 4 commits into
masterfrom
worktree-agent-a64307fdf1547bec2
Aug 17, 2026
Merged

fix: replace greedy consolidation clustering with average-link (UPGMA)#261
jagoff merged 4 commits into
masterfrom
worktree-agent-a64307fdf1547bec2

Conversation

@jagoff

@jagoff jagoff commented Aug 17, 2026

Copy link
Copy Markdown
Owner

What

  1. Replace greedy consolidation clustering with average-link (UPGMA)_cluster_within_scope now clusters via _average_link_cluster. Greedy split 38.4% of above-threshold pairs (frozen-first-representative bug); single-linkage chained 157/950 memories through bridges. Average-link sits between: measured purity on real title+body pairs ~55-60% vs greedy's ~30%.
  2. Lance-Williams merge updates (added after two CI hangs exposed it): the first UPGMA implementation recomputed every cross-cluster block mean per candidate pair per merge — O(k⁴). The corpus-scale conformance fixture (20 topics × ~500 near-identical vectors) turned memo_consolidate into ~2h of GIL-holding work: test_mcp_response_budget timed out inside it and the still-running FastMCP worker starved test_output_paths afterwards. Lance-Williams computes the exact same merge in O(k³) at C speed — 0.11s for the k=500 dense component; the two previously-hanging conformance tests now pass locally in 61s total. Regression test pins the dense-component shape with a 30s bound.
  3. Slow-suite ABSORB sweep: 4 tests in 2 files (test_ask_disputes_mlx.py, test_contradict.py) staged contradiction pairs whose members default-on ABSORB (feat: enable MEMO_SAVE_ABSORB by default #262) now merges into one id → ValidationError: a relation requires two distinct memory ids. Invisible to fast CI (-m "not slow"). Fixtures now pin MEMO_SAVE_ABSORB=0; the other 6 pair-seeding files audited clean.

Test plan

  • tests/test_consolidate_average_link.py (6 tests: greedy-split repro, no-chaining, numpy/pure-python equivalence, singletons, empty, dense-component time bound)
  • tests/conformance/test_mcp_response_budget.py + test_output_paths.py locally: 10 passed in 61s (previously 82min/2h19m CI hangs)
  • All 8 pair-seeding slow-suite files: 117 passed
  • quality gate, ruff, mypy, pre-push recall gate

🤖 Generated with Claude Code

jagoff and others added 2 commits August 17, 2026 07:51
_greedy_cluster compared each new memory only to each existing cluster's
FIRST member (frozen forever as its "representative"), never to members
added afterwards — so two above-threshold near-duplicates could land in
different clusters purely because of pull order. Measured on the live
corpus: 38.4% of above-threshold pairs (861/1450) split this way.

Single-linkage (connected components of the threshold graph) was tried
and rejected as the fix: it transitively chains anything reachable
through a path of individually-strong pairs — on the live corpus one
(project, type) bucket alone chained 157 of its 950 memories into one
unmergeable blob.

_cluster_within_scope now clusters via average-link (UPGMA) agglomerative
clustering instead: two clusters merge only when the AVERAGE similarity
across every cross-pair clears the threshold. Raw pair-recall against
the blind threshold is a hair lower than greedy's (58.8% vs 61.6%), but
hand-checked purity on real title+body pairs roughly doubled (~30% ->
~50%) and max cluster size dropped from 157 to 6 — the missing pairs
were mostly the false merges greedy was matching by accident.
synthesize_cross_cluster and dream_distill.run_distill (read-only
insight generation, not merges) keep using the original _greedy_cluster.
…d conformance tests

Third data point: master baseline runs the same conformance tests cleanly
(57s, confirmed); the failing jobs hang specifically in the sequential
--timeout=600 conformance step on tests unrelated to clustering
(test_mcp_response_budget, test_output_paths). If this hangs identically a
third time, treat as confirmed and force the pure-python fallback in
_average_link_cluster instead of the numpy path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jagoff
jagoff force-pushed the worktree-agent-a64307fdf1547bec2 branch from 7684c3f to 070a8d3 Compare August 17, 2026 10:52
jagoff added 2 commits August 17, 2026 08:07
…dates

Root cause of the two CI conformance hangs (82min, 2h19m): the corpus-scale
conformance fixture seeds 20 topics of ~500 near-identical vectors, so
memo_consolidate's threshold graph forms dense components of ~500 members.
_upgma_merge_numpy recomputed every cross-cluster block mean per candidate
pair per merge — an O(k^2) Python pair loop of np.ix_().mean() calls per
merge, O(k^4) element touches overall — turning one MCP tool call into ~2h
of GIL-holding work. test_mcp_response_budget timed out inside it, and the
still-running FastMCP worker thread starved test_output_paths afterwards
(both timeouts landed in unrelated stdlib frames), then spat
'cannot schedule new futures after interpreter shutdown' per cluster at
teardown. The live corpus never showed it: largest real component is 157.

Lance-Williams average-linkage updates compute the exact same UPGMA merge —
new avg sim to C after merging A,B is (|A|·sim(A,C)+|B|·sim(B,C))/(|A|+|B|)
— as an O(k) row update plus an O(k^2) C-level argmax per merge: measured
0.11s for the k=500 dense component (was minutes), and the two previously
hanging conformance tests now pass locally in 61s total. Pure-python
fallback rewritten with the same update rule; the numpy/pure-python
equivalence test still passes. New regression test pins the dense-component
shape with a 30s bound (~300× measured, impossible for an O(k^4) version).
…licate pairs

Sweep of the slow-suite fallout from flipping MEMO_SAVE_ABSORB on by default
(#262): fixtures whose purpose is a *contradiction pair* — two distinct
records the stub embedder deliberately maps to near-identical vectors — now
had the second save absorbed into the first, collapsing the pair into one id
and failing relation creation with 'a relation requires two distinct memory
ids'. Invisible to fast CI: all four failing tests are excluded by
-m 'not slow'. Audited all 8 files that seed pairs via
upsert_open/create_relation_candidate; only these 2 failed (4 tests), the
other 6 already isolate their env or seed genuinely distinct content.
@jagoff
jagoff merged commit 28fc1b0 into master Aug 17, 2026
17 checks passed
@jagoff
jagoff deleted the worktree-agent-a64307fdf1547bec2 branch August 17, 2026 11:24
@jagoff jagoff mentioned this pull request Aug 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant