Skip to content

feat(maintenance): backfill stored connector documents into the memory tree - #136

Merged
YellowSnnowmann merged 5 commits into
tinyhumansai:mainfrom
YellowSnnowmann:feat/6012-connector-tree-backfill
Sep 3, 2026
Merged

feat(maintenance): backfill stored connector documents into the memory tree#136
YellowSnnowmann merged 5 commits into
tinyhumansai:mainfrom
YellowSnnowmann:feat/6012-connector-tree-backfill

Conversation

@YellowSnnowmann

Copy link
Copy Markdown
Contributor

Summary

Adds MemoryMaintenance::backfill_connector_trees — re-files already-stored connector documents into the memory tree.

tinyhumansai/openhuman#6007 (this repo's #134) fixed the routing: connector items now reach mem_tree_chunks as they sync. It could not fix the records already on disk, and neither can a re-sync — the per-item sync gate treats an ingested document as done, so it fetches nothing and creates no tree rows. On the profile that reported the bug that is ~3000 documents, fully embedded in the namespace store and invisible to tree recall, the memory graph, and the source row's ingest status. Recovery today means removing and re-adding the account, which re-fetches everything at the provider's expense.

Tracked as tinyhumansai/openhuman#6012.

Shape

The walk lives in tinymemory-core::backfill, next to the funnel the sync path uses, and the provider is a thin conversion. That placement is the point: a backfilled row and a freshly-synced row have to be the same row, and they only stay that way while one function writes both. The alternative — reconstructing {toolkit}:{connection_id}:{item_id} at a second call site — is precisely what caused #6007.

Namespaces are rebuilt from the source registry, not parsed out of list_namespaces(). The registry is what the writers used (accept_source_items composes source:{toolkit}:{connection_id} from the same row), so reconstructing it the same way cannot drift; parsing a namespace string back into halves has to guess where the toolkit ends.

Legacy skill-{toolkit} namespaces are the awkward half. store_skill_sync takes an _integration_id it never persists, so pre-migration documents record no connection at all — and the tree scope needs one. Where the registry holds exactly one connection for the toolkit there is only one possible answer and it is used; where it holds several, those documents are skipped and the reason is reported. A wrong attribution in a memory system is worse than a missing one.

One change outside the new module

ingest_connector_item_into_tree returned (), which cannot distinguish "I just treed this" from "the tree already had it" — and that distinction is the whole difference between a backfill that reports progress and one that reports nothing. It now returns Option<IngestResult>: None is the skip contract (no resolvable scope), Some(result) carries already_ingested.

Both existing callers drop the payload, so their behaviour is unchanged. The funnel's own test now asserts None on a blank scope rather than discarding the value — a skip that started answering Some would otherwise be silently counted as work done.

