Skip to content

feat(search): add temporal decay to recall ranking#928

Open
Srinath279 wants to merge 2 commits into
rohitg00:mainfrom
Srinath279:feat/temporal-decay-recall
Open

feat(search): add temporal decay to recall ranking#928
Srinath279 wants to merge 2 commits into
rohitg00:mainfrom
Srinath279:feat/temporal-decay-recall

Conversation

@Srinath279

@Srinath279 Srinath279 commented Jun 14, 2026

Copy link
Copy Markdown

Recall fuses BM25 + vector + graph into a single RRF relevance score with no notion of time, so an equally-relevant note from this morning and one from a year ago rank identically. This adds an opt-in temporal decay reweight that blends an exponential recency factor (and optional importance term) into the relevance score, so fresh and reinforced memories surface ahead of equally-relevant stale ones.

Design:

  • Exponential decay parameterized by a configurable HALF-LIFE (days), the interpretable forgetting-curve knob.
  • Multiplicative reweight of the (small, unnormalized) RRF score rather than additive, so relevance stays the dominant signal.
  • A floor on the multiplier so decay demotes but never erases an old-but-highly-relevant hit.
  • "Use it or lose it": effective age is measured from the later of creation or last access (reuses the existing AccessLog), so recall refreshes recency.
  • Importance slows decay, mirroring the Generative Agents importance term.

OFF by default (AGENTMEMORY_TEMPORAL_DECAY=true), matching the project's opt-in posture for changes that alter recall ordering. When disabled the reweight short-circuits with zero added cost.

Adds src/functions/temporal-decay.ts (pure, fully unit-tested), config getters + safeParseFloat, HybridSearch integration, and .env.example docs.

Summary by CodeRabbit

  • New Features

    • Added temporal decay to memory recall scoring, re-ranking results using recency in combination with importance.
  • Chores

    • Introduced configuration controls to enable/disable temporal decay and tune half-life, recency/importance weighting, and a minimum decay floor (documented in the example environment file).
    • Added comprehensive automated tests covering recency factor, decay bounds, and timestamp handling.

Recall fuses BM25 + vector + graph into a single RRF relevance score
with no notion of time, so an equally-relevant note from this morning
and one from a year ago rank identically. This adds an opt-in temporal
decay reweight that blends an exponential recency factor (and optional
importance term) into the relevance score, so fresh and reinforced
memories surface ahead of equally-relevant stale ones.

Design:
- Exponential decay parameterized by a configurable HALF-LIFE (days),
  the interpretable forgetting-curve knob.
- Multiplicative reweight of the (small, unnormalized) RRF score rather
  than additive, so relevance stays the dominant signal.
- A floor on the multiplier so decay demotes but never erases an
  old-but-highly-relevant hit.
- "Use it or lose it": effective age is measured from the later of
  creation or last access (reuses the existing AccessLog), so recall
  refreshes recency.
- Importance slows decay, mirroring the Generative Agents importance term.

OFF by default (AGENTMEMORY_TEMPORAL_DECAY=true), matching the project's
opt-in posture for changes that alter recall ordering. When disabled the
reweight short-circuits with zero added cost.

Adds src/functions/temporal-decay.ts (pure, fully unit-tested), config
getters + safeParseFloat, HybridSearch integration, and .env.example docs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Srinath <srinath12519@gmail.com>
@vercel

vercel Bot commented Jun 14, 2026

Copy link
Copy Markdown

@Srinath279 is attempting to deploy a commit to the rohitg00's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: adf17829-f345-4633-9674-b66a28a6dc60

📥 Commits

Reviewing files that changed from the base of the PR and between f0fd2a9 and 62e52ad.

📒 Files selected for processing (1)
  • src/config.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/config.ts

📝 Walkthrough

Walkthrough

A new temporal decay system reweights hybrid search result scores based on memory age, importance, and last-access time. A src/functions/temporal-decay.ts module implements exponential recency and importance-blended score multipliers with a configurable floor. Five new config accessors in src/config.ts read AGENTMEMORY_TEMPORAL_DECAY* environment variables. HybridSearch gains an applyDecayReweight method that applies decay reweighting before reranking. src/index.ts wires the full config into startup. A comprehensive Vitest suite covers all utilities.

Changes

Temporal Decay Memory Recall

