Repository navigation
fix(plugin): stop clamping storage list queries to 500 rows - #2464
akushonkamen wants to merge 2 commits into
Conversation
## Summary - `clampLimit` honored explicit limits only up to 500, so every "fetch a wide window" caller (`limit: 100_000 / 5_000 / 2_000`) got the newest 500 rows back. - `buildPageClauses` treated a missing `limit` as a 500-row page, so unpaginated scans (L3 policy clustering, L2 candidate/dedup sweeps, decision guidance, retrieval candidates, q-substring counting) silently never looked past the newest 500 rows. - Now an omitted `limit` means all matching rows (no LIMIT clause — the contract MemTensor#2076 already pinned for `traces.list({ episodeId })`), and explicit limits are honored up to a 100_000 safety ceiling. - Update the two `traces-count` assertions that pinned the old cap and add regression coverage at the helper, repo, and MemoryCore layers. ## Why Fixes MemTensor#2401. Viewer counters (policies/episodes/skills/world models) reported at most 500, `exportBundle` dropped everything past 500 rows per table, and internal consumers made decisions over truncated data: L3 clustering only saw the newest 500 active policies, L2 dedup re-induced duplicates from outside the window, and the q-substring trace count under-counted. MemTensor#1954 raised the cap on a release branch but never landed the default/clamp semantics on main; this change includes and supersedes it. ## Validation - `npx vitest run tests/unit/storage/` (95/95) - `npx vitest run tests/unit/pipeline/memory-core.test.ts` (51/51) - `npx vitest run tests/unit/bridge/methods.test.ts tests/unit/server/http.test.ts` (86/86) - `npm run test:unit` (1602 passed, 2 skipped) - `npm run lint` (tsc --noEmit, clean) New tests fail on the pre-fix code (clampLimit(100_000) === 500, `LIMIT 500` injected into unpaginated queries, counters/export returning 500 instead of 600 seeded rows).
🤖 Open Code ReviewTarget: PR #2464 🔍 OpenCodeReview found 3 issue(s) in this PR. 1.
|
|
…ling Addressing open-code-review findings on MemTensor#2464: - buildPageClauses now treats an invalid limit (non-finite, 0, negative) exactly like an omitted one — no LIMIT clause, never a substituted page (OCR finding MemTensor#1). PageOptions JSDoc updated to match. - The 100_000 magic number is now the exported MAX_QUERY_LIMIT constant (OCR finding MemTensor#3), and clampLimit's defensive fallback for invalid input is the ceiling itself rather than a 500-row page, consistent with the unbounded semantics of MemTensor#2401. Tests updated red→green: invalid-limit assertions now pin "no LIMIT clause" behavior at buildPageClauses level plus MAX_QUERY_LIMIT capping at clampLimit level.
|
Thanks @Memtensor-AI for the careful review — all three points are fair. Updates in cd70df6: Addressed in this commit
On #2 (unbounded scan as a breaking change) — this one is intentional, for the maintainer's call:
CI note: the autotest run reports |
|
|
Follow-up on the heads-up above: the re-run at 16:43 UTC on this PR's head ( Opened #2476 to track with the full timeline. Could a CI admin check the |
Fixes #2401
Summary
Two defects in
apps/memos-local-plugin/core/storage/repos/_helpers.tsmade everylist query in the local plugin's SQLite storage layer silently truncate at 500 rows:
clampLimithard-capped explicit limits at 500 (old_helpers.ts:58).Callers that deliberately asked for a wide window — the established
"uncapped fetch" idiom
limit: 100_000 / 5_000 / 2_000— still got back thenewest 500 rows.
buildPageClausestreated a missinglimitas a 500-row page (old_helpers.ts:51,opts?.limit ?? 500). Unpaginated scans never looked pastthe newest 500 rows.
New semantics
limitmeans all matching rows:buildPageClausesappends noLIMIT/OFFSETat all. This is the exact contract issue local-plugin v2.0.8: gateway CPU 100% — synchronous full-table vector scan (scanAndTopK) + unbounded trace re-insertion from dedup pagination cap #2076 already pinnedfor
traces.list({ episodeId })(theshouldUseUncappedEpisodeScanspecial case in
core/storage/repos/traces.ts); this PR generalizes it toevery repo instead of special-casing one filter shape. (SQLite has no
OFFSETwithoutLIMIT, so an offset without a limit is simply not a pagerequest.)
limitis honored, clamped only by a 100_000-row safetyceiling (the shape fix: remove 500-row cap that truncated viewer count displays #1954 intended). The invalid-input fallback
(
NaN/<= 0) stays at 500.Root cause
#1954 (merged to
dev-v2.0.25) only raised the ceiling on a release branch andnever landed on
main; the?? 500default was left untouched. This PRincludes and supersedes #1954: it fixes both the ceiling and the default.
Affected call sites (all fixed by the two helper changes)
core/pipeline/memory-core.tscountPolicieslist({status, limit:100_000})/list({status})countWorldModelsworldModel.list({limit:100_000})countEpisodesepisodes.list({sessionId, limit:100_000})countSkillsskills.list({status, limit:100_000})countTracesq pathtraces.list({...})— "Walk all matching traces (no limit)"listTracessearch+group scantraces.list({...})— "scan, filter, then paginate by distinct turn key"exportBundlelimit:100_000 / 5_000 / 2_000Outside
memory-core.ts(defect B — no-limit scans)core/memory/l3/l3.ts:107policies.list({status:"active"})core/memory/l2/l2.ts:466policies.list({status:"candidate"})core/memory/l2/l2.ts:602policies.list({limit:5_000})core/retrieval/decision-guidance.ts:113policies.list({status:"active"})core/pipeline/retrieval-repos.ts:136–139repos.policies.list({...})Untouched on purpose (below the old cap, per "one logical change per PR"):
core/pipeline/orchestrator.ts:242(limit 20),core/skill/skill.ts:347andcore/feedback/feedback.ts:258(limit 200), and the separate localclampLimitin
core/storage/repos/hub.ts:628. The fixedapiLogs.listwindow reported in#2380 is a different root cause and is not addressed here.
Behavior-change notes
The no-limit default change is deliberate: the no-limit call sites above
(
countPolicieswith-q,countTracesq path, thelistTracessearch+groupscan, and the four scans outside
memory-core.ts) are all "give me everything"intents on low-frequency background paths, and
their memory use now scales with table size instead of silently under-reading.
Viewer list endpoints (
listTraces/listPolicies/...) are unaffected — theyalways pass explicit
limit/offsetpages.episodes.listClosedPagekeeps itsexplicit
?? 500page size (a real page request, unchanged).Tests
New regression coverage (fails on the pre-fix code, passes now):
tests/unit/storage/page-limit.test.ts(new, 10 tests)clampLimit(100_000) > 500, mid-size limits preserved, small limits untouched, invalid input still falls back positive;buildPageClauseswithout limit emits noLIMIT; explicit limits kept, large limits not clamped;policies.list({status})(L3/L2 call shape) andpolicies.list({limit:5_000})(L2 dedup shape) return everything;policies.countexact;traces.list({sessionId})(q-count call shape) returns everything.tests/unit/pipeline/memory-core.test.ts(+3 tests, "> 500 rows (2.0.20 still truncates internal queries at 500 rows: countPolicies/countEpisodes/countSkills/countWorldModels report 500, and no-limit call sites lose rows (#1954 only changed the ceiling) #2401)")countPolicies/countEpisodes/countSkills/countWorldModels) return 600, not 500;countTraces({q})substring path counts hits outside the newest-500 window (12, was 2);exportBundlereturns all 600 rows per table, not 500.tests/unit/storage/traces-count.test.ts— the two assertions that pinnedthe old cap ("list with no limit still caps at 500") now assert the new
contract (600 rows; small pages still page), keeping the Fix #1593: Web UI memory count stuck at 500, actual traces exceed 1400 #1877
count()accuracy assertions.
Red→green evidence (pre-fix run on
main@a7367d0):Validation (all offline, local SQLite):
npx vitest run tests/unit/storage/— 95/95npx vitest run tests/unit/pipeline/memory-core.test.ts— 51/51npx vitest run tests/unit/bridge/methods.test.ts tests/unit/server/http.test.ts— 86/86npm run test:unit— 182 files, 1602 passed, 2 skipped (zero regressions)npm run lint(tsc -p tsconfig.json --noEmit) — clean