Properties worth knowing

  • Idempotent by construction, not by bookkeeping. The ingest pipeline answers already_ingested when its transaction persists nothing, so a second pass writes nothing and an interrupted pass loses nothing. There is no watermark to keep, which is why limit bounds cost rather than carrying a cursor — resuming is just calling again.
  • Expensive, and therefore explicit. list_documents carries no content column, so each document needs its own read, and each ingest embeds its chunks. dry_run reports what a pass would examine while writing nothing — the honest way to show the size of the job before paying for it. Nothing calls this automatically: firing it on upgrade would quietly spend a user's embedding budget, which is its own bug (openhuman#5324).
  • Four counters, not one. "Did nothing" has three different causes a caller must tell apart — the tree already held everything, nothing could be addressed, or there was nothing to look at. Collapsing them would make an unresolvable account read exactly like a fully backfilled one.
  • Corruption still aborts. Per-document failures are tolerated and counted, reusing corruption::escalate_or_count; a corrupt store fails every later document identically, so continuing would burn the whole limit reproducing one error (openhuman#5820).
  • The null driver refuses, matching every other mutating Maintenance member — "backfilled nothing" must not read as work done by a driver that stores nothing.

Test plan

  • cargo fmt --all -- --check
  • cargo clippy --all-targets --all-features -- -D warnings
  • cargo test --all-features

Five new cases in crates/tinymemory-core/src/backfill_tests.rs:

  • a stored document reaches the tree at exactly gmail:conn-1:msg-1 with path_scope gmail:conn-1, having first asserted the tree is empty — that precondition is #6007;
  • a second pass reports already_present: 1, writes nothing, and adds no chunks;
  • targets are rebuilt from the registry, including the legacy namespace when it is unambiguous;
  • the legacy namespace is skipped, by name, when a toolkit has several connections, while both current namespaces stay addressable;
  • a dry run counts without writing, and a bounded pass reports more_pending rather than reading as complete.

Release note for the host

This adds a contract member, so it needs a minor bump rather than a patch — the host cannot call backfill_connector_trees until a release carries it and modules::registry is re-pinned. Calling it against an older pinned artifact answers Unsupported, which is strictly worse than the feature being absent, so the openhuman side (module forwarder, guard family, RPC surface) is deliberately not opened until this lands and ships.

No new capability family — this is a member of the existing Maintenance family, so Capability::ALL is unchanged and the host's capability-count assertion is unaffected. METHODS moves 142 → 143.

Refs tinyhumansai/openhuman#6012, tinyhumansai/openhuman#6007

…y tree

tinyhumansai#134 fixed the routing: connector items reach `mem_tree_chunks` as they sync.
It could not fix the records already on disk, and neither can a re-sync — the
per-item sync gate treats an ingested document as done, so it fetches nothing
and creates no tree rows. Those memories stay fully embedded in the namespace
store and invisible to tree recall, the memory graph and the source row's
ingest status, and recovery today means removing and re-adding the account.

`MemoryMaintenance::backfill_connector_trees` walks the connector namespaces
and feeds each stored document through the funnel the sync path already uses,
so a backfilled row and a freshly-synced row are the same row. The walk lives
in `tinymemory-core::backfill` next to that funnel and the provider is a thin
conversion: reconstructing `{toolkit}:{connection_id}:{item_id}` at a second
call site is exactly what caused openhuman#6007.

Namespaces are rebuilt from the source registry rather than parsed out of
`list_namespaces`, because the registry is what the writers used and cannot
drift, while parsing a namespace back into halves has to guess where the
toolkit ends. Legacy `skill-<toolkit>` documents record no connection at all —
`store_skill_sync` takes an `_integration_id` it never persists — so they are
attached only where the registry holds exactly one connection for the toolkit,
and skipped by name where it holds several. A wrong attribution in a memory
system is worse than a missing one.

`ingest_connector_item_into_tree` now answers `Option<IngestResult>` instead of
`()`. Without it the backfill cannot tell "just treed this" from "the tree
already had it", and that distinction is the difference between reporting
progress and reporting nothing. Both existing callers drop the payload, and the
funnel's own test now asserts `None` on a blank scope rather than discarding
the value.

The member is appended at the tail of `METHODS`, not filed beside
`FlushPending` where its family sits. Member order is wire order, so filing it
with its family would renumber every member after it and make a host built
against v1.13.8 invoke the wrong method — silently. The append-only guard
caught that; the slot is now pinned at 142.

Idempotent by construction: the ingest pipeline answers `already_ingested` when
its transaction persists nothing, so a second pass writes nothing and an
interrupted pass loses nothing. `limit` therefore bounds cost rather than
carrying a cursor. `dry_run` reports what a pass would examine while writing
nothing, because a full pass is one read and one set of chunk embeddings per
document and that cost should be visible before it is paid. Nothing calls this
automatically.

The null driver refuses, as every other mutating Maintenance member does:
"backfilled nothing" must not read as work done by a driver that stores
nothing.

Refs tinyhumansai/openhuman#6012, tinyhumansai/openhuman#6007
@coderabbitai

coderabbitai Bot commented Sep 3, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Team

Run ID: 5d4bb7a5-0906-4faf-a76f-9e74eadb5bf4

Warning

Your free Security trial is over. An organization admin can upgrade to Advanced for continuous pull request security review or dismiss this notice.


Comment @coderabbitai help to get the list of available commands.

@tinysweeper

tinysweeper Bot commented Sep 3, 2026

Copy link
Copy Markdown

How this change flows

0 changed behaviours across 5 relationships. 4 surrounding behaviours are shown (60 graph nodes walked). 38 further behaviours left out to keep the diagram readable.

flowchart LR
  n0["resolve_targets"]:::impacted
  n1["...e_registry_including_the_legacy_namespace"]:::impacted
  n2["...ed_when_a_toolkit_has_several_connections"]:::impacted
  n3["Result"]:::impacted
  n0 -->|uses| n3
  n1 -->|calls| n0
  n1 -->|tests| n0
  n2 -->|calls| n0
  n2 -->|tests| n0
  classDef changed fill:#0d4429,stroke:#238636,color:#e6edf3
  classDef impacted fill:#161b22,stroke:#6e7681,color:#c9d1d9
  classDef flagged fill:#5a1e02,stroke:#d93f0b,color:#ffffff
  classDef blocking fill:#67060c,stroke:#f85149,color:#ffffff
Loading

Green: changed behaviour. Grey: surrounding behaviour. Arrows name the call, use, implementation, or test relationship. Orange: has findings. Red: has a finding that blocks the merge.

tinysweeper 0.1.0

@tinysweeper tinysweeper Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tinysweeper found nothing blocking. Approving.

             $0.0443 · 370,350 in / 11,044 out · 86,272 cached (23%) · openrouter/openai/text-embedding-3-small, deepseek/deepseek-v4-flash, z-ai/glm-5.2 · 756 embedded
critique:    $0.0172 · 170,644 in / 5,023 out  · 17,837 cached (10%) · deepseek/deepseek-v4-flash, z-ai/glm-5.2
security:    $0.0238 · 160,750 in / 5,812 out  · 68,435 cached (43%) · deepseek/deepseek-v4-flash, z-ai/glm-5.2
tests:       $0.0020 · 23,519 in  / 151 out    · 0 cached (0%)       · deepseek/deepseek-v4-flash
description: $0.0013 · 15,437 in  / 58 out     · 0 cached (0%)       · deepseek/deepseek-v4-flash

@tinysweeper tinysweeper Bot added the priority: p3 Whenever. Cosmetic, a nicety, or a cleanup with no user visible effect. label Sep 3, 2026
The module is excluded from the root workspace and carries its own Cargo.lock,
so `cargo check --workspace`, `cargo clippy --all-targets --all-features` and
`cargo test --all-features` at the repo root never compile it. All three were
green with this crate broken in two ways.

- The contract types were never imported into `service/mod.rs`. The identical
  import was needed in `null.rs` and in the tinycortex provider, where the root
  workspace surfaced it immediately; this copy stayed broken and invisible.

- The forwarder went in beside `flush_pending`, where its family sits.
  `#[tinybus::interface]` derives member order from the impl block, and
  `the_served_members_are_exactly_the_published_contract` compares that order
  positionally against `tinymemory_bus::METHODS`. So the same append-only rule
  that governs the wire table governs this block: a member inserted mid-impl
  renumbers every member after it. Moved to the tail, with the reason recorded
  where the next person will be tempted to tidy it back.

The manifest in `lib.rs` also had to learn the name. `#[tinybus::interface]`
serves whatever the impl declares, and `every_served_method_is_declared_in_the_manifest`
fails on a member that is served but undeclared — which is the state where no
host can call it. That list is compared as a set, so it stays grouped with its
family; only the two ordered lists are append-only.

Refs tinyhumansai/openhuman#6012
@tinysweeper tinysweeper Bot added priority: p2 Soon. Real but survivable — a rough edge, a gap, a thing that will bite later. and removed priority: p3 Whenever. Cosmetic, a nicety, or a cleanup with no user visible effect. labels Sep 3, 2026
`engine-containment.sh` (tinyhumansai#18 §C1) fails a `tinymemory-core` file outside
`src/engine/` that names tinycortex in code, and the new backfill test named
`tinycortex::memory::chunks::ListChunksQuery` directly.

The import was copied from `engine/sync_tests.rs`, where that spelling is legal
precisely because that file is inside the seam. Copying it out of the seam is
what broke containment — the same query is reachable as
`crate::store::chunks::ListChunksQuery`, which is how the other non-engine
callers name it.

Refs tinyhumansai/openhuman#6012
@tinysweeper tinysweeper Bot added priority: p3 Whenever. Cosmetic, a nicety, or a cleanup with no user visible effect. and removed priority: p2 Soon. Real but survivable — a rough edge, a gap, a thing that will bite later. labels Sep 3, 2026
The module workspace runs only its own tests, so `tinymemory-core`'s backfill
suite never executes in that lane while its coverage gate still measures core's
production source. `backfill.rs` was landing there as ~270 uncovered lines.

Adding it to the ignore-regex would have cleared the gate and hidden a real
gap, so this covers it instead — and the test earns its place independently:
nothing else exercised service -> provider -> core for this member, which is
the path a host actually calls. The gate was pointing at something true.

The document is written straight through the store client rather than with
`accept_source_items`, deliberately. That member now trees on the way in, so
using it would leave the backfill nothing to do; a document sitting in the
namespace store with no tree row IS the state this feature repairs.

Idempotence is asserted here as well as in core, because a host will offer this
as a button and someone will press it twice.

Refs tinyhumansai/openhuman#6012
`the_manifest_declares_every_method_the_module_serves` compares the module's
manifest against `EXPECTED_METHODS`, a fourth hand-maintained list of member
names — after `tinymemory_bus::METHODS`, the `#[tinybus::interface]` impl block,
and the manifest in `lib.rs`.

It lives in `tests/`, so `cargo test --lib` never ran it. That is the same class
of miss as the module workspace itself: a scope the obvious command silently
steps over, green locally and red in CI.

Compared as a `BTreeSet`, so this entry sits with its family. Only the two
positional lists are append-only, and the comment says which is which so the
next member does not have to rediscover the difference the hard way.

Refs tinyhumansai/openhuman#6012
@YellowSnnowmann
YellowSnnowmann merged commit d489cba into tinyhumansai:main Sep 3, 2026
27 checks passed
YellowSnnowmann added a commit to tinyhumansai/openhuman that referenced this pull request Sep 4, 2026
#6007 fixed the routing for items synced from then on, and #6013 shipped it.
Neither recovers the records already stored: the per-item sync gate treats an
ingested document as done, so a re-sync fetches nothing and creates no tree
rows. On the profile that reported the bug that is ~3000 documents, fully
embedded in the document store and invisible to tree recall, the memory graph
and the source row's ingest status. Recovery today means removing and re-adding
the account, which re-fetches everything at the provider's expense.

The walk itself lives in the engine (tinyhumansai/tinymemory#136), beside the
funnel the sync path uses, so a backfilled row and a freshly-synced row are the
same row. This is the host half: the module forwarder, the guard, and the RPC.

- The forwarder takes the bulk deadline. A pass reads and re-embeds up to its
  whole limit of documents, and the default 30s bus deadline is what made the
  connector sync retry a finished handoff forever.
- The guard takes the write tier, like every other mutating maintenance member.
  A readonly operator may inspect a store; re-filing thousands of its documents
  is not inspection.
- `dry_run` defaults to TRUE at the RPC boundary. A pass costs one read and one
  set of chunk embeddings per document against the user's embedding budget
  (#5324), so a caller that omits the field gets the preview and the write is
  something they ask for.

The response carries four counters rather than one because "did nothing" has
three different causes an operator must tell apart: the tree already held
everything, nothing could be addressed, or there was nothing to look at.

Refs #6012
YellowSnnowmann added a commit to tinyhumansai/openhuman that referenced this pull request Sep 4, 2026
v1.14.0 carries tinyhumansai/tinymemory#136, which adds
`MemoryMaintenance::backfill_connector_trees` — the engine-side walk that
re-files connector documents stored before the #6007 routing fix. The host half
landed in the previous commit and cannot reach the module until this pin moves:
against 1.13.8 the call answers `UnknownMethod`.

A minor bump rather than a patch, because it adds a contract member.

Four pins move together, as they must:

- `vendor/tinymemory` -> the v1.14.0 commit (c253e70 -> d5ac1ed)
- the registry descriptor: version, release_url, and all 11 platform assets
  with the checksums the release published
- `ARTIFACT_CAPABILITIES_PIN`
- `memory_version` / `memory_sha256` in ci-full.yml, ci-lite.yml and
  e2e-reusable.yml (4 sites), which pin the ubuntu-22.04-x86_64 archive
  independently of the registry

Verified rather than assumed: v1.14.0 has #136's merge (d489cba) as an
ancestor and carries the member in both the engine and the bus vocabulary; all
11 checksums were taken from the release's own checksum.toml and cross-checked
back against it, with each archive name and its digest rewritten as one unit;
and `git diff v1.13.8..v1.14.0 -- crates/tinymemory-api/src/capabilities.rs` is
empty, so no family was added — `backfill_connector_trees` is a member of the
existing `Maintenance` family, the advertised list is unchanged, and the host's
capability-count assertion is untouched.

Refs #6012
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

priority: p3 Whenever. Cosmetic, a nicety, or a cleanup with no user visible effect.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant