Skip to content

feat(embedding): dual-model registry (MiniLM + paraphrase) per ADR-043#407

Merged
ohdearquant merged 15 commits into
mainfrom
slice/v022/07-dual-embedding
May 25, 2026
Merged

feat(embedding): dual-model registry (MiniLM + paraphrase) per ADR-043#407
ohdearquant merged 15 commits into
mainfrom
slice/v022/07-dual-embedding

Conversation

@ohdearquant
Copy link
Copy Markdown
Owner

@ohdearquant ohdearquant commented May 25, 2026

Summary

Slice 7/9 from PR #399 (v022-polish show). Stacked on slice 5 (recall-knobs).

Multi-model embedding support across the runtime + storage + memory stack. Workspace dual-embedding now reachable end-to-end.

What's changed

khive-runtime

  • RuntimeConfig.additional_embedding_models: Vec<EmbeddingModel>
  • Replaces single OnceCell<embedder> with HashMap<model_name, embedder>
  • default_embedder_name() + embedder(name) public methods
  • KHIVE_ADDITIONAL_EMBEDDING_MODELS env-var parsing
  • configured_embedding_models() helper enumerates active set

khive-db

  • V16 migration: add embedding_model TEXT NOT NULL DEFAULT '<default>' column to vectors table with backfill + composite index
  • VectorStore.insert / search scoped by embedding_model

khive-storage

  • VectorRecord carries model tag
  • Vector search params include model scope

khive-pack-memory

  • recall + remember accept optional embedding_model arg
  • Validation: must be a registered model name

kkernel

Notes

  • 17 files changed, +634/-159 lines
  • Tests rebaselined for V16 (failed_migration_rolls_back tests V17 now; store_ddl_then_event_migration_is_idempotent expects V16 head)
  • Lattice gap status: N/A — lattice-embed 0.2.4 already exposes both MiniLM + paraphrase as 384-d local models with EmbeddingRoutingConfig primitives

Dependencies

Depends on #406 (slice 5: recall-knobs). Base branch is slice/v022/05-recall-knobs — will auto-rebase when slice 5 merges.

Test plan

  • cargo build --workspace passes
  • cargo test --workspace passes
  • cargo clippy --workspace --all-targets -- -D warnings passes
  • cargo fmt --all -- --check passes
  • CI green

ohdearquant and others added 4 commits May 25, 2026 11:21
Route pack-memory's fuse_candidates through khive_retrieval::fuse_search_results,
making khive-retrieval a real consumed facade instead of an orphan crate.

- Add khive-retrieval dep to khive-pack-memory/Cargo.toml
- Replace direct fuse_with_strategy call with retrieval adapter
  (CandidateMeta side-map, HybridConfig builder, FusionStrategy conversion)
- Fix issue #309: resolve --all-features compile failures in khive-retrieval
  (stale SqliteStore imports, missing NodeId/LinkStore imports)
- Add 5 integration tests (3 fusion_surface, 2 pack-memory recall adapter)
- RRF k=1 discriminator test proves strategy propagation (30x score gap)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…recall (ADR-033 §6)

- Add three optional per-request fields to RecallParams: top_k (usize),
  fusion_strategy (string), and score_floor (f32)
- fusion_strategy validated against {"rrf","weighted","union"}; clear error
  with valid values on invalid input
- top_k overrides the result limit for a single call (capped at 100)
- score_floor applied as a post-filter on the composite score after compute_score
- Add parse_fusion_strategy_str helper; wire override into cfg.fuse_strategy
  before passing to fuse_candidates
- Add 4 integration tests: default_identity, top_k_override,
  fusion_strategy_override (including rejection), score_floor
- Document knobs in ADR-033 §6.1 with table, semantics, and example DSL

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Multi-model embedding support landed across the runtime + storage + memory
stack. Workspace dual-embedding now reachable end-to-end:

khive-runtime:
  - RuntimeConfig.additional_embedding_models: Vec<EmbeddingModel>
  - Replaces single OnceCell<embedder> with HashMap<model_name, embedder>
  - default_embedder_name() + embedder(name) public methods
  - KHIVE_ADDITIONAL_EMBEDDING_MODELS env-var parsing
  - configured_embedding_models() helper enumerates active set

khive-db:
  - V16 migration: add `embedding_model TEXT NOT NULL DEFAULT '<default>'`
    column to vectors table with backfill + composite index
  - VectorStore.insert / search scoped by embedding_model

khive-storage:
  - VectorRecord carries model tag
  - vector search params include model scope

khive-pack-memory:
  - recall + remember accept optional embedding_model arg
  - validation: must be a registered model name

