Topic tree
Why B-Tree invariants instead of flat search
Section titled “Why B-Tree invariants instead of flat search”Semantic similarity can answer “which existing topic is closest to this
item?” — it cannot safely answer “should this be a new topic?”,
“should two topics merge?”, or “is retrieval getting noisy?”. A
growing topic can hold many similar artifacts and still be bad for
retrieval (a Database topic containing migrations, queries, and
transactions).
The tree borrows B-Tree invariants, not its data structure (embeddings have no total order):
Topic node = routing page (centroid + radius + counts)Leaf topic = bucket owning members (≤ maxLeafItems)Internal topic = router over child topics (≤ maxChildren) __root__ ← fixed entry point ┌────┴────┐ Internal Internal ← routers: closest centroid ┌────┴───┐ ┌┴────┐ wins at each level Leaf Leaf Leaf Leaf ← buckets: hold the memories [memories] [memories] [memories] (≤ maxLeafItems each)Topics track local shape — centroid, radius, member count, child count, depth, model — and the golden rule: compare a topic against itself, its children, or its siblings; never against one global threshold.
CF-triplets (the shared primitive)
Section titled “CF-triplets (the shared primitive)”Every centroid/radius computation uses BIRCH CF-triplets — sufficient statistics over a vector set:
CF = { count n, linear sum LS, sum of squares SS }
centroid = LS / nRMS radius = sqrt(max(SS/n − |centroid|², 0))weighted radius = SS − |LS|² / n (sum of squared deviations)The point: these are exact from the triplet alone — no vector rescan. Adding or removing one vector updates the triplet in O(dim); split, merge, rebalance, and concept clustering all derive centroids/radii from triplets. This is the canonical definition — the same primitive appears in Concepts & consolidation.
Insert routing
Section titled “Insert routing”Algorithm — greedy descent:
route_insert(artifact): vector = embed(artifact) node = __root__ while node has children: children = sorted(node.children by topic_key) node = child with closest centroid (ties → smaller topic_key) insert artifact into node refresh centroid/radius/counts upward (CF-triplet updates) if node.member_count > maxLeafItems: enqueue split(node)Deterministic by construction: same input data, model version, and sort
order ⇒ same placement. Ties always break to the lexicographically
smaller topic_key — never to insertion order or memory ids.
Splits
Section titled “Splits”New topics are created only by splits — never by one-off similarity
guesses. A leaf over maxLeafItems runs:
Algorithm — deterministic split (farthest-pair + Lloyd refinement):
leaf over budget (129 members) after deterministic split ───────────────────────────── ───────────────────────── t_x t_x │ farthest-pair ┌───┴───┐ ▼ + Lloyd refine ▼ ▼ 129 members ──────────────────────▶ leaf-a leaf-b (64) (65)
accept ONLY IF: left_count ≥ minLeafItems right_count ≥ minLeafItems weighted_child_radius < parent_radius (children are cleaner)
child keys = hashes of the CANONICAL vectors (never memory ids)The acceptance rule is the anti-thrash guard: only split if the children
are cleaner than the parent. A rejected split marks the topic
split_deferred — it stays fully routable while a durable job retries.
A leaf that keeps rejecting hits the hard-limit backstop
(splitDeferredHardLimit = 4× the budget) and is force-split.
Determinism at the root: child keys are hashes of the canonical vectors, never memory ids — so the same embeddings produce the same tree across runs, machines, and databases (regression-tested).
Internal nodes split the same way when child_count > maxChildren,
with child topics as the units.
Merge & borrow
Section titled “Merge & borrow”Underflow (member_count < minLeafItems) self-heals without thrash:
underflow (10 < minLeafItems 32) │ ├─▶ borrow ── nearest larger sibling donates │ min(needed, donor) at a time — donor-min rule │ (skip manual_lock members) │ └─▶ merge ── consume the nearest sibling ONLY when the result ≤ maxLeafItems (a merge must never immediately re-overflow) otherwise stay underfilledmanual_lock memberships are never auto-moved — a locked member stays
put unless a reviewer transfers it.
Rebalance
Section titled “Rebalance”Even valid siblings can become uneven:
Auth OAuth 34 items Sessions 126 items Tokens 118 itemsRule: rebalance when largest_sibling_count > 2 × smallest_sibling_count.
Move borderline members (closer to the small sibling’s centroid than the
large one’s, manual_lock = false, radii preserved or improved) from the
large side to the small side. Cooldown-gated (rebalanceCooldownMs) to
avoid churn.
Reserved topics
Section titled “Reserved topics”__root__ the tree's fixed entry point (never removed)__unassigned_active__ members without embeddings / ambiguous routes — batched and promoted only through the same deterministic split logic