Skip to content

v1.15.0 — write-side valid_from stamp

Choose a tag to compare

@goldenwo goldenwo released this 05 Aug 02:09
· 128 commits to main since this release
7129ba1

Temporal query-resolution shipped in v1.14.0 ranking on metadata.valid_from and nothing else — and only 167 of 353 live points (47.3%) carried that field. The rest had createdAt alone, which is bulk-arrival time: 86.5% of them land on just two import days. Ranking on it would order half the corpus by which batch it arrived in, so it can never serve as a fallback.

That deficit was a write-path defect. buildPayload stamped createdAt on every add and valid_from on none, so any direct add whose caller supplied no event time — the fact-extraction path above all — landed permanently ungradeable by the temporal window.

This release closes it, forward-only: no backfill, no migration, no reindex.

What changed

  • The stamp. Every direct, non-rebuild, non-system add now carries an event time, sharing one clock read with createdAt so valid_from === createdAt is byte-exact.
  • isUsableDate — a shared predicate exported from ranking.mjs beside resolveItemDate, deliberately stricter than the reader. The invariant is accept(isUsableDate) ⊆ resolvable(resolveItemDate): everything the writer preserves is readable, never the reverse. Without it, [] and 'yesterday' would be stored present-but-unreadable, and true/1 would become 1970 dates.
  • Rebuild and system-doc paths excluded, so a reindex can only ever preserve an event time, never mint one.
  • valid_from.missing_on_index warn — a doc indexed without an event time is otherwise permanently ungradeable and completely silent. The stamp cannot close that hole; this does.
  • Frontmatter clobber fixed in memory_capture and memory_supersede. Both spread the caller's metadata after the server default, so valid_from: null silently won.
  • valid_from added to R6_PROTECTED_KEYS, closing an unguarded caller write to the ranking date via PUT /v1/memories/:id.
  • S11 smoke gate for the flat→nested projection — the one property no unit test can reach, since the mapping happens inside mem0ai's Memory.search.

Read-path behaviour is unchanged under the default configuration. UM_TEMPORAL_QUERY stays false and this release does not touch it.

⚠ Do not adopt yet if you run UM_TEMPORAL_DECAY=true

applyTemporalDecay leaves an undated point's score untouched. Undated points therefore behave as though their decay factor were permanently 1.0, while every stamped point's factor falls below it as it ages. With no backfill, points written before this release keep that advantage indefinitely.

Stated precisely, because the loose version is alarming and wrong:

This is about the factor, not final rank. Decay multiplies the cosine score, so an undated point does not outrank a higher-scoring stamped one — undated at 0.30 still loses to a stamped 0.90 aged 7 days at a 30-day half-life (0.90 × 0.79 = 0.713). The bias is systematic but score-relative.

Two further precisions: a freshly stamped point has factor ≈ 1.0 and ties, so the disadvantage appears as it ages rather than at once. And the undated cohort is not "the oldest" — its createdAt clusters on recent bulk-import days, and treating that as age is exactly the inference this design forbids.

server/.env.example carries this warning at the flag.

The follow-up that closes this arc

The backfill (#223) is what retires the advisory above. It cannot simply copy createdAt — see the 86.5% figure — so it needs a genuine per-point event-time source, and that design does not exist yet. It is specifiable now but blocked from being built until the current measurement frame closes.

Until then, a decay-enabled operator's correct move is to stay on v1.14.0.

Also in this release

The mem0ai patch and source-hash integrity checks now run on every PR without secrets (#232). They previously lived inside the secret-gated smoke job, so dependabot PRs — which run in a separate secrets scope — presented as green with the entire test suite skipped. #211 (mem0ai 3.1.2) was a live example: the source hash drifts and the version-pinned patch does not apply. Reconciliation tracked in #231.

Verification

  • Suite 2170 tests / 2155 pass / 0 fail / 15 skipped
  • Every guard mutation-verified, and each red control checked for which tests it flips rather than merely that it failed
  • computeFactId byte-identical to the prior release — dedup identity provably unmoved

⏩ Forward-pointer (added 2026-08-10): the advisory above is resolved on main

The undated-point advantage described above is fixed after this release by a read-path policy rather than a backfillapplyTemporalDecay now imputes a fixed factor (exp(-0.25) ≈ 0.78) for a point with no resolvable event time instead of leaving it at 1.0 (PR #236). The backfill this release anticipated was measured out: no surviving source can date more than 22 of the 186 legacy points (11.8%), so #223 closes via the policy, not a migration. Recall safety was measured before and after on a corpus-realistic fixture (perfect retention both sides, doubled runs); the full evidence trail and the production-flip precondition live in #239. The advisory remains accurate for this release — decay-enabled operators should adopt the next release rather than staying on v1.14.0.