Layer / File(s) Summary
Decay params interface and env-var config
.env.example, src/config.ts, src/functions/temporal-decay.ts
TemporalDecayParams interface and DEFAULT_TEMPORAL_DECAY defaults defined with enable/disable switch, half-life days, recency/importance blend weights, and floor. safeParseFloat utility parses trimmed env strings into finite numbers with fallback. Five exported config accessors read AGENTMEMORY_TEMPORAL_DECAY* env vars. .env.example documents all knobs as commented-off defaults.
Core decay math utilities
src/functions/temporal-decay.ts
Implements clamp01 (value sanitization), recencyFactor (exponential half-life decay from age in ms), normalizeParams (proportional weight rescaling), decayMultiplier (floor-bounded blended multiplier), applyTemporalDecay (score reweighter, no-op when disabled), and effectiveTimestampMs (computes later-of observation/last-access time).
Comprehensive test suite
test/temporal-decay.test.ts
Full Vitest coverage validates recencyFactor boundary values and monotonic behavior, decayMultiplier floor/upper bounds and weight normalization, applyTemporalDecay disabled pass-through and relevance scaling, and effectiveTimestampMs timestamp selection and safe invalid-input handling.
HybridSearch decay reweighting
src/state/hybrid-search.ts
HybridSearch constructor extends with decayParams field. New applyDecayReweight method fetches per-result KV access logs, computes effective timestamps, applies decay to each result's score, and re-sorts. tripleStreamSearch applies decay reweighting before reranking with decayed-results fallback on rerank error.
Startup config wiring and boot logging
src/index.ts
Imports temporal-decay config accessors, constructs temporalDecay object at startup from config values, passes it to HybridSearch constructor, and adds boot log line reporting temporal decay status and configured half-life.

Sequence Diagram

sequenceDiagram
  participant Client
  participant HybridSearch
  participant applyDecayReweight
  participant KV
  participant applyTemporalDecay
  participant Reranker

  Client->>HybridSearch: tripleStreamSearch(query)
  HybridSearch->>applyDecayReweight: enriched results[]
  applyDecayReweight->>KV: getAccessLog per observationId
  KV-->>applyDecayReweight: AccessLog with lastAccessIso
  applyDecayReweight->>applyTemporalDecay: combinedScore, effectiveTimestampMs, importance
  applyTemporalDecay-->>applyDecayReweight: decayed score (relevance × multiplier)
  applyDecayReweight-->>HybridSearch: re-sorted decayed results[]
  HybridSearch->>Reranker: decayed results window
  Reranker-->>HybridSearch: reranked results or decayed truncated fallback
  HybridSearch-->>Client: final scored results
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~28 minutes

Poem

🐇 Memories age like clover in the sun,
Exponential decay—half-life's artful fun.
Fresh thoughts leap high, old wisdom gently wanes,
Yet never lost, just ranked by time's refrains.
The rabbit scores what matters most today! 🌟

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 58.82% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main objective: adding temporal decay to recall ranking, which is the core feature introduced across the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/state/hybrid-search.ts (1)

269-275: Consider requesting a batched read API for StateKV to reduce access-log lookup overhead in decay reweight.

The applyDecayReweight method issues one kv.get per result via Promise.all, but StateKV has no batch/multi-get primitive—only individual get, set, update, delete, and list operations. While Promise.all parallelizes these calls, a true batch API would reduce per-query RPC overhead, especially for expansion-heavy searches with many results. If access patterns justify it, consider adding a getMany or batch method to StateKV.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/state/hybrid-search.ts` around lines 269 - 275, The applyDecayReweight
method currently issues individual kv.get calls for each result via Promise.all,
which creates unnecessary RPC overhead. Add a batched read method (such as
getMany or batch) to the StateKV class to support fetching multiple access logs
in a single operation, then refactor the code that maps over results and calls
kv.get for each r.observation.id to use this new batched method instead, passing
all the IDs at once.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/config.ts`:
- Around line 19-23: The safeParseFloat function uses parseFloat which is too
lenient and accepts strings with trailing non-numeric characters like "0.2oops".
Replace the parseFloat call with Number(value) to enforce strict numeric parsing
that rejects partial matches. Number() returns NaN for invalid input, which
Number.isFinite() will properly catch and trigger the fallback value.

---

Nitpick comments:
In `@src/state/hybrid-search.ts`:
- Around line 269-275: The applyDecayReweight method currently issues individual
kv.get calls for each result via Promise.all, which creates unnecessary RPC
overhead. Add a batched read method (such as getMany or batch) to the StateKV
class to support fetching multiple access logs in a single operation, then
refactor the code that maps over results and calls kv.get for each
r.observation.id to use this new batched method instead, passing all the IDs at
once.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8f9ad511-3312-4398-9d06-448c65b79109

📥 Commits

Reviewing files that changed from the base of the PR and between f6f9e3c and f0fd2a9.

📒 Files selected for processing (6)
  • .env.example
  • src/config.ts
  • src/functions/temporal-decay.ts
  • src/index.ts
  • src/state/hybrid-search.ts
  • test/temporal-decay.test.ts

Comment thread src/config.ts
parseFloat accepts trailing non-numeric text ("0.2oops" -> 0.2), so a
malformed env value was silently honored instead of falling back to the
default. Switch to Number() for strict parsing and guard the
empty-after-trim case (Number("") is 0, not NaN). Addresses CodeRabbit
review feedback.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Srinath <srinath12519@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant