Skip to content

Releases: opendb-ai/openDB

v2.0.1 — a superseded fact could outrank the fact that replaced it

Choose a tag to compare

@wuwangzhang1216 wuwangzhang1216 released this 04 Aug 20:25
c2a5a66

Patch for a knowledge-update regression introduced in 2.0.0. Upgrade if you are on 2.0.0.

2.0.0 shipped rank_weight_recency = 0.5. At that weight a stale memory beat the memory that replaced it whenever the stale row matched one more query token.

Two rows, 947 and 795 days old, asked "frontend framework team":

"The team uses React for the frontend."          lexical=1.0000  recency=0.0051  -> 1.3038  ← won
"We migrated the frontend from React to Svelte."  lexical=0.4735  recency=0.8412  -> 1.1940

Lexical ratio-to-best handed the older row a 0.53 edge while recency returned only 0.42 after weighting. The row saying the team migrated away from React lost to the row saying it uses React — the knowledge-update case this project is built around, regressed against 1.5.0.

memory_stress_bench.py catches it. 2.0.0 published that suite's result as 23/23 while the shipped code scored 22/23, confirmed by A/B against the commit before the architecture-review branch.

The fix

rank_weight_recency 0.5 → 0.75. The inversion flips below 0.63, so this leaves margin without reaching 1.0, which would assert recency and relevance matter equally — a stronger claim than anything measured here.

2.0.0 2.0.1
memory stress suite 22/23 23/23
pooled LongMemEval R@5 79.1% 79.1%
CodeMemEval R@5, questions as written 100% 100%
CodeMemEval R@5, questions restated 75.0% 79.2%
temporal ranking suite top-1 80.0% 80.0%

Nothing regressed, including the two cases the temporal suite already fails.

The temporal suite cannot select this value — it returns an identical 80.0% top-1 with identical per-category results for every weight in [0.25, 1.5], and its own cross-validation prefers 0.25, which fails the case harder. The choice rests on the two suites that can see the difference.

Also fixed

  • RankingWeights carried a second, independent default. from_settings reads config, but a bare RankingWeights() used the dataclass default — changing only config.py left every caller that skips settings on the old weights. A test now asserts the two agree.
  • ranking.py's module docstring described an algorithm the module does not implement (Reciprocal Rank Fusion plus a bare hyperbolic recency map; the code has used ratio-to-best and a span-weighted blend since the rewrite). It is the first thing read when a ranking result looks wrong, and it sent two separate investigations down the wrong path.
  • Removed a duplicated tier assignment in fuse().

Docs

REPORT and README carry numbers re-measured against the fixed scorer: the restated CodeMemEval split rises to 79.2% R@5 (19/24, 95% CI [59.5%, 90.8%]) and pooled R@1 to 52.3%.

Tests 417 → 420, including a regression test pinning the 0.63 boundary so a future weight change fails loudly instead of silently degrading knowledge-update recall.

Full changelog: https://github.com/opendb-ai/openDB/blob/main/CHANGELOG.md

v2.0.0 — architecture review remediation

Choose a tag to compare

@wuwangzhang1216 wuwangzhang1216 released this 04 Aug 18:38
1e112c2

An architecture review of the storage and memory layers found defects reproducible on the default configuration. This release fixes them, adds regression coverage that fails against the previous code, and withdraws or restates published claims that were measured on an instrument that could not fail.

Verification is A/B — the same behaviour probe against the previous release and against this one:

v1.5.0:      14 defects present in this tree
v2.0.0:       0 defects present in this tree

Tests 282 → 417. With PostgreSQL available: 417 passed. Without: 403 passed, 14 skipped.

⚠️ Breaking: defaults changed

setting was now
backend postgres sqlite
host 0.0.0.0 127.0.0.1
cors_origins ["*"] []
vision_enabled true false

With auth unenforced, the old combination made the workspace reachable from the local network, scriptable by any web page the developer visited, and uploaded indexed images to a third-party API off an ambient env var. An upgrade that does not set these explicitly will find a Postgres-backed server running on SQLite and no longer reachable off localhost.

Data loss and correctness

  • Supersede destroyed data on the default memory type. Three dated bug-fix records about one component collapsed into one, and superseded_id pointed at a row that no longer existed. Prior versions now go to memory_revisions, readable via memory_history().
  • Reading a memory reset its time-decay clock. A 400-day-old fact that had been read once outranked the fresh fact that replaced it.
  • Recall set confidence = 1.0 on every hit, so the FSRS curve could only demote memories nobody read. Recall is read-only now.
  • total was the candidate-window size, not the match count. Any offset >= 60 returned nothing however many memories matched.

Transactions, retrieval, PostgreSQL parity

  • All writes go through one write_txn(): BEGIN IMMEDIATE, rollback on any BaseException, bounded retry with jitter on SQLITE_BUSY.
  • CreateInvoice was a single unicode61 token, so invoice, create invoice and "where do we create invoices" all returned zero results — on the project's own flagship example.
  • Japanese and Korean were routed to jieba, a Chinese segmenter. Tokenization is now fingerprinted and skew is reported by opendb doctor.
  • PostgreSQL had carried none of the SQLite fixes, plus an unconditional BEFORE UPDATE trigger. It now passes the same conformance suite, every assertion parametrised over both backends.

Security

ApiKeyMiddleware was implemented, documented, unit-tested — and never mounted, so FILEDB_AUTH_API_KEY did nothing. _resolve_workspace swallowed the ValueError from relative_to() and returned the path anyway, making the only containment check a no-op. A crafted grep pattern hung over 20 seconds on a two-line file. sort_field/sort_dir were interpolated into ORDER BY unvalidated on PostgreSQL. Uploads read the whole body then checked the size.

⚠️ Benchmark claims withdrawn and restated

"100% R@5" is withdrawn. It was measured on longmemeval_oracle.json, where all 500 questions satisfy set(haystack_session_ids) == set(answer_session_ids) with zero distractors — R@5 is arithmetically guaranteed for 497/500. Pooled mode indexes every question's sessions into one 882-session corpus: R@5 = 79.1%, median recall 7.5 ms.

CodeMemEval is restated with its error bars. 96.3% is 26/27, whose 95% interval is [81.7%, 99.3%] — 17.6 points wide, overlapping the cheap reader's 92.6% across nearly its whole width. This benchmark cannot tell those two readers apart, and per-category results (n = 3–6) are now reported as counts.

The paraphrase split is now measured rather than asserted. Same corpus, same gold sessions, only the question wording differs:

Questions R@1 R@5
as written 100% (24/24) 100% (24/24)
restated, 0% lexical overlap 45.8% (11/24) 75.0% (18/24)

That gap is the honest boundary of a pure-lexical retriever, and it was previously averaged away.

The LLM judge is unvalidated. Both accuracy figures come from a judge sharing a model family with the reader it grades, and nothing has yet measured how often it accepts a wrong answer — treat them as upper bounds. codemem_hard.py --run-judge now measures it, scoring 10 plausible-but-false answers against correct-answer controls (a judge that rejects everything otherwise scores a flawless 0% false-accept rate).

Notes

  • Eight existing tests asserted the defective behaviour (e.g. test_recall_resets_confidence_to_one literally tested the bug). They are rewritten to assert corrected semantics, not deleted.
  • PostgreSQL tests skip unless OPENDB_TEST_PG_DSN is set, so CI stays green without a server.

Full changelog: https://github.com/opendb-ai/openDB/blob/main/CHANGELOG.md