test(memory_tree): unit tests for retrieval RPC handlers (#710)#839
Conversation
…inyhumansai#710) Expose the source/global/topic summary trees built in Phases 3a-3c as six LLM-callable JSON-RPC primitives under the existing `memory_tree` namespace. Each tool is deterministic and scope-specific; orchestration (which tool to call, how to combine results) is left to the LLM — no classifier, gate, or composer in this phase. Tools registered (all return JSON-serialisable `QueryResponse` / `Vec<RetrievalHit>` / `Vec<EntityMatch>`): - `openhuman.memory_tree_query_source` — per-source summary retrieval - `openhuman.memory_tree_query_global` — window-scoped cross-source digest - `openhuman.memory_tree_query_topic` — entity-scoped retrieval across trees - `openhuman.memory_tree_search_entities` — free-text LIKE search over canonical ids - `openhuman.memory_tree_drill_down` — walk summary children - `openhuman.memory_tree_fetch_leaves` — batch chunk hydration Module layout follows the light-mod.rs + controller-only exposure rules: `retrieval/` ships one file per tool, shared `types.rs` with `RetrievalHit` / `QueryResponse` / `EntityMatch`, `rpc.rs` for handler bodies, and `schemas.rs` for the controller registration. Wiring into `src/core/all.rs` follows the Phase 1 ingest pattern. Design decisions worth flagging: - `RetrievalHit` is a uniform shape across summaries and leaves so the LLM sees one schema regardless of which tool ran; `node_kind` discriminates `Leaf` vs. `Summary`. - `query_source` without a `source_id`/`source_kind` filter returns every source tree — useful for "what trees exist?" exploration. - `scope_matches_kind` uses platform-prefix heuristics (slack/gmail/notion) in addition to literal `chat:` / `email:` / `document:` prefixes so existing Phase 3 scopes like `slack:#eng` classify correctly. - `query_topic` combines the entity index (lookup_entity) with the topic tree's root if one has materialised, sorted (score DESC, time DESC). - `query_global` is a thin wrapper on `global_tree::recap::recap` — returns one synthetic summary hit whose `child_ids` point at the folded daily/weekly summaries for drill-down. - Summary-level entities/topics come directly from the stored summary row — no re-derivation at query time (honours the Phase 3 summariser contract; `InertSummariser` emits empty entities/topics). - `fetch_leaves` caps batches at 20 with silent truncation + warn log so a runaway LLM call can't stall the runtime. - `drill_down` does BFS up to `max_depth` levels; unknown ids and leaf inputs return empty vecs rather than errors so the LLM can recover. Small pre-existing bug fixed in passing: `src/openhuman/threads/ops.rs` test module referenced `collapse_whitespace` without importing it, blocking `cargo test --lib`. Added the missing `use` in the test-only scope. Testing: - 46 new unit/integration tests in `memory::tree::retrieval` (all pass). - Full `memory::tree` suite: 268 tests pass, 0 regressions. - `cargo fmt --all` clean. - `cargo clippy --lib` clean for the new module (pre-existing warnings unchanged). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nk (tinyhumansai#710) Amends Phase 4 (commit 956c66b) with the embedding layer. Every new chunk and summary now carries a 768-dim embedding from a local Ollama `nomic-embed-text` endpoint; retrieval tools `query_source` and `query_topic` accept an optional `query` string and rerank candidates by cosine similarity when it's provided. ## Changes * **`score/embed/` module** — new. `Embedder` trait (async), constant `EMBEDDING_DIM = 768`, `OllamaEmbedder` HTTP client, `InertEmbedder` zero-vector fallback, `pack_embedding` / `unpack_embedding` helpers, `cosine_similarity`, and `build_embedder_from_config` factory. * **Schema migration** — idempotent `ALTER TABLE mem_tree_summaries ADD COLUMN embedding BLOB` in `tree/store.rs::with_connection`. * **SummaryNode** — adds `embedding: Option<Vec<f32>>` field (`#[serde(default)]`), wired through `insert_summary_tx`, `row_to_summary`, `get_summary`, `list_summaries_at_level`, plus new `set_summary_embedding` / `get_summary_embedding` accessors. * **Ingest** — `tree/ingest.rs::persist` embeds each kept chunk serially before opening the SQLite write tx. If any embed call fails, the whole batch bails — no partial writes. * **Seal** — `source_tree/bucket_seal.rs::seal_one_level` embeds the summariser output before opening the write tx; embedding is written in the same transaction as the `SummaryNode`. * **Config** — new `storage_memory.rs::MemoryTreeConfig` fields: `embedding_endpoint: Option<String>` (default `http://localhost:11434`), `embedding_model: Option<String>` (default `nomic-embed-text`), `embedding_timeout_ms: Option<u64>` (default 10_000), `embedding_strict: bool` (default `true` — fail ingest/seal if embedder is unavailable and strict is on). Env var overrides via `load.rs`: `OPENHUMAN_MEMORY_EMBED_{ENDPOINT,MODEL,TIMEOUT_MS,STRICT}`. * **Retrieval** — `query_source` and `query_topic` gain `query: Option<String>`. When `Some`, candidates are fetched with their embeddings, a single Ollama call embeds the query, and results are reranked by cosine DESC. NULL-embedding rows (legacy Phase 1-3 data) go to the bottom rather than being dropped. When `None`, existing recency/score ordering is used. * **JSON-RPC schemas** — `retrieval/schemas.rs` + `retrieval/rpc.rs` accept the new optional `query` field on `query_source` / `query_topic` methods. ## Legacy data Existing chunks and summaries from Phases 1-3 have NULL embeddings. New writes require `Some(...)` under `embedding_strict=true`; retrieval handles legacy NULLs by appending them after embedded candidates in rank order. A `backfill_embeddings` admin helper is a follow-up — not in this commit. ## Design decisions worth review * **Serial embedding per chunk at ingest** — ~50-200ms per chunk against local Ollama. Parallel batching is a follow-up; starting serial to keep error handling simple. * **No embedding cache** — every call hits Ollama. Caching can be added later behind a feature flag. * **Brute-force cosine** — fine up to ~100k nodes (few ms). Vector index / ANN is a follow-up when scale warrants it. * **`embedding_strict=true` by default** — ingest/seal fails loudly if Ollama is down. Consistent with the user-requested invariant that every new chunk and summary must carry an embedding. * **InertEmbedder fallback** — used in tests for determinism and triggered at runtime only when `embedding_strict=false` and the user hasn't configured Ollama. Emits a warn log in that case. ## Tests `cargo test --lib memory::tree` — 301 passed / 0 failed (+33 new vs 268 baseline from the Phase 4 retrieval PR). Unit coverage: cosine similarity edge cases, pack/unpack round-trip, Ollama HTTP happy path + transport error + dim mismatch, InertEmbedder determinism, ingest/seal write paths under both strict and lenient modes, retrieval rerank ordering with mixed embedded + NULL candidates. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
tinyhumansai#710) Adds `query: Option<String>` and `limit: Option<usize>` to the `memory_tree_drill_down` tool and its Rust API. When `query` is provided, the BFS-walked children are reranked by cosine similarity against the query embedding — mirrors the pattern just shipped for `query_source` and `query_topic` in the embedding amend. Closes a small gap called out in PR tinyhumansai#831 review: summaries with many children (e.g. an L2 over 25+ L1 summaries) would dump everything into the LLM context; with a query, the LLM can surface top-K relevant children by semantic similarity. ## Behaviour * `query: None` → BFS order preserved (unchanged). * `query: Some(q)` → embed q, fetch per-child embedding during the blocking walk (avoids a second DB round-trip), cosine DESC sort. Children with no stored embedding (legacy rows) go to the bottom preserving their relative BFS order. * `limit` applies AFTER rerank so top-K is relevance-based when `query` is set, BFS-based otherwise. * Unknown / leaf / max_depth=0 paths unchanged (empty vec, no error). ## Changes * `retrieval/drill_down.rs` — refactored `walk()` into `walk_with_embeddings()` returning `(Vec<RetrievalHit>, Vec<Option<Vec<f32>>>)` so the rerank pass doesn't have to round-trip through the DB a second time. Added `rerank_by_semantic_similarity` helper mirroring the `query_source` / `query_topic` implementation. * `retrieval/rpc.rs::DrillDownRequest` — adds optional `query` and `limit` fields (`#[serde(default)]` so existing callers unaffected). * `retrieval/schemas.rs` — updated controller schema + description so the LLM sees the new params and learns when to use them. * Test module — new tests `query_with_limit_truncates_after_rerank` and `query_without_limit_returns_all_children`; existing callsites updated for the new signature. ## Tests `cargo test --lib memory::tree` — 303 passed / 0 failed (+2 vs 301 baseline from the embedding amend). No regressions. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
) Six findings from CodeRabbit on PR tinyhumansai#831. All addressed in this commit; one (missing JSON-RPC E2E tests) deferred to a follow-up since it touches the external test harness. ## Fixes * **🔴 `source.rs:scope_matches_kind`** — hardcoded list of 4 chat platforms silently excluded chunks from irc, matrix, mattermost, lark, linq, signal, imessage, dingtalk, qq. Replaced with a central `PLATFORM_KINDS` registry that now covers 15 chat providers, 5 email providers, and 7 document providers. Adding a new integration now touches one place. * **🟠 `drill_down.rs`** — walk was DFS (`Vec::pop` → LIFO), not BFS as the doc comment claimed. Switched to `VecDeque` with `pop_front` + `push_back` so sibling summaries surface before descendants. * **🟠 `rpc.rs` log redaction** — `RpcOutcome::single_log` messages were emitting raw `entity_id`, `query`, and `node_id` which can carry emails, handles, or workspace hints. Replaced with: - query_source: `has_source_id`, `source_kind`, `has_query`, `hits` - query_topic: `entity_kind` prefix only, `has_query`, `hits` - search_entities: `query_len`, `has_kinds`, `n` - drill_down: `node_kind` prefix, `depth`, `has_query`, `limit`, `n` Counts and structural metadata stay for debuggability; values omitted. * **🟠 `search.rs` blank query** — empty/whitespace query became `LIKE '%%'` which is a full entity-index dump. Trim first, return empty vec on empty. Demoted the input log from info→debug and replaced the raw query string with its length. * **🟠 `schemas.rs` output shape** — `query_source` / `query_global` / `query_topic` declared a single output field `response: QueryResponse`, but the handlers serialize `QueryResponse` flat. Extracted a `query_response_outputs()` helper that declares the three top-level fields (`hits`, `total`, `truncated`) so the controller schema matches the wire. * **🟠 `topic.rs` duplicate hits** — the same node could appear multiple times across `lookup_entity` results (one row per occurrence) plus overlap with the topic-tree root summary, inflating `total` and wasting result slots. Now dedupes by `node_id` via `HashMap` merge (higher score wins, newer `time_range_end` breaks ties) before sorting and truncating. ## Deferred **🛠️ `rpc.rs:238` missing JSON-RPC E2E** — adding coverage in `tests/json_rpc_e2e.rs` needs harness changes outside this PR's scope. Will land as a follow-up. Logged in tinyhumansai#831. ## Tests `cargo test --lib memory::tree` — 303 passed / 0 failed (same as pre-fix baseline). All existing unit tests still cover the fixed call sites; the behavioural changes preserve the input→output contract. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…#710) CI's `Rust Tests + Quality` was failing on `json_rpc_memory_tree_end_to_end` because Phase 4 makes embedding mandatory at ingest by default, and Ollama isn't running in the CI environment: memory_tree_ingest: JSON-RPC error: ingest: embed chunk_id=... during ingest ## Fixes * **`tests/json_rpc_e2e.rs`** — add `EnvVarGuard::set(key, value)` helper and use it in `json_rpc_memory_tree_end_to_end` to export: - `OPENHUMAN_MEMORY_EMBED_STRICT=false` - `OPENHUMAN_MEMORY_EMBED_ENDPOINT=""` - `OPENHUMAN_MEMORY_EMBED_MODEL=""` so the factory falls back to `InertEmbedder` (zero vectors) instead of trying to reach Ollama. * **`src/openhuman/config/schema/load.rs`** — previously the env var handlers for embedding endpoint/model only updated config when the value was non-empty, so setting them to `""` was a no-op and the default (`http://localhost:11434` + `nomic-embed-text`) survived. Now empty value explicitly clears the field to `None`, letting CI and test environments opt into InertEmbedder without editing config.toml. Production config is unaffected because prod doesn't set the env var to empty. ## Tests `cargo test --test json_rpc_e2e json_rpc_memory_tree_end_to_end` — 1 passed / 0 failed locally. This is the test CI was red on. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
) Self-audit of commit `de2afd1f` surfaced two real gaps and two missing regression tests. All four addressed here. ## Fixes * **PII in module-level logs** — `de2afd1f` redacted `RpcOutcome` log messages (the one CodeRabbit cited) but the upstream retrieval modules still emitted raw identifiers at `log::info!`: - `retrieval::source::query_source` logged full `source_id` (e.g. `slack:#<channel>`) - `retrieval::topic::query_topic` logged full `entity_id` (e.g. `email:<addr>`) - `retrieval::drill_down::drill_down` logged full `node_id` (e.g. `chat:slack:#<channel>:<seq>`) - `retrieval::fetch::fetch_leaves` was at `info!` level (demoted to `debug!` for consistency; only logs counts) Now all four mirror the RPC layer: log only structural prefixes (`source_kind`, `entity_kind`, `node_kind`) and presence flags. ## New regression tests * **`drill_down::walk_visits_siblings_before_descendants_bfs_order`** Constructs a 2-level tree directly via `insert_summary_tx` and asserts BFS ordering — every L1 summary index must come before every leaf index. The previous single-level fixture couldn't distinguish BFS from DFS, so the `Vec::pop()` → DFS bug flagged on tinyhumansai#831 review could silently regress. This test would fail on the pre-fix implementation. * **`topic::duplicate_node_is_deduplicated_across_index_and_topic_tree_root`** Seeds a topic tree whose root summary is also indexed under its own entity_id (the exact scenario CodeRabbit cited). Without the HashMap-based dedup in `query_topic`, `resp.hits` would contain the summary twice and `resp.total == 2`. After the fix: one hit, `total == 1`. ## Verification / gaps NOT closed * Verified `search_entities`, `drill_down`, and `fetch_leaves` schema output shapes already match their handler serialisations — no bug there; the `query_*` schema mismatch from `de2afd1f` was isolated. * `PLATFORM_KINDS` drift detection (enumerate provider directory + assert every provider has an entry) is still a follow-up — the current registry covers the 9 platforms CodeRabbit named plus 18 others but nothing prevents future drift. ## Tests `cargo test --lib memory::tree` — 305 passed / 0 failed (+2 regression tests vs 303 baseline). No regressions. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ansai#710) Covers the six Phase 4 JSON-RPC entry points (query_source, query_global, query_topic, search_entities, drill_down, fetch_leaves) with 15 unit tests exercising parameter parsing, default fallbacks, SourceKind / EntityKind validation, RpcOutcome envelope shape, and PII-redacted log formatting. Closes CodeRabbit's deferred RPC-coverage gap from tinyhumansai#831. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
📝 WalkthroughWalkthroughThis PR introduces Phase 4 memory-tree embedding and retrieval infrastructure. It adds configurable embedding support (Ollama and inert), integrates embeddings into ingest/seal/digest pipelines, extends the database schema for persistence, and introduces a comprehensive retrieval module with six query/search functions plus RPC handlers and integration tests. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 8
🧹 Nitpick comments (11)
src/openhuman/memory/tree/topic_tree/routing.rs (1)
129-132: Optional: centralize embedding-test overrides in a shared helper.The same
memory_treeembedding-disable setup appears in multiple test modules; a small shared test helper would reduce drift.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/openhuman/memory/tree/topic_tree/routing.rs` around lines 129 - 132, Several test modules duplicate the embedding-disable setup (cfg.memory_tree.embedding_endpoint = None; cfg.memory_tree.embedding_model = None; cfg.memory_tree.embedding_strict = false); extract this into a shared test helper such as a function (e.g., disable_memory_tree_embedding(cfg: &mut Config) or tests::helpers::disable_embedding_overrides) and replace the inline assignments with a call to that helper from the tests that currently modify cfg.memory_tree.embedding_endpoint, cfg.memory_tree.embedding_model, and cfg.memory_tree.embedding_strict so all tests use the centralized helper.src/openhuman/memory/tree/source_tree/types.rs (1)
126-134: Lock in backward-compatible deserialization with a unit test.
#[serde(default)]is the right compatibility mechanism here. Please add a serde test for both missingembeddingand explicitembedding: nullto prevent regressions.As per coding guidelines: "Ship unit tests and coverage for behavior you are adding or changing before building additional features on top".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/openhuman/memory/tree/source_tree/types.rs` around lines 126 - 134, Add a unit test that verifies the serde(default) behavior for the embedding field: deserialize two JSON payloads—one that omits the embedding key and one that has "embedding": null—into the struct that declares pub embedding: Option<Vec<f32>> (the struct in types.rs where embedding is defined) using serde_json::from_str and assert the resulting struct.embedding is None in both cases; ensure the test is #[test] and placed with the existing serde/unit tests to lock in backward-compatible deserialization.src/openhuman/memory/tree/score/mod.rs (1)
8-8: Consider splittingscore/mod.rs; it’s over the size guideline.This module is already beyond the ~500-line target, so it would be a good time to move operational sections into sibling files and keep
mod.rsmore composition-focused.As per coding guidelines: "Source files should be ≤ ~500 lines; split modules when growing to improve maintainability".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/openhuman/memory/tree/score/mod.rs` at line 8, The score/mod.rs file has grown too large—split its operational code into sibling modules and keep mod.rs as a thin composition file: create new files (e.g., scorer.rs for core scoring logic, utils.rs for helpers, types.rs for structs/enums) and move the corresponding functions, impls, and type definitions out of mod.rs; then update mod.rs to declare and re-export those modules (e.g., pub mod scorer; pub mod utils; pub mod types; pub use scorer::Scorer; pub use types::ScoreType) and adjust any internal use paths in moved code to reference the new module names (ensure references to existing pub mod embed remain intact). Ensure no behavior changes and run tests to confirm compilation.src/openhuman/memory/tree/store.rs (1)
448-453: Add a focused migration test formem_tree_summaries.embedding.Please add a test that starts from a pre-column DB shape and verifies
with_connectionadds the nullableembeddingcolumn safely.As per coding guidelines: "Ship unit tests and coverage for behavior you are adding or changing before building additional features on top".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/openhuman/memory/tree/store.rs` around lines 448 - 453, Add a unit migration test that creates a temporary DB in the pre-column shape (i.e., a mem_tree_summaries table without the embedding column), opens it via the existing with_connection path that triggers add_column_if_missing, and then asserts the embedding column now exists and is nullable (e.g., PRAGMA table_info or equivalent inspection). Locate the test near other migration tests in the same module and call the same helpers used for DB setup/teardown; reference the functions with_connection and add_column_if_missing and the mem_tree_summaries table name so the test exercises the exact migration branch that adds the "embedding" BLOB column. Ensure the test fails if the column is missing or non-nullable and runs in isolation (temporary file or in-memory DB).src/openhuman/config/schema/load.rs (1)
855-886: Add focused unit tests for new memory-tree env overrides.This block introduces non-trivial parsing/clearing behavior (
"" => None, strict bool parsing, timeout > 0). Please add direct tests in this module to lock semantics and avoid regressions.💡 Suggested test addition
#[test] +fn apply_env_overrides_memory_embed_fields() { + let _g = ENV_LOCK.lock().unwrap(); + clear_env(&[ + "OPENHUMAN_MEMORY_EMBED_ENDPOINT", + "OPENHUMAN_MEMORY_EMBED_MODEL", + "OPENHUMAN_MEMORY_EMBED_TIMEOUT_MS", + "OPENHUMAN_MEMORY_EMBED_STRICT", + ]); + + let mut cfg = Config::default(); + + unsafe { + std::env::set_var("OPENHUMAN_MEMORY_EMBED_ENDPOINT", " "); + std::env::set_var("OPENHUMAN_MEMORY_EMBED_MODEL", ""); + std::env::set_var("OPENHUMAN_MEMORY_EMBED_TIMEOUT_MS", "15000"); + std::env::set_var("OPENHUMAN_MEMORY_EMBED_STRICT", "false"); + } + cfg.apply_env_overrides(); + + assert_eq!(cfg.memory_tree.embedding_endpoint, None); + assert_eq!(cfg.memory_tree.embedding_model, None); + assert_eq!(cfg.memory_tree.embedding_timeout_ms, Some(15_000)); + assert_eq!(cfg.memory_tree.embedding_strict, false); + + clear_env(&[ + "OPENHUMAN_MEMORY_EMBED_ENDPOINT", + "OPENHUMAN_MEMORY_EMBED_MODEL", + "OPENHUMAN_MEMORY_EMBED_TIMEOUT_MS", + "OPENHUMAN_MEMORY_EMBED_STRICT", + ]); +}As per coding guidelines, “Ship unit tests and coverage for behavior you are adding or changing before building additional features on top”.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/openhuman/config/schema/load.rs` around lines 855 - 886, Add focused unit tests in this module that exercise the env-override logic for memory_tree: set OPENHUMAN_MEMORY_EMBED_ENDPOINT and OPENHUMAN_MEMORY_EMBED_MODEL to empty string and non-empty values and assert that memory_tree.embedding_endpoint and memory_tree.embedding_model become None or Some(...) accordingly; set OPENHUMAN_MEMORY_EMBED_TIMEOUT_MS to "0", a valid positive number, and a non-numeric value and assert embedding_timeout_ms is only Some(n) when n>0; set OPENHUMAN_MEMORY_EMBED_STRICT to various truthy/falsey inputs and assert parse_env_bool("OPENHUMAN_MEMORY_EMBED_STRICT", &flag) drives memory_tree.embedding_strict properly; each test should manipulate the env vars, invoke the code path that applies these env overrides (the block that assigns self.memory_tree.embedding_*), and clean up env state between tests to avoid leakage.src/openhuman/memory/tree/retrieval/search.rs (1)
101-130: This query will full-scanmem_tree_entity_indexas the table grows.
LOWER(entity_id) LIKE '%…%' OR LOWER(surface) LIKE '%…%'prevents the existingentity_idindex from helping, and the table does not have a matching index forsurface/lowercased lookups. On a retrieval RPC that an LLM may call repeatedly, latency will trend with total entity rows. Consider moving this to FTS/trigram-style search, or storing normalized search columns with supporting indexes if substring matching is required.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/openhuman/memory/tree/retrieval/search.rs` around lines 101 - 130, The SQL in search.rs (the sql String built around mem_tree_entity_index using LOWER(entity_id) LIKE ?1 OR LOWER(surface) LIKE ?1 with pattern) forces full table scans and will not use the entity_id index as the table grows; replace this with a search that uses an indexed path: either switch to FTS (create an FTS virtual table on surface and entity_id and query MATCH) or add normalized columns (e.g., surface_search, entity_id_search) maintained when writing and create indexes on them, then change the WHERE in the construction that builds sql/params (variables sql, params, pattern, and the kinds branch) to use equality/prefix/trigram-capable operators that will hit indexes (or use the FTS MATCH clause) and keep the existing entity_kind IN handling; ensure LIMIT parameter remains and update any parameter placeholders accordingly.src/openhuman/memory/tree/global_tree/seal.rs (1)
189-198: Add one regression assertion for the new summary embedding path.
seal_one_levelnow persistsembedding: Some(...), but the nearby tests only switch the config to the inert embedder; they never verify that the sealed weekly summary actually got an embedding. A singleweekly.embedding.is_some()/ expected-length check in the existing threshold test would pin the behavior this change introduced.As per coding guidelines "Ship unit tests and coverage for behavior you are adding or changing before building additional features on top".
Also applies to: 218-218
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/openhuman/memory/tree/global_tree/seal.rs` around lines 189 - 198, Add a regression assertion in the existing threshold test that verifies the new embedding path added by seal_one_level actually persisted an embedding: after running the code that calls seal_one_level (which uses build_embedder_from_config and embed), assert weekly.embedding.is_some() and optionally assert the embedding length matches the expected vector size (or non-empty) so the test will fail if embedding persistence regresses; update the existing threshold test that switches the config to the inert embedder to include this check (and repeat for the analogous assertion referenced at the other location).src/openhuman/memory/tree/retrieval/global.rs (1)
39-47: Blocking DB call on async path.
get_or_create_global_treeperforms synchronous SQLite operations (per context snippet fromregistry.rs) but is called directly from the asyncquery_globalfunction withoutspawn_blocking. This can block the Tokio runtime, especially on first call when tree creation occurs.Consider wrapping in
spawn_blockingfor consistency with other retrieval functions in this PR.♻️ Proposed fix
+ let config_owned = config.clone(); + let tree = tokio::task::spawn_blocking(move || get_or_create_global_tree(&config_owned)) + .await + .map_err(|e| anyhow::anyhow!("query_global tree lookup join error: {e}"))??; - let tree = get_or_create_global_tree(config)?; let hits = recap_to_hits(recap_out, &tree.id, &tree.scope);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/openhuman/memory/tree/retrieval/global.rs` around lines 39 - 47, get_or_create_global_tree is a synchronous SQLite operation being called directly from the async query_global path; to avoid blocking the Tokio runtime, run the DB call inside tokio::task::spawn_blocking and await it instead of calling get_or_create_global_tree(...) inline. Move the call into a spawn_blocking closure (capturing config), unwrap or map the JoinHandle result into the expected Result<Tree, _> and propagate errors the same way as before, then pass the returned tree to recap_to_hits(tree.id, &tree.scope, ...) as before; ensure types and error conversions match the existing query_global flow.src/openhuman/memory/tree/retrieval/topic.rs (1)
174-199: N+1 blocking task spawns may impact performance.Each hit spawns a separate
spawn_blockingtask for embedding lookup (lines 181-188). WithLOOKUP_HEADROOM = 200hits, this creates up to 200 task spawns and context switches. Consider batching the DB lookups into a singlespawn_blockingcall.♻️ Sketch of batched approach
+ // Batch all embedding lookups in a single spawn_blocking call + let node_ids: Vec<(String, NodeKind)> = hits.iter().map(|h| (h.node_id.clone(), h.node_kind)).collect(); + let config_owned = config.clone(); + let embeddings: Vec<Option<Vec<f32>>> = tokio::task::spawn_blocking(move || -> Result<Vec<Option<Vec<f32>>>> { + node_ids.iter().map(|(node_id, node_kind)| { + match node_kind { + NodeKind::Summary => src_store::get_summary_embedding(&config_owned, node_id), + NodeKind::Leaf => get_chunk_embedding(&config_owned, node_id), + } + }).collect() + }) + .await + .map_err(|e| anyhow::anyhow!("embedding batch fetch join error: {e}"))??; + + let mut decorated: Vec<(f32, bool, RetrievalHit)> = hits + .into_iter() + .zip(embeddings) + .map(|(h, emb)| { /* same logic */ }) + .collect(); - let mut decorated: Vec<(f32, bool, RetrievalHit)> = Vec::with_capacity(hits.len()); - for h in hits { - // ... per-hit spawn_blocking - }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/openhuman/memory/tree/retrieval/topic.rs` around lines 174 - 199, The loop currently spawns a separate tokio::task::spawn_blocking per hit (inside the for over hits) causing O(N) blocking-task overhead; instead collect the node_ids and node_kinds from hits and perform a single spawn_blocking that calls src_store::get_summary_embedding or get_chunk_embedding for each id (returning a Vec<Option<Vec<f32>>>), then after .await iterate the returned embeddings to compute cosine_similarity(&query_vec, &v) and build the decorated Vec<(f32, bool, RetrievalHit)>; update references to NodeKind, get_chunk_embedding, src_store::get_summary_embedding, query_vec, cosine_similarity, and RetrievalHit so the batched result aligns with the original hit order.src/openhuman/memory/tree/score/embed/mod.rs (1)
23-145: Keepembed/mod.rsexport-focused.This
mod.rsnow owns trait definitions, cosine math, blob codecs, and the test suite. That makes the module heavier than the repo’smod.rsconvention; the operational pieces should move into sibling files and be re-exported here.Based on learnings: Keep domain
mod.rsfiles light and export-focused; place operational code in sibling files (ops.rs,store.rs,schedule.rs,types.rs) and re-export frommod.rs.Also applies to: 146-221
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/openhuman/memory/tree/score/embed/mod.rs` around lines 23 - 145, This file is too heavy for a mod.rs — move the operational implementations into sibling modules and keep mod.rs export-focused: create new files (e.g., ops.rs or math.rs for cosine_similarity, codec.rs for pack_embedding/unpack_embedding/pack_checked/decode_optional_blob, types.rs for EMBEDDING_DIM and Embedder trait) and re-export their public items from this mod via pub use (retain pub use factory::..., inert::..., ollama::...); ensure function/trait names (Embedder, EMBEDDING_DIM, cosine_similarity, pack_embedding, unpack_embedding, pack_checked, decode_optional_blob) are public in their new modules so mod.rs only declares pub mod X and pub use X::{Embedder, EMBEDDING_DIM, cosine_similarity, pack_embedding, unpack_embedding, pack_checked, decode_optional_blob} to restore the original public API.src/openhuman/memory/tree/retrieval/rpc.rs (1)
268-560: Split the test module out ofrpc.rs.Adding the full handler test suite here pushes the file past the repo’s size target and mixes production RPC code with a large amount of test-only logic. Moving the tests to a sibling module keeps this file easier to navigate and maintain.
As per coding guidelines, "src/**/*.rs: Source files should be ≤ ~500 lines; split modules when growing to improve maintainability".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/openhuman/memory/tree/retrieval/rpc.rs` around lines 268 - 560, The file `rpc.rs` contains a large #[cfg(test)] mod tests { ... } block that should be split into a test-only sibling module to keep the production file small; move the entire tests block out into a new file named rpc_tests.rs (adjacent to rpc.rs), remove the in-file #[cfg(test)] mod tests { ... } block, and add a declaration #[cfg(test)] mod rpc_tests; in rpc.rs; inside the new rpc_tests.rs use super::* and keep all test functions (e.g. query_source_rpc_returns_hits_with_no_filters, query_global_rpc_returns_response_for_valid_window, search_entities_rpc_passes_through_kinds_none, drill_down_rpc_defaults_max_depth_to_one_when_unset, fetch_leaves_rpc_hydrates_valid_ids, etc.) unchanged except rename the module header (no nested mod) so imports like upsert_chunks and types (Chunk, Metadata, SourceRef) still resolve.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/openhuman/config/schema/storage_memory.rs`:
- Around line 118-140: Currently the MemoryTreeConfig default provides active
Ollama values (default_memory_tree_embedding_endpoint,
default_memory_tree_embedding_model, default_memory_tree_embedding_strict) which
causes build_embedder_from_config to pick the Ollama path by default; change the
defaults to be inert by returning None for embedding_endpoint and
embedding_model and false for embedding_strict (leave embedding_timeout_ms as-is
or None if you prefer), and update the Default impl for MemoryTreeConfig to use
these inert defaults so an operator must explicitly enable embeddings before the
Ollama/embedding path is chosen.
In `@src/openhuman/memory/tree/retrieval/drill_down.rs`:
- Around line 196-206: The code silently swallows DB errors when fetching chunk
embeddings and logs potentially sensitive child ids; instead of using
get_chunk_embedding(...).unwrap_or(None) inside the drill_down loop, propagate
the error (return Err(...)) or convert it into a descriptive error using the
function's Result flow rather than treating failures as “no embedding” (update
the call sites around get_chunk_embedding and the surrounding function to
handle/forward the Result); and redact the child id in the warning (in the
log::warn! call) by replacing sensitive parts with a safe token or
hashed/shortened form (referencing the id variable), while keeping the rest of
the behavior that pushes embeddings and calls hit_from_chunk(&chunk, "",
&chunk.metadata.source_id, 0.0).
In `@src/openhuman/memory/tree/retrieval/fetch.rs`:
- Around line 51-57: The code logs the raw chunk id in retrieval::fetch
(log::debug! line around get_chunk call), which violates the PII-redaction
comment; update the log to avoid emitting the full `id` — either call a new or
existing helper like `redact_chunk_id(&id)` and log that redacted value, or
replace the id with a constant placeholder (e.g. "<redacted>") in the log
message; keep the rest of the `get_chunk(&config_owned, id)?` logic and variable
names (`chunk`, `id`, `config_owned`) unchanged.
In `@src/openhuman/memory/tree/retrieval/integration_test.rs`:
- Around line 120-131: The test uses by_email.hits.first() which may be a
summary so fetch_leaves may never be exercised; replace that flaky selection
with a guaranteed leaf chunk id (e.g., capture the chunk id from the ingest
output or run a leaf-only query) and call fetch_leaves(&cfg,
&[known_leaf_id]).await.unwrap(), then assert that got.len() == 1 and that
got[0].id (or the appropriate field) equals known_leaf_id; update references to
first_hit/node_id to use the new known_leaf_id variable so the test reliably
verifies leaf hydration via fetch_leaves.
- Around line 101-107: The test currently only checks QueryResponse bookkeeping
(asserting by_source_kind.total >= by_source_kind.hits.len()) which allows zero
hits; change the assertion after calling query_source(cfg, None,
Some(SourceKind::Chat), None, None, 20) to require at least one chat hit (e.g.,
assert!(by_source_kind.hits.len() >= 1) or assert!(by_source_kind.total >= 1)),
so that query_source and the SourceKind::Chat path actually return ingested
chats; keep the existing bookkeeping assertion if you wish (by_source_kind.total
>= by_source_kind.hits.len()) but add the non-empty check on by_source_kind.hits
or total to enforce the integration behavior.
In `@src/openhuman/memory/tree/retrieval/rpc.rs`:
- Around line 48-50: The parsing of request fields (e.g., the source_kind match
that calls SourceKind::parse(s) and the similar parsing of "kinds" later) uses
the `?` operator so parse errors bypass the later map_err wrapper and return
unprefixed errors; replace those `?` usages by mapping their errors into the
RPC-prefixed format (for example, change `SourceKind::parse(s)?` to
`SourceKind::parse(s).map_err(|e| format!("<tool>: {e}"))?`) and do the same for
the kinds parsing code path so all validation errors are formatted with the same
"<tool>: {e}" prefix before returning.
In `@src/openhuman/memory/tree/retrieval/topic.rs`:
- Around line 262-297: The warn logs expose potentially sensitive node_id
values; update the two log::warn! calls inside the retrieval path that reference
node_id (the branch handling summaries using get_summary and the leaf branch
using get_chunk) to redact or pseudonymize node_id before logging—e.g., call an
existing anonymization helper or produce a masked string/hash of node_id (not
the raw value) and log that masked value along with the same contextual message
so you still warn about the missing row without emitting PII.
In `@src/openhuman/memory/tree/score/embed/ollama.rs`:
- Around line 67-79: The fallback reqwest::Client created in the new() path
lacks any timeout and embed() does not apply per-request timeouts; update
embed() (the embed method) to call .timeout(timeout) on the outgoing reqwest
request(s) that use the client (or otherwise apply the timeout variable per
request) so that requests use the intended timeout even when client was created
via the fallback reqwest::Client::new(); locate the usage of client inside
embed() and add the per-request .timeout(timeout) on the RequestBuilder before
.send().
---
Nitpick comments:
In `@src/openhuman/config/schema/load.rs`:
- Around line 855-886: Add focused unit tests in this module that exercise the
env-override logic for memory_tree: set OPENHUMAN_MEMORY_EMBED_ENDPOINT and
OPENHUMAN_MEMORY_EMBED_MODEL to empty string and non-empty values and assert
that memory_tree.embedding_endpoint and memory_tree.embedding_model become None
or Some(...) accordingly; set OPENHUMAN_MEMORY_EMBED_TIMEOUT_MS to "0", a valid
positive number, and a non-numeric value and assert embedding_timeout_ms is only
Some(n) when n>0; set OPENHUMAN_MEMORY_EMBED_STRICT to various truthy/falsey
inputs and assert parse_env_bool("OPENHUMAN_MEMORY_EMBED_STRICT", &flag) drives
memory_tree.embedding_strict properly; each test should manipulate the env vars,
invoke the code path that applies these env overrides (the block that assigns
self.memory_tree.embedding_*), and clean up env state between tests to avoid
leakage.
In `@src/openhuman/memory/tree/global_tree/seal.rs`:
- Around line 189-198: Add a regression assertion in the existing threshold test
that verifies the new embedding path added by seal_one_level actually persisted
an embedding: after running the code that calls seal_one_level (which uses
build_embedder_from_config and embed), assert weekly.embedding.is_some() and
optionally assert the embedding length matches the expected vector size (or
non-empty) so the test will fail if embedding persistence regresses; update the
existing threshold test that switches the config to the inert embedder to
include this check (and repeat for the analogous assertion referenced at the
other location).
In `@src/openhuman/memory/tree/retrieval/global.rs`:
- Around line 39-47: get_or_create_global_tree is a synchronous SQLite operation
being called directly from the async query_global path; to avoid blocking the
Tokio runtime, run the DB call inside tokio::task::spawn_blocking and await it
instead of calling get_or_create_global_tree(...) inline. Move the call into a
spawn_blocking closure (capturing config), unwrap or map the JoinHandle result
into the expected Result<Tree, _> and propagate errors the same way as before,
then pass the returned tree to recap_to_hits(tree.id, &tree.scope, ...) as
before; ensure types and error conversions match the existing query_global flow.
In `@src/openhuman/memory/tree/retrieval/rpc.rs`:
- Around line 268-560: The file `rpc.rs` contains a large #[cfg(test)] mod tests
{ ... } block that should be split into a test-only sibling module to keep the
production file small; move the entire tests block out into a new file named
rpc_tests.rs (adjacent to rpc.rs), remove the in-file #[cfg(test)] mod tests {
... } block, and add a declaration #[cfg(test)] mod rpc_tests; in rpc.rs; inside
the new rpc_tests.rs use super::* and keep all test functions (e.g.
query_source_rpc_returns_hits_with_no_filters,
query_global_rpc_returns_response_for_valid_window,
search_entities_rpc_passes_through_kinds_none,
drill_down_rpc_defaults_max_depth_to_one_when_unset,
fetch_leaves_rpc_hydrates_valid_ids, etc.) unchanged except rename the module
header (no nested mod) so imports like upsert_chunks and types (Chunk, Metadata,
SourceRef) still resolve.
In `@src/openhuman/memory/tree/retrieval/search.rs`:
- Around line 101-130: The SQL in search.rs (the sql String built around
mem_tree_entity_index using LOWER(entity_id) LIKE ?1 OR LOWER(surface) LIKE ?1
with pattern) forces full table scans and will not use the entity_id index as
the table grows; replace this with a search that uses an indexed path: either
switch to FTS (create an FTS virtual table on surface and entity_id and query
MATCH) or add normalized columns (e.g., surface_search, entity_id_search)
maintained when writing and create indexes on them, then change the WHERE in the
construction that builds sql/params (variables sql, params, pattern, and the
kinds branch) to use equality/prefix/trigram-capable operators that will hit
indexes (or use the FTS MATCH clause) and keep the existing entity_kind IN
handling; ensure LIMIT parameter remains and update any parameter placeholders
accordingly.
In `@src/openhuman/memory/tree/retrieval/topic.rs`:
- Around line 174-199: The loop currently spawns a separate
tokio::task::spawn_blocking per hit (inside the for over hits) causing O(N)
blocking-task overhead; instead collect the node_ids and node_kinds from hits
and perform a single spawn_blocking that calls src_store::get_summary_embedding
or get_chunk_embedding for each id (returning a Vec<Option<Vec<f32>>>), then
after .await iterate the returned embeddings to compute
cosine_similarity(&query_vec, &v) and build the decorated Vec<(f32, bool,
RetrievalHit)>; update references to NodeKind, get_chunk_embedding,
src_store::get_summary_embedding, query_vec, cosine_similarity, and RetrievalHit
so the batched result aligns with the original hit order.
In `@src/openhuman/memory/tree/score/embed/mod.rs`:
- Around line 23-145: This file is too heavy for a mod.rs — move the operational
implementations into sibling modules and keep mod.rs export-focused: create new
files (e.g., ops.rs or math.rs for cosine_similarity, codec.rs for
pack_embedding/unpack_embedding/pack_checked/decode_optional_blob, types.rs for
EMBEDDING_DIM and Embedder trait) and re-export their public items from this mod
via pub use (retain pub use factory::..., inert::..., ollama::...); ensure
function/trait names (Embedder, EMBEDDING_DIM, cosine_similarity,
pack_embedding, unpack_embedding, pack_checked, decode_optional_blob) are public
in their new modules so mod.rs only declares pub mod X and pub use X::{Embedder,
EMBEDDING_DIM, cosine_similarity, pack_embedding, unpack_embedding,
pack_checked, decode_optional_blob} to restore the original public API.
In `@src/openhuman/memory/tree/score/mod.rs`:
- Line 8: The score/mod.rs file has grown too large—split its operational code
into sibling modules and keep mod.rs as a thin composition file: create new
files (e.g., scorer.rs for core scoring logic, utils.rs for helpers, types.rs
for structs/enums) and move the corresponding functions, impls, and type
definitions out of mod.rs; then update mod.rs to declare and re-export those
modules (e.g., pub mod scorer; pub mod utils; pub mod types; pub use
scorer::Scorer; pub use types::ScoreType) and adjust any internal use paths in
moved code to reference the new module names (ensure references to existing pub
mod embed remain intact). Ensure no behavior changes and run tests to confirm
compilation.
In `@src/openhuman/memory/tree/source_tree/types.rs`:
- Around line 126-134: Add a unit test that verifies the serde(default) behavior
for the embedding field: deserialize two JSON payloads—one that omits the
embedding key and one that has "embedding": null—into the struct that declares
pub embedding: Option<Vec<f32>> (the struct in types.rs where embedding is
defined) using serde_json::from_str and assert the resulting struct.embedding is
None in both cases; ensure the test is #[test] and placed with the existing
serde/unit tests to lock in backward-compatible deserialization.
In `@src/openhuman/memory/tree/store.rs`:
- Around line 448-453: Add a unit migration test that creates a temporary DB in
the pre-column shape (i.e., a mem_tree_summaries table without the embedding
column), opens it via the existing with_connection path that triggers
add_column_if_missing, and then asserts the embedding column now exists and is
nullable (e.g., PRAGMA table_info or equivalent inspection). Locate the test
near other migration tests in the same module and call the same helpers used for
DB setup/teardown; reference the functions with_connection and
add_column_if_missing and the mem_tree_summaries table name so the test
exercises the exact migration branch that adds the "embedding" BLOB column.
Ensure the test fails if the column is missing or non-nullable and runs in
isolation (temporary file or in-memory DB).
In `@src/openhuman/memory/tree/topic_tree/routing.rs`:
- Around line 129-132: Several test modules duplicate the embedding-disable
setup (cfg.memory_tree.embedding_endpoint = None;
cfg.memory_tree.embedding_model = None; cfg.memory_tree.embedding_strict =
false); extract this into a shared test helper such as a function (e.g.,
disable_memory_tree_embedding(cfg: &mut Config) or
tests::helpers::disable_embedding_overrides) and replace the inline assignments
with a call to that helper from the tests that currently modify
cfg.memory_tree.embedding_endpoint, cfg.memory_tree.embedding_model, and
cfg.memory_tree.embedding_strict so all tests use the centralized helper.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 5999bc3a-20cf-40f9-8bd3-e6dce21ccfb9
📒 Files selected for processing (37)
src/core/all.rssrc/openhuman/config/schema/load.rssrc/openhuman/config/schema/mod.rssrc/openhuman/config/schema/storage_memory.rssrc/openhuman/config/schema/types.rssrc/openhuman/memory/mod.rssrc/openhuman/memory/tree/global_tree/digest.rssrc/openhuman/memory/tree/global_tree/recap.rssrc/openhuman/memory/tree/global_tree/seal.rssrc/openhuman/memory/tree/ingest.rssrc/openhuman/memory/tree/mod.rssrc/openhuman/memory/tree/retrieval/drill_down.rssrc/openhuman/memory/tree/retrieval/fetch.rssrc/openhuman/memory/tree/retrieval/global.rssrc/openhuman/memory/tree/retrieval/integration_test.rssrc/openhuman/memory/tree/retrieval/mod.rssrc/openhuman/memory/tree/retrieval/rpc.rssrc/openhuman/memory/tree/retrieval/schemas.rssrc/openhuman/memory/tree/retrieval/search.rssrc/openhuman/memory/tree/retrieval/source.rssrc/openhuman/memory/tree/retrieval/topic.rssrc/openhuman/memory/tree/retrieval/types.rssrc/openhuman/memory/tree/score/embed/factory.rssrc/openhuman/memory/tree/score/embed/inert.rssrc/openhuman/memory/tree/score/embed/mod.rssrc/openhuman/memory/tree/score/embed/ollama.rssrc/openhuman/memory/tree/score/mod.rssrc/openhuman/memory/tree/source_tree/bucket_seal.rssrc/openhuman/memory/tree/source_tree/flush.rssrc/openhuman/memory/tree/source_tree/mod.rssrc/openhuman/memory/tree/source_tree/store.rssrc/openhuman/memory/tree/source_tree/types.rssrc/openhuman/memory/tree/store.rssrc/openhuman/memory/tree/topic_tree/backfill.rssrc/openhuman/memory/tree/topic_tree/routing.rssrc/openhuman/threads/ops.rstests/json_rpc_e2e.rs
| let by_source_kind = query_source(&cfg, None, Some(SourceKind::Chat), None, None, 20) | ||
| .await | ||
| .unwrap(); | ||
| // Each channel may or may not have sealed; what we lock in here is | ||
| // that the source-kind query returns a well-formed, possibly empty | ||
| // response. The stronger per-channel assertion lives in source.rs. | ||
| assert!(by_source_kind.total >= by_source_kind.hits.len()); |
There was a problem hiding this comment.
Assert that query_source returns the ingested chats.
After ingesting three chat batches, this only rechecks QueryResponse bookkeeping and still passes if query_source regresses to returning zero hits. The integration test should require at least one SourceKind::Chat hit here.
Suggested tightening
- assert!(by_source_kind.total >= by_source_kind.hits.len());
+ assert!(
+ by_source_kind.total > 0 && !by_source_kind.hits.is_empty(),
+ "query_source should surface the ingested chat batches"
+ );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/openhuman/memory/tree/retrieval/integration_test.rs` around lines 101 -
107, The test currently only checks QueryResponse bookkeeping (asserting
by_source_kind.total >= by_source_kind.hits.len()) which allows zero hits;
change the assertion after calling query_source(cfg, None,
Some(SourceKind::Chat), None, None, 20) to require at least one chat hit (e.g.,
assert!(by_source_kind.hits.len() >= 1) or assert!(by_source_kind.total >= 1)),
so that query_source and the SourceKind::Chat path actually return ingested
chats; keep the existing bookkeeping assertion if you wish (by_source_kind.total
>= by_source_kind.hits.len()) but add the non-empty check on by_source_kind.hits
or total to enforce the integration behavior.
| let source_kind = match req.source_kind.as_deref() { | ||
| Some(s) => Some(SourceKind::parse(s)?), | ||
| None => None, |
There was a problem hiding this comment.
Prefix validation errors the same way as handler execution errors.
These early ? returns bypass the later map_err(|e| format!("<tool>: {e}")), so invalid source_kind / kinds come back unprefixed while runtime failures are prefixed. That gives the same RPC two different error formats.
Suggested fix
- Some(s) => Some(SourceKind::parse(s)?),
+ Some(s) => Some(SourceKind::parse(s).map_err(|e| format!("query_source: {e}"))?),- let parsed: Result<Vec<EntityKind>, String> =
- list.iter().map(|s| EntityKind::parse(s)).collect();
+ let parsed: Result<Vec<EntityKind>, String> = list
+ .iter()
+ .map(|s| EntityKind::parse(s).map_err(|e| format!("search_entities: {e}")))
+ .collect();Also applies to: 169-175
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/openhuman/memory/tree/retrieval/rpc.rs` around lines 48 - 50, The parsing
of request fields (e.g., the source_kind match that calls SourceKind::parse(s)
and the similar parsing of "kinds" later) uses the `?` operator so parse errors
bypass the later map_err wrapper and return unprefixed errors; replace those `?`
usages by mapping their errors into the RPC-prefixed format (for example, change
`SourceKind::parse(s)?` to `SourceKind::parse(s).map_err(|e| format!("<tool>:
{e}"))?`) and do the same for the kinds parsing code path so all validation
errors are formatted with the same "<tool>: {e}" prefix before returning.
…ansai#839) - Resolve merge conflict in memory/mod.rs (combine NamespaceSummary/RecallOpts from main with all_retrieval_* from PR) - Fix breaking default config: MemoryTreeConfig now defaults to None endpoint/model and embedding_strict=false so existing installs without Ollama configured don't fail at runtime - Fix PII leaks: redact chunk id from fetch.rs log, redact node_id from drill_down.rs warn log, redact summary/chunk node_id from topic.rs warn logs - Fix silent error swallowing in drill_down.rs: propagate DB errors from get_chunk_embedding instead of treating them as "no embedding" - Prefix validation errors consistently in rpc.rs: SourceKind and EntityKind parse errors now carry the handler name prefix matching runtime error format - Add per-request .timeout() call to OllamaEmbedder.embed() so fallback client respects the configured timeout - Tighten fetch_leaves integration test: use a guaranteed NodeKind::Leaf hit instead of first() which may be a summary
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (1)
src/openhuman/memory/tree/retrieval/integration_test.rs (1)
133-140:⚠️ Potential issue | 🟡 MinorAssert that
fetch_leavesreturns the requested node.
got.len() == 1still passes if the single returned hit is the wrong leaf. Please also assertgot[0].node_id == leaf_hit.node_idso this test locks in the ID-to-hit mapping.Suggested tightening
assert_eq!( got.len(), 1, "fetch_leaves must hydrate the known leaf chunk id" ); + assert_eq!(got[0].node_id, leaf_hit.node_id);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/openhuman/memory/tree/retrieval/integration_test.rs` around lines 133 - 140, Test currently only asserts got.len() == 1 which can pass even if the returned hit is for a different node; update the assertion after calling fetch_leaves(&cfg, &[leaf_hit.node_id.clone()]) to also verify that the returned hit matches the requested node by asserting got[0].node_id == leaf_hit.node_id (use the existing got and leaf_hit.node_id symbols to locate where to add this check).
🧹 Nitpick comments (2)
src/openhuman/memory/tree/retrieval/topic.rs (2)
1-583: Split this module before it grows further.This file is already over the repo's ~500-line target and currently bundles query orchestration, reranking, hydration helpers, and a large test module. Pulling the tests and/or helper logic into sibling modules would make future retrieval changes easier to review and maintain.
As per coding guidelines:
Source files should be ≤ ~500 lines; split modules when growing to improve maintainability.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/openhuman/memory/tree/retrieval/topic.rs` around lines 1 - 583, The module is too large; split it into smaller sibling modules so query orchestration, reranking, hydration, and tests are separated. Extract rerank_by_semantic_similarity and its embedder imports into a new module (e.g., retrieval::topic::rerank) and re-export the function; move fetch_topic_tree_root_summary, entity_hit_to_retrieval_hit, and filter_by_window into a hydration/helper module (e.g., retrieval::topic::hydrate) and keep query_topic in the top-level retrieval::topic module which should call the extracted functions; finally move the large #[cfg(test)] test module out into a dedicated test file (e.g., tests/topic_tests.rs or retrieval/topic/tests.rs) so tests no longer inflate the production source file. Update use/import paths and module declarations to match the new layout and ensure public/private visibility for functions used across the new modules.
55-65: Add a redacted per-entity correlation field to these logs.Logging only
entity_kindmakes it hard to tie the entry/exit lines back to onequery_topiccall when multiple requests of the same kind are in flight. A short hash or other redacted token derived fromentity_idwould keep PII out of the logs while preserving correlation.As per coding guidelines:
Use \log` / `tracing` at `debug` / `trace` levels. Default to verbose diagnostics on new/changed flows with stable grep-friendly prefixes (`[domain]`, `[rpc]`, `[ui-flow]`) and correlation fields (request IDs, method names, entity IDs). Never log secrets or full PII — redact.`Also applies to: 144-148
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/openhuman/memory/tree/retrieval/topic.rs` around lines 55 - 65, Add a short redacted correlation token derived from entity_id (e.g., a stable short hash or HMAC) and include it in the debug logs for query_topic so concurrent calls can be correlated without exposing PII: compute the token from entity_id inside the query_topic function (use a non-reversible hash/truncate to ~8 chars), add it as a field like correlation=<token> in the existing log::debug! call shown (and the similar log at lines ~144-148), and reuse the same token for all logs within that query_topic invocation; keep using the existing "[retrieval::topic]" prefix and debug level.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/openhuman/config/schema/storage_memory.rs`:
- Around line 85-87: The doc comment for `embedding_strict` is incorrect: it
says the default is `true` but the code sets the default to `false`; update the
comment block above the config to state that `embedding_strict` defaults to
`false` and that when `false` the system falls back to the inert zero-vector
embedder with a warning, matching the actual default value in the
`embedding_strict` config field.
In `@src/openhuman/memory/tree/retrieval/integration_test.rs`:
- Around line 256-276: The test query_source_with_query_returns_same_count must
seed a guaranteed sealed source before comparing recency vs semantic counts:
reuse the seal-driving setup from seal_populates_summary_embedding (or call the
same helper that forces sealing/summarization after ingest) after ingest_chat so
at least one sealed summary exists, then run query_source for both recency and
semantic paths and assert equality; update the test to call the sealing helper
(or ingest enough messages/trigger seal logic) prior to invoking query_source to
ensure the semantic branch has real candidates to rerank.
---
Duplicate comments:
In `@src/openhuman/memory/tree/retrieval/integration_test.rs`:
- Around line 133-140: Test currently only asserts got.len() == 1 which can pass
even if the returned hit is for a different node; update the assertion after
calling fetch_leaves(&cfg, &[leaf_hit.node_id.clone()]) to also verify that the
returned hit matches the requested node by asserting got[0].node_id ==
leaf_hit.node_id (use the existing got and leaf_hit.node_id symbols to locate
where to add this check).
---
Nitpick comments:
In `@src/openhuman/memory/tree/retrieval/topic.rs`:
- Around line 1-583: The module is too large; split it into smaller sibling
modules so query orchestration, reranking, hydration, and tests are separated.
Extract rerank_by_semantic_similarity and its embedder imports into a new module
(e.g., retrieval::topic::rerank) and re-export the function; move
fetch_topic_tree_root_summary, entity_hit_to_retrieval_hit, and filter_by_window
into a hydration/helper module (e.g., retrieval::topic::hydrate) and keep
query_topic in the top-level retrieval::topic module which should call the
extracted functions; finally move the large #[cfg(test)] test module out into a
dedicated test file (e.g., tests/topic_tests.rs or retrieval/topic/tests.rs) so
tests no longer inflate the production source file. Update use/import paths and
module declarations to match the new layout and ensure public/private visibility
for functions used across the new modules.
- Around line 55-65: Add a short redacted correlation token derived from
entity_id (e.g., a stable short hash or HMAC) and include it in the debug logs
for query_topic so concurrent calls can be correlated without exposing PII:
compute the token from entity_id inside the query_topic function (use a
non-reversible hash/truncate to ~8 chars), add it as a field like
correlation=<token> in the existing log::debug! call shown (and the similar log
at lines ~144-148), and reuse the same token for all logs within that
query_topic invocation; keep using the existing "[retrieval::topic]" prefix and
debug level.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 4ee21c7f-9476-4cd0-95a4-db360df57389
📒 Files selected for processing (9)
src/core/all.rssrc/openhuman/config/schema/storage_memory.rssrc/openhuman/memory/mod.rssrc/openhuman/memory/tree/retrieval/drill_down.rssrc/openhuman/memory/tree/retrieval/fetch.rssrc/openhuman/memory/tree/retrieval/integration_test.rssrc/openhuman/memory/tree/retrieval/rpc.rssrc/openhuman/memory/tree/retrieval/topic.rssrc/openhuman/memory/tree/score/embed/ollama.rs
✅ Files skipped from review due to trivial changes (3)
- src/openhuman/memory/mod.rs
- src/openhuman/memory/tree/retrieval/drill_down.rs
- src/openhuman/memory/tree/retrieval/rpc.rs
🚧 Files skipped from review as they are similar to previous changes (2)
- src/core/all.rs
- src/openhuman/memory/tree/score/embed/ollama.rs
| /// `embedding_strict`: | ||
| /// - `true` (default): ingest/seal bail with a clear config error. | ||
| /// - `false`: fall back to the inert zero-vector embedder and warn. |
There was a problem hiding this comment.
Fix the embedding_strict default stated in docs.
Line 86 says true is the default, but Line 138-Line 140 sets embedding_strict default to false. Please align the comment with the actual default to avoid config confusion.
Suggested doc fix
/// `embedding_strict`:
-/// - `true` (default): ingest/seal bail with a clear config error.
-/// - `false`: fall back to the inert zero-vector embedder and warn.
+/// - `true`: ingest/seal bail with a clear config error.
+/// - `false` (default): fall back to the inert zero-vector embedder and warn.📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| /// `embedding_strict`: | |
| /// - `true` (default): ingest/seal bail with a clear config error. | |
| /// - `false`: fall back to the inert zero-vector embedder and warn. | |
| /// `embedding_strict`: | |
| /// - `true`: ingest/seal bail with a clear config error. | |
| /// - `false` (default): fall back to the inert zero-vector embedder and warn. |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/openhuman/config/schema/storage_memory.rs` around lines 85 - 87, The doc
comment for `embedding_strict` is incorrect: it says the default is `true` but
the code sets the default to `false`; update the comment block above the config
to state that `embedding_strict` defaults to `false` and that when `false` the
system falls back to the inert zero-vector embedder with a warning, matching the
actual default value in the `embedding_strict` config field.
| async fn query_source_with_query_returns_same_count() { | ||
| let (_tmp, cfg) = test_config(); | ||
| ingest_chat(&cfg, "slack:#eng", "alice", vec![], chat_about_phoenix(0)) | ||
| .await | ||
| .unwrap(); | ||
|
|
||
| let recency = query_source(&cfg, None, Some(SourceKind::Chat), None, None, 20) | ||
| .await | ||
| .unwrap(); | ||
| let semantic = query_source( | ||
| &cfg, | ||
| None, | ||
| Some(SourceKind::Chat), | ||
| None, | ||
| Some("phoenix migration"), | ||
| 20, | ||
| ) | ||
| .await | ||
| .unwrap(); | ||
| assert_eq!(recency.total, semantic.total); | ||
| assert_eq!(recency.hits.len(), semantic.hits.len()); |
There was a problem hiding this comment.
Seed a guaranteed sealed source before comparing these two paths.
With one short batch, query_source(..., Some(SourceKind::Chat), ...) can legitimately return zero summaries, so both assertions pass even if the semantic branch never reranks real candidates. Reuse the seal-driving setup from seal_populates_summary_embedding or otherwise force at least one sealed source hit before comparing counts.
Based on learnings: Ship unit tests for new/changed behavior before stacking features. Untested code is incomplete.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/openhuman/memory/tree/retrieval/integration_test.rs` around lines 256 -
276, The test query_source_with_query_returns_same_count must seed a guaranteed
sealed source before comparing recency vs semantic counts: reuse the
seal-driving setup from seal_populates_summary_embedding (or call the same
helper that forces sealing/summarization after ingest) after ingest_chat so at
least one sealed summary exists, then run query_source for both recency and
semantic paths and assert equality; update the test to call the sealing helper
(or ingest enough messages/trigger seal logic) prior to invoking query_source to
ensure the semantic branch has real candidates to rerank.
…i#710) (tinyhumansai#839) - Add JSON-RPC handlers for source, global, and topic-based memory retrieval. - Integrate Ollama embeddings for semantic reranking of chunks and summaries. - Implement 15 unit tests for RPC handlers covering parameter parsing and PII redaction. - Redact PII from logs by removing raw source, entity, and node identifiers. - Fix BFS traversal for drill-down and deduplicate results in topic queries. - Add configuration for embedding endpoints, models, and strictness modes. Co-authored-by: sanil-23 <sanil@vezures.xyz> Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai>
…i#710) (tinyhumansai#839) - Add JSON-RPC handlers for source, global, and topic-based memory retrieval. - Integrate Ollama embeddings for semantic reranking of chunks and summaries. - Implement 15 unit tests for RPC handlers covering parameter parsing and PII redaction. - Redact PII from logs by removing raw source, entity, and node identifiers. - Fix BFS traversal for drill-down and deduplicate results in topic queries. - Add configuration for embedding endpoints, models, and strictness modes. Co-authored-by: sanil-23 <sanil@vezures.xyz> Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai>
…i#710) (tinyhumansai#839) - Add JSON-RPC handlers for source, global, and topic-based memory retrieval. - Integrate Ollama embeddings for semantic reranking of chunks and summaries. - Implement 15 unit tests for RPC handlers covering parameter parsing and PII redaction. - Redact PII from logs by removing raw source, entity, and node identifiers. - Fix BFS traversal for drill-down and deduplicate results in topic queries. - Add configuration for embedding endpoints, models, and strictness modes. Co-authored-by: sanil-23 <sanil@vezures.xyz> Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai>
…ation) Resolves conflicts after main absorbed: - tinyhumansai#835 inline-test extraction to sibling _tests.rs files - tinyhumansai#803 memory namespaces (Memory trait gained `namespace` arg + `namespace_summaries`) - tinyhumansai#839 memory_tree retrieval RPC handlers - install.sh resolver / smoke test changes Conflict resolutions: - src/openhuman/agent/prompts/mod.rs + mod_tests.rs: kept branch's snapshot_pair production code (UserFilesSection now prefers session-frozen MemorySnapshot over workspace files), then applied tinyhumansai#835's extraction by moving the inline mod tests block to mod_tests.rs sibling. Tests retain the `curated_snapshot: None` additions from 1976ba4. - src/openhuman/agent/harness/subagent_runner/ops.rs + ops_tests.rs: same pattern — kept branch's session-snapshot wiring, extracted inline tests to ops_tests.rs sibling. - src/core/all.rs: kept both webview_apis (main) and life_capture + curated_memory (branch) controller registrations. - scripts/install.sh: kept branch's clearer "Resolved URL was empty…" diag and the test_install.sh resolver-unit-test job in installer-smoke.yml. Compatibility patches: - ops_tests.rs NoopMemory updated to new Memory trait (namespace arg on store/get/list/forget, RecallOpts on recall, added namespace_summaries). - help/prompt.rs test fixture gets curated_snapshot: None. cargo build --tests passes locally on darwin-aarch64.
Summary
Adds 15 unit tests for the six Phase 4 JSON-RPC handlers in
src/openhuman/memory/tree/retrieval/rpc.rs—query_source,query_global,query_topic,search_entities,drill_down,fetch_leaves.Closes the RPC-coverage gap that was deferred during CodeRabbit review on #831 (the Phase 4 retrieval PR). Until this PR, these handlers had 0 unit tests; the underlying domain functions in
source.rs/topic.rs/drill_down.rs/etc. already had ~55 tests, but the thin handler layer itself — where parameter parsing, default fallbacks, enum validation,RpcOutcomeenvelope shape, and PII-redacted log formatting live — was uncovered.Depends on #831. This branch is cut from
feat/710-retrieval. Once #831 merges to main, this PR will rebase cleanly.Test scope (per handler)
query_source_rpcSourceKind::parsesuccess + error,limit=Some,source_kind=Somequery_global_rpcquery_topic_rpcsplit_once(':')Some arm (colon present), None arm (prefix fallback to\"unknown\")search_entities_rpckinds=Nonebranch,kinds=Some(valid)branch,EntityKind::parsepartial-list errordrill_down_rpcmax_depthdefault to 1,split_once(':')Some/None arms, PII redactionfetch_leaves_rpcCoverage estimate
Line coverage on
retrieval/rpc.rs: ~88% (was 0%).Branch coverage: ~82%.
Uncovered remainder is
map_errarms when the underlying domain function returnsErr— these require corrupting the SQLite DB mid-call or mocking the domain layer. Deemed not worth the harness weight; the happy-path pattern is symmetric across all six handlers.PII redaction assertions
Every log-asserting test includes negative assertions that PII-prone fields are NOT in the log output:
query_source: rawsource_id(e.g.slack:#eng) absent from logquery_topic: rawentity_id(e.g. email address) absent from logsearch_entities: rawquerystring absent from logdrill_down: rawnode_idscope segments absent beyond the kind prefixThis locks in the redaction behaviour that was added during CodeRabbit review on #831, so a regression would fail a test rather than leak silently.
Test plan
cargo test --lib openhuman::memory::tree::retrieval::rpc::— 15/15 passcargo fmt -- --check— cleancargo clippyonretrieval/rpc.rs— no new warnings from this PR (pre-existing warnings in unrelated modules unchanged)🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
query_source,query_topic,query_global,search_entities,drill_down, andfetch_leavesfor flexible memory querying.Configuration