Files
openhuman/src
sanil-23andClaude Opus 4.7 8e55ddd71e feat(memory): source trees + bucket-seal foundation (#709)
Phase 3a of the memory architecture (#711 umbrella). Lifts admitted leaves
into a per-source hierarchy so Phase 4 retrieval has a tree to walk.

## What's in this PR

* **Schema** — three new tables in `chunks.db` alongside Phase 1/2:
  `mem_tree_trees`, `mem_tree_summaries`, `mem_tree_buffers`. Plus a
  `parent_summary_id` backlink column on `mem_tree_chunks`. All migrations
  are additive and idempotent (`ALTER TABLE ADD COLUMN`, same pattern as
  Phase 2).
* **`source_tree/` module** — isolated subdir; does not touch the legacy
  `openhuman::memory` layer.
  - `types.rs` — `Tree`, `SummaryNode`, `Buffer`, `TreeKind` (Source/Topic/Global),
    `TreeStatus`. Constants: `TOKEN_BUDGET = 10_000`, `DEFAULT_FLUSH_AGE_SECS = 7d`.
  - `store.rs` — CRUD for all three tables via the shared `with_connection`
    entry point. Writes are transactional; summary insert is idempotent
    on primary key (INSERT OR IGNORE).
  - `registry.rs` — `get_or_create_source_tree(scope)` — idempotent lookup
    keyed on `(kind, scope)`.
  - `bucket_seal.rs` — `append_leaf` + cascade: push a leaf into L0, seal
    when `token_sum >= TOKEN_BUDGET`, recurse upward. One seal = one
    transaction; children get a parent backlink (leaves via
    `parent_summary_id`, summaries via `parent_id`). Tree's `max_level`
    and `root_id` bump on root split.
  - `flush.rs` — `flush_stale_buffers(max_age)` force-seals any buffer
    whose `oldest_at` is older than `max_age`. Primitive only; wiring
    into a scheduler is out of scope.
  - `summariser/` — `Summariser` trait + `InertSummariser` fallback
    (concatenate children, union entities preserving first-seen order,
    hard-truncate to budget). Trait is async; Ollama implementation slots
    in later without breaking callers.
* **Ingest wiring** — `tree/ingest.rs::persist` now calls `append_leaf`
  once per kept chunk after chunks + scores land. Failures are logged at
  warn level and do not fail the ingest — leaves are already persisted
  and the next flush can still rebuild the tree.

## Concurrency / LLM

The async summariser call happens OUTSIDE any DB transaction, so a slow
LLM never holds SQLite locks. Blocking DB calls inside `append_leaf` are
acceptable for Phase 3a because the Inert summariser does no real I/O;
when a networked summariser lands, wrap the DB calls in
`tokio::task::spawn_blocking`.

## Tests (23 new, all green)

* `types` — enum round-trips, buffer staleness predicates
* `store` — tree insert + unique scope, summary insert + idempotence,
  buffer upsert/clear, stale-buffer ordering
* `registry` — `get_or_create` idempotence, distinct scopes yield distinct
  trees, id prefix format
* `summariser/inert` — provenance-prefixed concat, entity union with
  order preservation, budget truncation, empty-content skip
* `bucket_seal` — append-below-budget is buffered only (no seal);
  12k-token cross-over produces an L1 summary with correct `child_ids`,
  L0 cleared, L1 carries the new summary id, tree `max_level`/`root_id`
  updated, leaf → parent backlink populated
* `flush` — stale buffer force-seals under budget; recent buffer is a no-op

Broader `memory::tree` suite: 160 tests pass (23 new + 137 existing Phase
1/2), no regressions.

## Stacked on #775

This PR applies on top of `feat/708-memory-llm-ner` (PR #775, Phase 2
LLM-NER follow-up). Merge #775 first; this branch will rebase cleanly
onto main.

## Out of scope (tracked separately)

* **3b — Global activity digest tree** — time-aligned cross-source recap
* **3c — Topic trees** — hotness-driven per-entity materialisation
* **Phase 4 (#710)** — retrieval / query / gate

Closes part of #709 (source-tree foundation).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 20:26:31 +05:30
..