fix(retrieval): order the tier-2 vector scan window by recency, not physical position - #2310
fix(retrieval): order the tier-2 vector scan window by recency, not physical position#2310simpleqt wants to merge 2 commits into
Conversation
…hysical position scanAndTopK bounded the cosine candidate set with a bare LIMIT — SQLite returned rows in whatever order its physical scan visited them, so the globally best vector could be excluded from similarity computation purely by physical position. With the v2.0.14 defaults (tier2TopK=5, candidatePoolFactor=4) the effective window was 80-100 arbitrary rows. The scan now takes an orderBy option and all four vector repos pass their recency column (traces: ts DESC; policies/world_model/skills: updated_at DESC, all DESC-indexed), making the bounded window a deterministic, meaningful candidate policy — most recent qualifying rows first — that existing indexes already serve. Fixes MemTensor#2233
🤖 Open Code ReviewTarget: PR #2310 🔍 OpenCodeReview found 2 issue(s) in this PR. 1.
|
The open code review on MemTensor#2310 flagged that interpolating an unvalidated public-interface option into SQL is one careless future caller away from injection. Reject anything that is not an identifier list with optional ASC/DESC at the boundary; the repos' constant strings pass unchanged.
|
Both open-code-review findings addressed in 8e038cc:
|
✅ Automated Test Results: PASSEDAll tests passed (20/20 executed). memos_local_plugin/unit: 20/20. Duration: 5s [advisory, non-gating] AI-generated tests on branch test/auto-gen-d7dd1c5d708f6868-20260831101902: 93/93 passed — these do NOT affect the PR verdict; review the branch manually. Branch: |
Fixes #2233
Problem
scanAndTopKbounded the cosine candidate set with a bareLIMIT <hardCap>— noORDER BY— so the window was SQLite's arbitrary physical scan order. The globally best vector could be excluded from similarity computation purely by physical position (cosine 1.0, zero recall probability). With the v2.0.14 defaults (tier2TopK=5,candidatePoolFactor=4) the effective vector scan cap was ~80–100 arbitrary rows per channel.While writing the regression test I also confirmed the arbitrariness empirically: the same query over the same rows inserted in a different physical order returns a different winner under the unordered cap.
Fix
scanAndTopKtakes an optionalorderBy(applied before thehardCapLIMIT); the option's contract is repo-internal constant strings, never request input.traces→ts DESC, id DESC;policies/world_model/skills→updated_at DESC, id DESC. Every one of these is already served by an existingDESCindex (idx_traces_owner/share/session_ts,idx_policies_owner/status/...,idx_skills_owner), so the bounded window stays index-friendly.scanAndTopK: the window is now a deterministic candidate policy (most recent qualifying rows first) rather than a physical accident.This is the "deterministic, meaningful candidate policy" alternative the issue lists — it does not change the bounded-scan approximation itself, it makes the bound well-defined.
Testing
New case in
tests/unit/storage/vector-stream.test.ts: two perfect-match vectors (one old, one new) plus orthogonal fillers; withorderBy: "ts DESC, id DESC"andhardCap: 1, the window deterministically resolves to the newest perfect match in both physical row layouts (insertion order and reversed). The unordered path is deliberately left un-asserted — which row it returns is layout-dependent, which is exactly the bug.vitest run tests/unit→ 1550 passed, 2 skipped (179 files)tsc -p tsconfig.json --noEmit→ clean