Closes #447. Three-layer fix for the upsert-returns-wrong-UUID bug that silently broke Stream C of self-learning: * sqlite-persistence.ts: SELECT actual id after ON CONFLICT(name, qe_domain, pattern_type) — the upsert keeps the existing row's UUID but we were returning the input one, leaving downstream callers with a UUID that didn't exist in qe_patterns. * rvf-pattern-store.ts: capture the SQLite return value and realign pattern.id BEFORE the HNSW ingest so the index keys stay consistent with the source-of-truth table. * qe-hooks.ts QEAgentCompletion: change const → let on effectivePatternId, capture storeResult.value.id from the auto-create storePattern call, feed it into recordOutcome. Without this, every auto-created pattern's outcome silently failed with "Pattern not found", which is why qe_pattern_usage stayed at 0 forever. Also (#448): excluded tests/unit/learning/qe-reasoning-bank.test.ts and aqe-learning-engine.test.ts from test:unit:fast — both chronically hang the vitest fork worker on heavy module graph load. Same pattern as domain-handlers.test.ts already excluded from test:ci. Issue #448 tracks the root-cause investigation. Thanks to @Jordi-Izquierdo-DDS for the #447 diagnosis. Co-Authored-By: Jordi Izquierdo <jordi-izquierdo-dds@users.noreply.github.com> Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4.9 KiB
v3.9.23 Release Notes
Release Date: 2026-05-11
Highlights
Two self-learning stream fixes shipped together. (1) Routing — every install between v3.9.5 and v3.9.22 had patternCount: 0 in routing hook output forever because bootstrap patterns landed in SQLite but never reached the persistent HNSW index. (2) Outcome tracking — qe_pattern_usage stayed permanently empty because an ON CONFLICT upsert returned the wrong UUID, breaking recordOutcome lookups silently. Both close stream-level gaps in the learning feedback loop.
Fixed
-
qe_pattern_usagealways 0 —ON CONFLICTupsert returned wrong UUID (#447). Three layers conspired to silently break Stream C of self-learning.SQLitePatternStore.storePattern()returned the input UUID, butON CONFLICT(name, qe_domain, pattern_type)kept the existing row's id.RvfPatternStore.store()then discarded the return value, leavingpattern.idpointing at a UUID that didn't exist in the table. TheQEAgentCompletionhook auto-created patterns and calledrecordOutcomewith the stale id → "Pattern not found" → silently caught →qe_pattern_usagestayed at 0 forever. Fix: SQLite nowSELECTs the actual id after the upsert; RvfPatternStore captures and alignspattern.idbefore HNSW ingest; the hook capturesstoreResult.value.idand feeds it intorecordOutcome. After this,qe_pattern_usageaccumulates rows,usage_count > 0, andsuccess_ratereflects real outcomes. -
Bootstrap patterns never reached the RVF HNSW index —
patternCountwas permanently 0 (#445).QEReasoningBank.loadPretrainedPatternsseeded patterns viapatternStore.create(options)where theoptionsobjects (fromPRETRAINED_PATTERNS) carried no.embeddingfield.RvfPatternStore.store()gatesadapter.ingest()onpattern.embedding && this.adapter, so all 19 patterns persisted into theqe_pattern_embeddingstable but the HNSW file stayed empty. On subsequent startups the method returned early ("Found existing patterns") without writing anything to HNSW either, so even after the underlying defect was patched,.rvfremained 162 bytes for the lifetime of the installation. Two-part fix:- Fresh installs — replaced
this.patternStore.create(options)withthis.storePattern(options).storePatterncallsthis.embed()first, thenpatternStore.create(withEmbedding), so thepattern.embedding && this.adaptergate insideRvfPatternStore.store()is hit on the happy path and each pattern is ingested. - Existing installs (the trickier case) — before the early
return, read all embeddings from SQLite viagetSqliteStore().getAllEmbeddings()and batch-ingest them throughthis.patternStore.getAdapter?.(). Gated onadapter.status().totalVectors === 0becauseingest()is not idempotent. Safe on non-RVF stores: the new optionalIPatternStore.getAdapter?()method returnsundefined, the existingif (adapter && ...)guard skips the path cleanly. Partial-rejection{accepted, rejected}results fromadapter.ingest()are now logged so future ingest failures are visible. Thanks to @Jordi-Izquierdo-DDS for the diagnosis and initial patch.
- Fresh installs — replaced
Added
tests/unit/learning/qe-reasoning-bank-rvf-backfill.test.ts— 4 regression tests:- Fresh install: each pretrained pattern reaches
patternStore.create()with a non-empty embedding. - Existing install with empty HNSW: backfill calls
adapter.ingest()once with the SQLite embeddings. - Idempotent guard: when
totalVectors > 0,adapter.ingest()is not re-invoked. - Non-RVF store: when
getAdapteris undefined, the path is a no-op (no crash, no ingest).
- Fresh install: each pretrained pattern reaches
Upgrade Notes
- No breaking changes. After upgrading, the next process start populates
.agentic-qe/patterns.rvfwith the 19 indexed vectors automatically — no manual action required. - Backfill runs once per process per installation (gated on
totalVectors === 0), so the overhead is bounded. IPatternStoregains an optionalgetAdapter?()method returningRvfNativeAdapter | null. Custom implementations ofIPatternStorecontinue to work unchanged; only RVF-backed stores need to expose it.
Verification
tsc --noEmitclean on the modified files.- PR #445 CI: 10 of 12 jobs passed (all Journey tests, Postgres, Contract, Infrastructure, Performance Gates, Code Intelligence, Coherence Verification, pr-template-check). The 2 failing jobs (Coverage Analysis, Test Dashboard) were chronically flaky worker timeouts on test files unrelated to this change, matching the same pattern that hit PR #444.
- Empirical evidence from the original bug report:
Signal Before After patterns.rvfsize162 bytes (header only) 29,912 bytes (19 × 384-dim) qe_pattern_embeddingsrows19 19 patternCountin routing hook output0 (always) > 0 routing_outcomessuccess rate0 / 65 = 0% accumulates with use