kkernel:
  - engine list now returns real loaded models (no longer empty Vec)
  - engine migrate / drift-check still return not-implemented (#380/#385)

Notes:
- 16 files changed, +582/-138 lines
- Tests rebaselined for V16 (failed_migration_rolls_back tests V17 now;
  store_ddl_then_event_migration_is_idempotent expects V16 head)
- Workspace: cargo build + cargo test + clippy clean + fmt clean

Lattice gap status: N/A — lattice-embed 0.2.4 already exposes both
MiniLM + paraphrase as 384-d local models with EmbeddingRoutingConfig
primitives. khive-runtime now uses these directly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Addresses codex finding on PR #405: `khive_retrieval::hnsw::HnswIndex`
doesn't resolve because `hnsw` is not a public submodule — `HnswIndex` is
re-exported at the crate root (lib.rs:145). The doctest at persist/mod.rs:29
must use the public facade import.

Closes the remaining gap on issue #309 (--all-features doctest failure).

Verified: `RUSTC_WRAPPER= cargo test --offline -p khive-retrieval --all-features --doc` passes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@ohdearquant
Copy link
Copy Markdown
Owner Author

Verdict: REJECT
Findings: 1 Critical, 3 High, 1 Medium

Findings

[Critical] V16 Ships a Schema Shape That ADR-043 Still Rejects

Evidence: docs/adr/ADR-043-embedding-model-migration.md:110 says each vec_<engine> table gains embedding_model_id; docs/adr/ADR-043-embedding-model-migration.md:113 defines it as a BLOB FK to _embedding_models; docs/adr/ADR-043-embedding-model-migration.md:118 requires backfilling existing rows with the active model id. The PR instead adds embedding_model TEXT NOT NULL DEFAULT 'all-minilm-l6-v2' and indexes (subject_id, embedding_model) at crates/khive-db/src/migrations.rs:936 and crates/khive-db/src/migrations.rs:937. The migration ledger also still stops at V15 in docs/adr/ADR-015-schema-migrations.md:46, while the process requires new schema migrations to claim the next ledger version in the ADR at docs/adr/ADR-015-schema-migrations.md:63.

Why this matters: This is an accepted ADR/code mismatch on a production schema migration. Either the string-tag design is the new decision, or the FK registry design is. Shipping V16 while the accepted ADR says the opposite leaves future migrations, kkernel engine state, and vector cleanup without a stable contract.

Suggested fix: Amend ADR-043 and ADR-015 in this PR to explicitly adopt the string-tag V16 design, or change V16 to implement the accepted embedding_model_id FK/backfill contract.

[High] V16 Does Not Correctly Backfill Existing Non-Default or sqlite-vec Rows

Evidence: The only regular-table backfill value is the hard-coded default at crates/khive-db/src/migrations.rs:936, so every existing regular vec_* row is tagged as all-minilm-l6-v2 regardless of the table/model it came from. Runtime configuration can select a different default model through KHIVE_EMBEDDING_MODEL at crates/khive-runtime/src/runtime.rs:182, and vector stores are opened per resolved model key/name at crates/khive-runtime/src/runtime.rs:374. For existing sqlite-vec tables, the open path does not backfill at all: missing embedding_model causes the whole table to be dropped at crates/khive-db/src/backend.rs:323 and crates/khive-db/src/backend.rs:324.

Why this matters: A migrated deployment with BGE/paraphrase vectors will either tag them as MiniLM or delete them on open. Model-scoped recall then filters on embedding_model = ? and can silently lose the vector side of recall.

Suggested fix: Backfill from the active registry row or from the model key represented by each vector table. For sqlite-vec, implement an explicit rebuild/copy/reindex plan with auditability instead of silently dropping old tables.

[High] Non-Default Memory Vectors Are Not Deleted With Their Notes

Evidence: remember.embedding_model reaches create_note_with_decay_for_embedding_model at crates/khive-pack-memory/src/handlers.rs:429, and the runtime inserts that note vector into vectors_for_model(token, model_name) at crates/khive-runtime/src/operations.rs:943 and crates/khive-runtime/src/operations.rs:945. Note deletion only deletes from self.vectors(token) when the default embedding model is configured at crates/khive-runtime/src/operations.rs:1392 and crates/khive-runtime/src/operations.rs:1403.

Why this matters: Memories embedded with embedding_model="paraphrase" leave orphaned paraphrase vectors after note deletion. Those stale rows can continue participating in vector candidate sets until later note filtering removes them, wasting recall budget and violating the model-scoped cleanup semantics this PR adds.

Suggested fix: Persist the vector model used for a note or delete across all registered model stores for that subject id. Add a regression that remembers with a non-default model, deletes the note, and proves the non-default vector row is gone.

[High] Unknown remember.embedding_model Errors After Persisting the Note

Evidence: The memory handler passes the raw optional model string through at crates/khive-pack-memory/src/handlers.rs:440. create_note_inner writes the note at crates/khive-runtime/src/operations.rs:912 and the FTS row at crates/khive-runtime/src/operations.rs:919 before model resolution happens in embed_with_model at crates/khive-runtime/src/operations.rs:943. Unknown names are rejected by resolve_embedding_model at crates/khive-runtime/src/runtime.rs:441 and crates/khive-runtime/src/runtime.rs:452.

Why this matters: A typo in remember(embedding_model=...) returns an error but still leaves a memory note and text index row behind. That is a write-after-failure bug, and it makes the requested unknown-model validation non-atomic.

Suggested fix: Resolve and validate embedding_model before any note/FTS/vector write, ideally at the start of create_note_inner or in handle_remember. Add a regression that an unknown model leaves no note, FTS row, or vector row.

[Medium] KHIVE_ADDITIONAL_EMBEDDING_MODELS Silently Drops Unknown Names

Evidence: RuntimeConfig::default reads KHIVE_ADDITIONAL_EMBEDDING_MODELS at crates/khive-runtime/src/runtime.rs:186 and delegates to parse_embedding_model_list; that parser uses filter_map at crates/khive-runtime/src/runtime.rs:542 and crates/khive-runtime/src/runtime.rs:544, so invalid names disappear without an error or warning.

Why this matters: The format is otherwise robust for comma/whitespace-separated values, but a config typo produces a runtime with fewer models than the operator requested. The later UnknownModel error is clear, but it occurs at request time rather than configuration time.

Suggested fix: Parse this env var as Result<Vec<EmbeddingModel>, RuntimeError> and fail startup or emit a prominent warning listing invalid names. Keep the current whitespace/empty-entry behavior.

Looks Right

  • Local checkout matches the requested feature commit: 7402dda feat(embedding): dual-model registry (MiniLM + paraphrase) per ADR-043.
  • V16 is append-only in the Rust migration array at crates/khive-db/src/migrations.rs:497, and migration tests are rebaselined to V16: rollback now uses bad V17 at crates/khive-db/src/migrations.rs:1277, and the event-DDL idempotency test expects V16 at crates/khive-db/src/migrations.rs:1586.
  • The regular-table composite index exists as idx_{t}_subject_model ON {t}(subject_id, embedding_model) at crates/khive-db/src/migrations.rs:937.
  • Vector search is model-scoped: the SQL subquery includes embedding_model = ?4 at crates/khive-db/src/stores/vectors.rs:419, and exact candidate scoring also includes e.embedding_model = ?3 at crates/khive-db/src/stores/vectors.rs:586.
  • Backward-compatible default embedding calls still use the configured default and return Unconfigured("embedding_model") when no default exists at crates/khive-runtime/src/retrieval.rs:48 and crates/khive-runtime/src/retrieval.rs:75.
  • embedder(name) returns RuntimeError::UnknownModel for unregistered names through resolve_embedding_model / map lookup at crates/khive-runtime/src/runtime.rs:464 and crates/khive-runtime/src/runtime.rs:470.
  • recall.embedding_model reaches model-specific embedding and vector search through collect_recall_candidates at crates/khive-pack-memory/src/handlers.rs:307 and crates/khive-pack-memory/src/handlers.rs:314.
  • kkernel engine list no longer returns only a stub/default; it queries all _embedding_models rows via khive_db::query_embedding_models at crates/kkernel/src/engine.rs:205 and crates/kkernel/src/engine.rs:209.
  • Concurrent embedder loading is threadsafe: each immutable registry entry owns a tokio::sync::OnceCell at crates/khive-runtime/src/runtime.rs:210, and model construction goes through get_or_init at crates/khive-runtime/src/runtime.rs:472.
  • The per-call HashMap lookup is not a material performance concern compared with embedding inference; service construction and result caching remain behind per-model OnceCell/CachedEmbeddingService.

Commands Run

  • git diff --stat origin/slice/v022/05-recall-knobs..HEAD: confirmed 17 changed files, +634/-159.
  • git log --oneline --decorate -5: confirmed HEAD is 7402dda on slice/v022/07-dual-embedding.
  • git diff --check origin/slice/v022/05-recall-knobs..HEAD: passed.
  • cargo fmt --all --manifest-path crates/Cargo.toml -- --check: passed.
  • RUSTC_WRAPPER= CARGO_BUILD_RUSTC_WRAPPER= cargo test --offline --manifest-path crates/Cargo.toml -p khive-db --all-features migrations::tests:: -- --nocapture: passed, 10 migration tests.
  • RUSTC_WRAPPER= CARGO_BUILD_RUSTC_WRAPPER= cargo test --offline --manifest-path crates/Cargo.toml -p khive-db --all-features vector_filter_contract -- --nocapture: passed, 3 contract tests.
  • RUSTC_WRAPPER= CARGO_BUILD_RUSTC_WRAPPER= cargo test --offline --manifest-path crates/Cargo.toml -p khive-runtime --all-features -- --nocapture: passed, 262 unit + 18 integration tests; 2 ignored model-loading tests.
  • RUSTC_WRAPPER= CARGO_BUILD_RUSTC_WRAPPER= cargo test --offline --manifest-path crates/Cargo.toml -p khive-pack-memory --all-features -- --nocapture: passed, 61 unit + 27 integration tests.
  • RUSTC_WRAPPER= CARGO_BUILD_RUSTC_WRAPPER= cargo test --offline --manifest-path crates/Cargo.toml -p kkernel --all-features engine::tests -- --nocapture: passed, 6 engine tests.
  • gh pr view 407 --json ... --repo ohdearquant/khive: failed because this sandbox cannot connect to api.github.com.

What I Did Not Check

  • I did not run full cargo test --workspace or clippy; I ran targeted crate gates for the changed migration/runtime/memory/kkernel surfaces.
  • I could not fetch live PR metadata from GitHub because network access is unavailable in this environment.

Re-Review Guidance

Re-review broadly after fixes. The migration/ADR decision must be settled first; then rerun model-scoped remember/recall/delete tests against both default and non-default embedding models.

Domain utility: SKIPPED — no mcp__lore tools are available in this environment, so I used the repository ADRs, local diff, and targeted Rust gates.

ohdearquant and others added 2 commits May 25, 2026 11:48
…ger tests

Two Medium findings from codex review:

1. top_k cast bug: `(k as u32).min(100)` truncates k before capping. A
   request with `top_k = 4_294_967_297` (larger than u32::MAX) truncates
   to 1 BEFORE the cap is applied, so the result limit becomes 1, not 100.
   Fixed: `u32::try_from(k.min(100)).unwrap_or(100)` clamps to usize first,
   then narrows safely.

2. Weak tests: test_recall_default_identity only checked length and the
   top hit. Strengthened to compare full ordered note_id+score lists across
   all positions, with all three knobs explicitly set to null.
   test_recall_fusion_strategy_override only validated string acceptance.
   Added a new unit test (fusion_strategy_change_produces_observable_ordering_difference)
   with a deterministic fixture where RRF and Weighted strategies MUST
   produce different orderings — proving the fusion_strategy override
   actually flows into fuse_candidates, not just validation.

Verified: cargo test -p khive-pack-memory --lib passes (62 unit tests).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ta-safety fixes

Per Ocean direction (2026-05-25): amend ADR-043 to formalize the V16
string-tag schema design, claim V16 in ADR-015 ledger, and fix the
data-safety bugs codex flagged. Defer sqlite-vec data preservation to
a follow-up issue.

ADR amendments:
- ADR-043 §1.1 (vector store column addition): replaces the old FK-based
  description with the actual V16 design (TEXT embedding_model column with
  composite index). Includes rationale for TEXT vs BLOB FK (hot-path join
  cost, end-to-end shape consistency with kkernel/env-vars/registry).
  Documents sqlite-vec rebuild behavior and follow-up.
- ADR-015 schema ledger: V16 row added with cluster-22 amendment notes.

High 3 fix — atomic unknown-model validation (handlers.rs):
- handle_remember now calls resolve_embedding_model(Some(name)) BEFORE
  create_note_with_decay_for_embedding_model. resolve_embedding_model is
  synchronous and doesn't load the model — it only checks registration.
  An unknown model is rejected before any note/FTS/vector row is written.

High 2 fix — scoped delete across all model stores (operations.rs):
- delete_note now iterates over registered_embedding_model_names() (new
  public method on KhiveRuntime) and deletes the note's vector from EVERY
  registered model's vector store. Previously only the default model's
  store was touched, leaving non-default vectors orphaned.

Medium fix — KHIVE_ADDITIONAL_EMBEDDING_MODELS warning on bad names:
- parse_embedding_model_list now logs tracing::warn for non-empty raw
  names that don't parse, instead of silently filtering them out. The
  function still returns a Vec (no startup failure on partial validity),
  but operator typos now surface at startup rather than as UnknownModel
  errors at request time.

Deferred (follow-up issue, see ADR-043 §1.1 final paragraph):
- High 1 — V16 backfill hard-codes 'all-minilm-l6-v2' for all regular
  vec_* tables, and sqlite-vec virtual tables are still dropped-and-rebuilt
  on schema mismatch (data loss for non-default deployments). A copy-with-
  default rebuild path is tracked separately because it requires a careful
  multi-step migration with vec0 INSERT INTO SELECT FROM and a verification
  step. Operators are warned via ADR §1.1 to back up before upgrading.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ohdearquant added a commit that referenced this pull request May 25, 2026
…ta-safety fixes

Per Ocean direction (2026-05-25): amend ADR-043 to formalize the V16
string-tag schema design, claim V16 in ADR-015 ledger, and fix the
data-safety bugs codex flagged. Defer sqlite-vec data preservation to
a follow-up issue.

ADR amendments:
- ADR-043 §1.1 (vector store column addition): replaces the old FK-based
  description with the actual V16 design (TEXT embedding_model column with
  composite index). Includes rationale for TEXT vs BLOB FK (hot-path join
  cost, end-to-end shape consistency with kkernel/env-vars/registry).
  Documents sqlite-vec rebuild behavior and follow-up.
- ADR-015 schema ledger: V16 row added with cluster-22 amendment notes.

High 3 fix — atomic unknown-model validation (handlers.rs):
- handle_remember now calls resolve_embedding_model(Some(name)) BEFORE
  create_note_with_decay_for_embedding_model. resolve_embedding_model is
  synchronous and doesn't load the model — it only checks registration.
  An unknown model is rejected before any note/FTS/vector row is written.

High 2 fix — scoped delete across all model stores (operations.rs):
- delete_note now iterates over registered_embedding_model_names() (new
  public method on KhiveRuntime) and deletes the note's vector from EVERY
  registered model's vector store. Previously only the default model's
  store was touched, leaving non-default vectors orphaned.

Medium fix — KHIVE_ADDITIONAL_EMBEDDING_MODELS warning on bad names:
- parse_embedding_model_list now logs tracing::warn for non-empty raw
  names that don't parse, instead of silently filtering them out. The
  function still returns a Vec (no startup failure on partial validity),
  but operator typos now surface at startup rather than as UnknownModel
  errors at request time.

Deferred (follow-up issue, see ADR-043 §1.1 final paragraph):
- High 1 — V16 backfill hard-codes 'all-minilm-l6-v2' for all regular
  vec_* tables, and sqlite-vec virtual tables are still dropped-and-rebuilt
  on schema mismatch (data loss for non-default deployments). A copy-with-
  default rebuild path is tracked separately because it requires a careful
  multi-step migration with vec0 INSERT INTO SELECT FROM and a verification
  step. Operators are warned via ADR §1.1 to back up before upgrading.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@ohdearquant ohdearquant changed the base branch from slice/v022/05-recall-knobs to main May 25, 2026 15:54
@ohdearquant
Copy link
Copy Markdown
Owner Author

Codex findings addressed

Verdict: REJECT → fixes pushed (per Ocean direction: amend ADR + fix data-safety bugs)

Critical: ADR-043 vs V16 schema mismatch

Fixed by amending ADR-043 to formalize the string-tag design that V16 ships. Commit 78f0a6e.

The ADR §1.1 (vector store column addition) now describes the actual implementation:

  • embedding_model TEXT NOT NULL DEFAULT 'all-minilm-l6-v2' column on regular vec_* tables
  • Composite index (subject_id, embedding_model) for scoped recall
  • Rationale for TEXT vs BLOB FK (hot-path join cost, end-to-end shape consistency with kkernel/env-var/registry, _embedding_models.model_id as natural join key)

ADR-015 ledger amended to claim V16 (vector_embedding_model_tag) with a cluster-22 amendment note explaining the addition.

High 3: Atomic unknown-model validation

Fixed. handle_remember now calls resolve_embedding_model(Some(model_name)) BEFORE create_note_with_decay_for_embedding_model. The resolver is synchronous (no model load) and checks registry membership only. Unknown name → error returned before any note/FTS/vector row is written.

High 2: Scoped delete across all model stores

Fixed. New public method KhiveRuntime::registered_embedding_model_names() enumerates the embedder registry. delete_note now iterates over every registered model and calls vectors_for_model(token, name).delete(id) for each — both on hard delete and on soft delete.

Medium: KHIVE_ADDITIONAL_EMBEDDING_MODELS silent drop

Fixed. parse_embedding_model_list now emits tracing::warn for non-empty raw names that fail to parse, instead of silently filtering them out. Operator typos surface at startup. Function still returns a Vec (partial validity → functional runtime) — the warning is unambiguous.

High 1: V16 backfill / sqlite-vec rebuild — deferred

The codex finding about hard-coded 'all-minilm-l6-v2' backfill and silent sqlite-vec table drop is acknowledged in ADR-043 §1.1 final paragraph as a documented limitation with operator backup warning. Implementing a per-table model-inferred backfill + sqlite-vec INSERT-INTO-SELECT-FROM rebuild requires a careful multi-step migration with verification — tracked separately rather than added to this PR.

Operators are warned: back up before upgrading any production deployment with persisted non-default embeddings.

Verification

  • cargo build --workspace: pass
  • cargo test -p khive-runtime -p khive-pack-memory --lib: 262 + 62 = 324 tests pass
  • cargo fmt --all --check: pass
  • deno fmt --check docs/adr/ADR-015...md docs/adr/ADR-043...md: pass

Commits: 78f0a6e (fixes), daa3c6b (cargo fmt), f188955 (deno fmt).

Ready for re-review.

ohdearquant and others added 2 commits May 25, 2026 11:57
…l consistency

Two findings from codex round 2 on PR #407:

1. Medium: runtime-level unknown-model atomicity. handle_remember validated
   embedding_model before calling create_note_with_decay_for_embedding_model
   (round 1 fix), but the runtime API itself was permissive — direct Rust
   callers (other packs, integration tests) would still hit the write-after-
   failure bug.

   Fix: resolve_embedding_model is now called at the start of
   create_note_inner BEFORE any note/FTS/vector write. The pack-handler check
   remains as an earlier error boundary, but atomicity is enforced at the
   lowest layer that performs the write.

2. High: ADR-043 internal inconsistency. The §1.1 amendment landed in round 1,
   but the rest of the ADR still contained normative text for the rejected
   FK-based design:
   - §8 (Backward compat) described embedding_model_id BLOB column + CHECK rebuild
   - Alternatives table rejected per-record model_id (which is what V16 ships)
   - Migration version section claimed V5 + BLOB FK

   Fix:
   - §8 rewritten to describe V14 + V16 (registry + tag column) split that
     actually shipped
   - Alternatives table row strikes-through with "Superseded by V16 (2026-05-25)"
     and points back to §1.1 for rationale
   - Migration version section split into V14 (cluster-20) and V16 (v022-polish)
     with the actual SQL each migration runs

   Codex's other round-1 findings remain addressed:
   - Critical (ADR drift) — now resolved end-to-end across §1.1, §8, alternatives, and §Migration version
   - High 2 (scoped delete) — confirmed by round-2 review at operations.rs:1395, 1408
   - Medium (env var warning) — confirmed at runtime.rs:549, 560

Verified: cargo test -p khive-runtime -p khive-pack-memory --lib passes
(262 + 62 tests), cargo fmt clean, deno fmt clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ohdearquant added a commit that referenced this pull request May 25, 2026
…l consistency

Two findings from codex round 2 on PR #407:

1. Medium: runtime-level unknown-model atomicity. handle_remember validated
   embedding_model before calling create_note_with_decay_for_embedding_model
   (round 1 fix), but the runtime API itself was permissive — direct Rust
   callers (other packs, integration tests) would still hit the write-after-
   failure bug.

   Fix: resolve_embedding_model is now called at the start of
   create_note_inner BEFORE any note/FTS/vector write. The pack-handler check
   remains as an earlier error boundary, but atomicity is enforced at the
   lowest layer that performs the write.

2. High: ADR-043 internal inconsistency. The §1.1 amendment landed in round 1,
   but the rest of the ADR still contained normative text for the rejected
   FK-based design:
   - §8 (Backward compat) described embedding_model_id BLOB column + CHECK rebuild
   - Alternatives table rejected per-record model_id (which is what V16 ships)
   - Migration version section claimed V5 + BLOB FK

   Fix:
   - §8 rewritten to describe V14 + V16 (registry + tag column) split that
     actually shipped
   - Alternatives table row strikes-through with "Superseded by V16 (2026-05-25)"
     and points back to §1.1 for rationale
   - Migration version section split into V14 (cluster-20) and V16 (v022-polish)
     with the actual SQL each migration runs

   Codex's other round-1 findings remain addressed:
   - Critical (ADR drift) — now resolved end-to-end across §1.1, §8, alternatives, and §Migration version
   - High 2 (scoped delete) — confirmed by round-2 review at operations.rs:1395, 1408
   - Medium (env var warning) — confirmed at runtime.rs:549, 560

Verified: cargo test -p khive-runtime -p khive-pack-memory --lib passes
(262 + 62 tests), cargo fmt clean, deno fmt clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@ohdearquant
Copy link
Copy Markdown
Owner Author

Codex round 2 verdict

REQUEST CHANGES → fixes pushed in 55c1f4a

Round 2 surfaced 2 new findings (0 Critical, 1 High, 1 Medium):

High — ADR-043 internal inconsistency

Round 1's §1.1 amendment landed cleanly, but the rest of the ADR still contained normative text for the rejected FK design:

  • §8 (Backward compat) described embedding_model_id BLOB migration + CHECK rebuild
  • Alternatives table rejected per-record model_id (which is exactly what V16 ships)
  • Migration version section claimed version=5 + BLOB FK

Fixed: §8 rewritten to describe the actual V14 + V16 split that shipped. Alternatives table row struck through with "Superseded by V16 (2026-05-25)" pointing to §1.1 for rationale. Migration version section split into V14 (cluster-20) and V16 (v022-polish) sub-sections with the actual SQL each runs.

Medium — Runtime-level atomicity gap

Round 1 fix to handle_remember was correct but the runtime layer (create_note_inner in khive-runtime/src/operations.rs) still had the write-after-failure pattern: note → FTS → embed (where unknown model errored).

Fixed: resolve_embedding_model now called at start of create_note_inner BEFORE any note/FTS/vector write. Direct Rust callers (other packs, integration tests) get atomicity at the lowest layer. The handler-level validation remains as an earlier error boundary.

Verification

  • cargo test -p khive-runtime -p khive-pack-memory --lib: 262 + 62 tests pass
  • cargo fmt --check: clean
  • deno fmt --check on ADR-043: clean

Confirmed in round 2's "Looks Right" section: ADR-015 V16 row landed, sqlite-vec deferral documented, handle_remember validates before pack write, delete_note iterates all model stores, env var emits tracing::warn, vector search remains model-scoped, migration tests rebaselined for V16.

Ready for round 3 (or APPROVE).

The codex round 2 full review file was generated locally — see commit 55c1f4a for the fix details. Findings are summarized above.

@ohdearquant ohdearquant marked this pull request as draft May 25, 2026 17:25
@ohdearquant ohdearquant marked this pull request as ready for review May 25, 2026 17:25
@ohdearquant ohdearquant reopened this May 25, 2026
@ohdearquant ohdearquant merged commit 232e774 into main May 25, 2026
@ohdearquant ohdearquant deleted the slice/v022/07-dual-embedding branch May 25, 2026 17:32
ohdearquant added a commit that referenced this pull request May 25, 2026
* feat(pack-memory): wire khive-retrieval as recall composer (ADR-011/021)

Route pack-memory's fuse_candidates through khive_retrieval::fuse_search_results,
making khive-retrieval a real consumed facade instead of an orphan crate.

- Add khive-retrieval dep to khive-pack-memory/Cargo.toml
- Replace direct fuse_with_strategy call with retrieval adapter
  (CandidateMeta side-map, HybridConfig builder, FusionStrategy conversion)
- Fix issue #309: resolve --all-features compile failures in khive-retrieval
  (stale SqliteStore imports, missing NodeId/LinkStore imports)
- Add 5 integration tests (3 fusion_surface, 2 pack-memory recall adapter)
- RRF k=1 discriminator test proves strategy propagation (30x score gap)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(pack-memory): expose top_k/fusion_strategy/score_floor knobs on recall (ADR-033 §6)

- Add three optional per-request fields to RecallParams: top_k (usize),
  fusion_strategy (string), and score_floor (f32)
- fusion_strategy validated against {"rrf","weighted","union"}; clear error
  with valid values on invalid input
- top_k overrides the result limit for a single call (capped at 100)
- score_floor applied as a post-filter on the composite score after compute_score
- Add parse_fusion_strategy_str helper; wire override into cfg.fuse_strategy
  before passing to fuse_candidates
- Add 4 integration tests: default_identity, top_k_override,
  fusion_strategy_override (including rejection), score_floor
- Document knobs in ADR-033 §6.1 with table, semantics, and example DSL

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* style(adr-033): deno fmt re-pad recall knob table (post-merge cleanup)

* feat(embedding): dual-model registry (MiniLM + paraphrase) per ADR-043

Multi-model embedding support landed across the runtime + storage + memory
stack. Workspace dual-embedding now reachable end-to-end:

khive-runtime:
  - RuntimeConfig.additional_embedding_models: Vec<EmbeddingModel>
  - Replaces single OnceCell<embedder> with HashMap<model_name, embedder>
  - default_embedder_name() + embedder(name) public methods
  - KHIVE_ADDITIONAL_EMBEDDING_MODELS env-var parsing
  - configured_embedding_models() helper enumerates active set

khive-db:
  - V16 migration: add `embedding_model TEXT NOT NULL DEFAULT '<default>'`
    column to vectors table with backfill + composite index
  - VectorStore.insert / search scoped by embedding_model

khive-storage:
  - VectorRecord carries model tag
  - vector search params include model scope

khive-pack-memory:
  - recall + remember accept optional embedding_model arg
  - validation: must be a registered model name

kkernel:
  - engine list now returns real loaded models (no longer empty Vec)
  - engine migrate / drift-check still return not-implemented (#380/#385)

Notes:
- 16 files changed, +582/-138 lines
- Tests rebaselined for V16 (failed_migration_rolls_back tests V17 now;
  store_ddl_then_event_migration_is_idempotent expects V16 head)
- Workspace: cargo build + cargo test + clippy clean + fmt clean

Lattice gap status: N/A — lattice-embed 0.2.4 already exposes both
MiniLM + paraphrase as 384-d local models with EmbeddingRoutingConfig
primitives. khive-runtime now uses these directly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* tune(recall): grid search infra + PARTIAL default changes

* fix(retrieval): correct doctest import — use re-export at crate root

Addresses codex finding on PR #405: `khive_retrieval::hnsw::HnswIndex`
doesn't resolve because `hnsw` is not a public submodule — `HnswIndex` is
re-exported at the crate root (lib.rs:145). The doctest at persist/mod.rs:29
must use the public facade import.

Closes the remaining gap on issue #309 (--all-features doctest failure).

Verified: `RUSTC_WRAPPER= cargo test --offline -p khive-retrieval --all-features --doc` passes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* revert(memory): keep RecallConfig defaults — corpus ceiling made changes unjustified

Codex review on PR #408 flagged that the three default changes from the
tune sweep (temporal_half_life_days 30 to 14, decay exp to hyp, multiplier
20 to 10) were made even though REPORT.md explicitly states the synthetic
eval set produced identical recall@10 = 0.9333 for ALL 116 configs — the
landscape was too flat to discriminate these parameters.

Reverts those three lines back to their prior values. The tuned-config.toml
artifact stays as an experimental record; the grid_search.py infra stays
runnable; only RecallConfig::default() is restored.

A discriminating eval corpus (embed-enabled, synonym queries, partial
matches) is the prerequisite before changing runtime defaults.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(pack-memory): address PR #406 codex findings — top_k cast + stronger tests

Two Medium findings from codex review:

1. top_k cast bug: `(k as u32).min(100)` truncates k before capping. A
   request with `top_k = 4_294_967_297` (larger than u32::MAX) truncates
   to 1 BEFORE the cap is applied, so the result limit becomes 1, not 100.
   Fixed: `u32::try_from(k.min(100)).unwrap_or(100)` clamps to usize first,
   then narrows safely.

2. Weak tests: test_recall_default_identity only checked length and the
   top hit. Strengthened to compare full ordered note_id+score lists across
   all positions, with all three knobs explicitly set to null.
   test_recall_fusion_strategy_override only validated string acceptance.
   Added a new unit test (fusion_strategy_change_produces_observable_ordering_difference)
   with a deterministic fixture where RRF and Weighted strategies MUST
   produce different orderings — proving the fusion_strategy override
   actually flows into fuse_candidates, not just validation.

Verified: cargo test -p khive-pack-memory --lib passes (62 unit tests).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(embedding): address PR #407 codex findings — ADR amendment + 3 data-safety fixes

Per Ocean direction (2026-05-25): amend ADR-043 to formalize the V16
string-tag schema design, claim V16 in ADR-015 ledger, and fix the
data-safety bugs codex flagged. Defer sqlite-vec data preservation to
a follow-up issue.

ADR amendments:
- ADR-043 §1.1 (vector store column addition): replaces the old FK-based
  description with the actual V16 design (TEXT embedding_model column with
  composite index). Includes rationale for TEXT vs BLOB FK (hot-path join
  cost, end-to-end shape consistency with kkernel/env-vars/registry).
  Documents sqlite-vec rebuild behavior and follow-up.
- ADR-015 schema ledger: V16 row added with cluster-22 amendment notes.

High 3 fix — atomic unknown-model validation (handlers.rs):
- handle_remember now calls resolve_embedding_model(Some(name)) BEFORE
  create_note_with_decay_for_embedding_model. resolve_embedding_model is
  synchronous and doesn't load the model — it only checks registration.
  An unknown model is rejected before any note/FTS/vector row is written.

High 2 fix — scoped delete across all model stores (operations.rs):
- delete_note now iterates over registered_embedding_model_names() (new
  public method on KhiveRuntime) and deletes the note's vector from EVERY
  registered model's vector store. Previously only the default model's
  store was touched, leaving non-default vectors orphaned.

Medium fix — KHIVE_ADDITIONAL_EMBEDDING_MODELS warning on bad names:
- parse_embedding_model_list now logs tracing::warn for non-empty raw
  names that don't parse, instead of silently filtering them out. The
  function still returns a Vec (no startup failure on partial validity),
  but operator typos now surface at startup rather than as UnknownModel
  errors at request time.

Deferred (follow-up issue, see ADR-043 §1.1 final paragraph):
- High 1 — V16 backfill hard-codes 'all-minilm-l6-v2' for all regular
  vec_* tables, and sqlite-vec virtual tables are still dropped-and-rebuilt
  on schema mismatch (data loss for non-default deployments). A copy-with-
  default rebuild path is tracked separately because it requires a careful
  multi-step migration with vec0 INSERT INTO SELECT FROM and a verification
  step. Operators are warned via ADR §1.1 to back up before upgrading.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* docs(tune): document khive_contract dependency + run instructions

Codex Major 2 (PR #408): the tune CLI is not re-runnable from a fresh
clone of this branch because khive_contract is in the parent package
(slice 06, PR #403 — already merged to main). Codex tested slice 08
in isolation and hit ModuleNotFoundError.

Adds tests/khive-contract/tune/README.md explaining:
- Install khive_contract first: uv pip install -e . from
  tests/khive-contract/
- Run via uv run python -m tune --quick (or full)
- Documents the corpus-ceiling limitation surfaced in REPORT.md
- Explicit note that RecallConfig::default() was intentionally NOT
  changed (defaults reverted in d943b7f based on codex Major 1)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* style: cargo fmt --all

* style: deno fmt ADR tables

* fix(test): use single-token query matching all fixture memories

* fix(test): align partial_config test with reverted default decay_model

* fix(embedding): codex round 2 — runtime-layer atomicity + ADR internal consistency

Two findings from codex round 2 on PR #407:

1. Medium: runtime-level unknown-model atomicity. handle_remember validated
   embedding_model before calling create_note_with_decay_for_embedding_model
   (round 1 fix), but the runtime API itself was permissive — direct Rust
   callers (other packs, integration tests) would still hit the write-after-
   failure bug.

   Fix: resolve_embedding_model is now called at the start of
   create_note_inner BEFORE any note/FTS/vector write. The pack-handler check
   remains as an earlier error boundary, but atomicity is enforced at the
   lowest layer that performs the write.

2. High: ADR-043 internal inconsistency. The §1.1 amendment landed in round 1,
   but the rest of the ADR still contained normative text for the rejected
   FK-based design:
   - §8 (Backward compat) described embedding_model_id BLOB column + CHECK rebuild
   - Alternatives table rejected per-record model_id (which is what V16 ships)
   - Migration version section claimed V5 + BLOB FK

   Fix:
   - §8 rewritten to describe V14 + V16 (registry + tag column) split that
     actually shipped
   - Alternatives table row strikes-through with "Superseded by V16 (2026-05-25)"
     and points back to §1.1 for rationale
   - Migration version section split into V14 (cluster-20) and V16 (v022-polish)
     with the actual SQL each migration runs

   Codex's other round-1 findings remain addressed:
   - Critical (ADR drift) — now resolved end-to-end across §1.1, §8, alternatives, and §Migration version
   - High 2 (scoped delete) — confirmed by round-2 review at operations.rs:1395, 1408
   - Medium (env var warning) — confirmed at runtime.rs:549, 560

Verified: cargo test -p khive-runtime -p khive-pack-memory --lib passes
(262 + 62 tests), cargo fmt clean, deno fmt clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* ci: force re-trigger

* ci: force re-trigger via doc whitespace touch

* style: deno fmt

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant