Skip to content

fix: route semantic memory eviction to correct KV store using source …#132

Merged
rohitg00 merged 4 commits intorohitg00:mainfrom
Tanmay-008:fix/semantic-memory-eviction
Apr 18, 2026
Merged

fix: route semantic memory eviction to correct KV store using source …#132
rohitg00 merged 4 commits intorohitg00:mainfrom
Tanmay-008:fix/semantic-memory-eviction

Conversation

@Tanmay-008
Copy link
Copy Markdown
Contributor

@Tanmay-008 Tanmay-008 commented Apr 13, 2026

The mem::retention-evict loop was previously hardcoded to delete candidates
only from KV.memories. This caused semantic memories to fail silently and
remain alive in KV.semantic indefinitely, even with sub-threshold scores.

Changes implemented:

  • Added source: 'episodic' | 'semantic' to the RetentionScore interface.
  • Populated the source field during mem::retention-score calculation.
  • Branched the deletion logic in mem::retention-evict to route deletes
    to either KV.memories or KV.semantic based on the candidate's source.
  • Added test cases to seed both memory types and verify successful eviction
    from their respective KV stores when scores drop below the threshold.

Summary by CodeRabbit

  • Bug Fixes

    • Enhanced memory eviction logic to properly validate and identify correct storage locations before deletion. When the source is unclear, the system now probes available storage options to ensure data is deleted from the right location, preventing unintended data loss.
  • Updates

    • Extended type definitions to improve retention score tracking and source identification.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 13, 2026

📝 Walkthrough

Walkthrough

The PR modifies memory eviction routing to safely probe KV namespaces before deletion, treating source identifiers as nullable and only resolving them after confirming backing data exists. It adds a source discriminator field to track whether retained memories are episodic or semantic, and removes "relation_create" from audit operation types.

Changes

Cohort / File(s) Summary
Eviction Routing Safety
src/functions/retention.ts
Modified eviction logic to probe KV.memories when source is unknown, then probe KV.semantic, and only select for deletion when the entry is confirmed to exist; skips candidate if neither namespace contains it. Ensures counters and audit records use resolved source type.
Type Definitions
src/types.ts
Removed "relation_create" from AuditEntry.operation union; added optional source field to RetentionScore to discriminate between episodic and semantic memory sources.

Sequence Diagram

sequenceDiagram
    participant Eviction as Eviction Logic
    participant KVMem as KV.memories
    participant KVSem as KV.semantic
    participant Counter as Counters & Audit

    Eviction->>Eviction: Check candidate.source
    alt source is missing/unknown
        Eviction->>KVMem: Probe for entry
        alt found in memories
            KVMem-->>Eviction: Entry exists
            Eviction->>Eviction: Set resolvedSource = "episodic"
            Eviction->>KVMem: Delete entry
        else not in memories
            KVMem-->>Eviction: Not found
            Eviction->>KVSem: Probe for entry
            alt found in semantic
                KVSem-->>Eviction: Entry exists
                Eviction->>Eviction: Set resolvedSource = "semantic"
                Eviction->>KVSem: Delete entry
            else not in semantic
                KVSem-->>Eviction: Not found
                Eviction->>Eviction: Skip candidate (continue)
            end
        end
    else source is provided
        Eviction->>Eviction: Use source directly
        Eviction->>Eviction: Delete from appropriate namespace
    end
    Eviction->>Counter: Update counters & recordAudit
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 A safer path through memory's store,
We probe before we delete some more,
No blindfolded swings at namespaces unknown—
Each episodic memory finds its home,
Semantic seeds now safely sown! 🌱

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly addresses the main fix: routing semantic memory eviction to the correct KV store using source field, which matches the primary objective of the PR.

✏️ 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.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
src/types.ts (1)

782-782: Prefer making RetentionScore.source required for new writes.

This field now drives eviction routing; keeping it optional reduces compile-time safety and makes future omissions easier. A stricter type plus legacy fallback handling in eviction is safer.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/types.ts` at line 782, Make the RetentionScore.source property required
(change source?: to source:) so new writes must include "episodic" | "semantic";
update any constructors/builders that create RetentionScore to supply a source.
Then update the eviction routing logic (the function/method that inspects
RetentionScore.source, e.g., the eviction handler or routeEvictionByScore) to
treat missing/legacy scores defensively by detecting undefined/null and mapping
them to a sensible default (explicit fallback branch such as "semantic" or a
legacy tag) so existing data still evaporates correctly while new records are
type-safe.
test/retention.test.ts (1)

246-277: Add a legacy retention-score regression case (source missing).

This test covers the new happy path. Please add one case that seeds mem:retention entries without source and verifies semantic IDs still evict from mem:semantic.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/retention.test.ts` around lines 246 - 277, Add a regression test that
seeds a legacy retention entry missing the `source` field and verifies semantic
items still get evicted: in the same test file use the existing helpers
(makeMemory, makeSemanticMemory, mockKV, mockSdk) and registerRetentionFunctions
to insert a mem:retention record without `source` (e.g., directly into the
mockKV store), trigger the existing "mem::retention-score" and then
"mem::retention-evict" flows, and assert that mem:semantic entries are removed
and evicted count increments; refer to registerRetentionFunctions,
mem::retention-score, mem::retention-evict, and the KV keys "mem:retention",
"mem:memories", and "mem:semantic" when locating where to add this case.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/functions/retention.ts`:
- Around line 221-229: The deletion branch for evicted candidates currently
calls kv.delete(...) (for KV.semantic or KV.memories and KV.retentionScores)
without audit records; update both branches in the eviction logic to call
recordAudit(...) for each kv.delete call (include memoryId, the KV namespace key
like KV.semantic/KV.memories/KV.retentionScores, operation type "delete", and
any relevant candidate metadata) immediately before or after each kv.delete to
ensure all state-changing deletes are recorded; make changes around the eviction
block that references candidate.memoryId, candidate.source, KV.semantic,
KV.memories, KV.retentionScores, kv.delete and evicted and reuse the same audit
schema used elsewhere in the codebase when invoking recordAudit.
- Around line 221-229: The eviction branch currently treats any non-"semantic"
candidate as episodic, which lets semantic entries missing candidate.source
survive; update the logic in retention.ts around candidate.source to explicitly
check: if candidate.source === "semantic" delete KV.semantic and
KV.retentionScores for candidate.memoryId; else if candidate.source ===
"episodic" delete KV.memories and KV.retentionScores; else (unknown/undefined)
log a warning and delete both KV.semantic and KV.memories plus
KV.retentionScores for candidate.memoryId to ensure entries without a source are
not misrouted.

---

Nitpick comments:
In `@src/types.ts`:
- Line 782: Make the RetentionScore.source property required (change source?: to
source:) so new writes must include "episodic" | "semantic"; update any
constructors/builders that create RetentionScore to supply a source. Then update
the eviction routing logic (the function/method that inspects
RetentionScore.source, e.g., the eviction handler or routeEvictionByScore) to
treat missing/legacy scores defensively by detecting undefined/null and mapping
them to a sensible default (explicit fallback branch such as "semantic" or a
legacy tag) so existing data still evaporates correctly while new records are
type-safe.

In `@test/retention.test.ts`:
- Around line 246-277: Add a regression test that seeds a legacy retention entry
missing the `source` field and verifies semantic items still get evicted: in the
same test file use the existing helpers (makeMemory, makeSemanticMemory, mockKV,
mockSdk) and registerRetentionFunctions to insert a mem:retention record without
`source` (e.g., directly into the mockKV store), trigger the existing
"mem::retention-score" and then "mem::retention-evict" flows, and assert that
mem:semantic entries are removed and evicted count increments; refer to
registerRetentionFunctions, mem::retention-score, mem::retention-evict, and the
KV keys "mem:retention", "mem:memories", and "mem:semantic" when locating where
to add this case.
🪄 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: d03285df-a47d-466e-b6d8-a427b919b67d

📥 Commits

Reviewing files that changed from the base of the PR and between c1a63b0 and 3445314.

📒 Files selected for processing (3)
  • src/functions/retention.ts
  • src/types.ts
  • test/retention.test.ts

Comment thread src/functions/retention.ts
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
src/functions/retention.ts (1)

244-248: ⚠️ Potential issue | 🟠 Major

Audit the retention-score delete too.

Line 248 still mutates state without an audit entry. That means the fallback path can remove KV.retentionScores with no trace even though this function now audits the scoped memory delete.

Suggested fix
           if (deletedScope) {
             await recordAudit(kv, "delete", "mem::retention-evict", [candidate.memoryId], { scope: deletedScope });
           }
 
+          await recordAudit(
+            kv,
+            "delete",
+            "mem::retention-evict",
+            [candidate.memoryId],
+            { scope: KV.retentionScores },
+          );
           await kv.delete(KV.retentionScores, candidate.memoryId);
           evicted++;

As per coding guidelines: Use recordAudit() for all state-changing operations in TypeScript functions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/functions/retention.ts` around lines 244 - 248, The code deletes
KV.retentionScores via kv.delete(KV.retentionScores, candidate.memoryId) without
an audit entry; add a corresponding recordAudit call before or after this delete
(similar to the scoped delete) so that the deletion of retention scores is
recorded — reference the existing recordAudit function and use the same
action/type pattern (e.g., "delete", "mem::retention-evict") and include
candidate.memoryId and any relevant metadata (scope or null) when recording this
operation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/functions/retention.ts`:
- Around line 230-241: The current use of .catch(() => null) on kv.get calls
(for KV.memories and KV.semantic using candidate.memoryId) treats transient
storage errors as "not found" and can cause deletion of the retention score and
incrementing evicted without confirming existence; change this to explicitly
distinguish NotFound vs other errors: call kv.get(...) without swallowing errors
or wrap each call in a try/catch that rethrows or returns an error sentinel for
non-NotFound errors, only proceed to kv.delete(...) and set deletedScope
("episodic"/"semantic") when the get successfully returns a record, and ensure
evicted is incremented only after a confirmed delete; if a storage error occurs,
surface or log the error and skip modifying retention/evicted so the failure
isn't masked.

---

Duplicate comments:
In `@src/functions/retention.ts`:
- Around line 244-248: The code deletes KV.retentionScores via
kv.delete(KV.retentionScores, candidate.memoryId) without an audit entry; add a
corresponding recordAudit call before or after this delete (similar to the
scoped delete) so that the deletion of retention scores is recorded — reference
the existing recordAudit function and use the same action/type pattern (e.g.,
"delete", "mem::retention-evict") and include candidate.memoryId and any
relevant metadata (scope or null) when recording this operation.
🪄 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: 82bf8321-7aba-43f2-b6d7-5b1ae7ab3de6

📥 Commits

Reviewing files that changed from the base of the PR and between 3445314 and a4f1676.

📒 Files selected for processing (1)
  • src/functions/retention.ts

Comment thread src/functions/retention.ts
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
src/functions/retention.ts (1)

244-249: ⚠️ Potential issue | 🟠 Major

Audit is still missing for retention-score deletion.

Line 248 mutates state (KV.retentionScores delete) without a corresponding recordAudit() entry. Please audit this delete too, including the fallback/orphan path where deletedScope is null.
As per coding guidelines: Use recordAudit() for all state-changing operations in TypeScript functions.

Suggested patch
-          if (deletedScope) {
-            await recordAudit(kv, "delete", "mem::retention-evict", [candidate.memoryId], { scope: deletedScope });
-          }
-
-          await kv.delete(KV.retentionScores, candidate.memoryId);
+          if (deletedScope) {
+            await recordAudit(
+              kv,
+              "delete",
+              "mem::retention-evict",
+              [candidate.memoryId],
+              { scope: deletedScope },
+            );
+          }
+
+          await recordAudit(
+            kv,
+            "delete",
+            "mem::retention-evict",
+            [candidate.memoryId],
+            { scope: "retentionScores", memoryScope: deletedScope ?? "unknown" },
+          );
+          await kv.delete(KV.retentionScores, candidate.memoryId);
           evicted++;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/functions/retention.ts` around lines 244 - 249, The deletion of the
retention score (kv.delete(KV.retentionScores, candidate.memoryId)) is not being
audited; add a recordAudit() call for that state change and ensure it runs
regardless of whether deletedScope is non-null or null (use deletedScope or a
clear fallback like "orphan" in the audit metadata). Locate the block around
recordAudit(..., "mem::retention-evict", ...) and KV.retentionScores deletion in
retention.ts and insert a call such as recordAudit(kv, "delete",
"kv::retentionScores", [candidate.memoryId], { scope: deletedScope ??
nullOrphanValue }) so every retention-score deletion is recorded; keep audit
ordering consistent with other audits in the function.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/functions/retention.ts`:
- Around line 244-249: The deletion of the retention score
(kv.delete(KV.retentionScores, candidate.memoryId)) is not being audited; add a
recordAudit() call for that state change and ensure it runs regardless of
whether deletedScope is non-null or null (use deletedScope or a clear fallback
like "orphan" in the audit metadata). Locate the block around recordAudit(...,
"mem::retention-evict", ...) and KV.retentionScores deletion in retention.ts and
insert a call such as recordAudit(kv, "delete", "kv::retentionScores",
[candidate.memoryId], { scope: deletedScope ?? nullOrphanValue }) so every
retention-score deletion is recorded; keep audit ordering consistent with other
audits in the function.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ad15d994-9969-4d60-9f3d-69b4e5a24932

📥 Commits

Reviewing files that changed from the base of the PR and between a4f1676 and 0166149.

📒 Files selected for processing (1)
  • src/functions/retention.ts

@rohitg00
Copy link
Copy Markdown
Owner

Is this related to some issue?

Resolve conflicts introduced by subsequent main commits touching the
same retention paths:

- types.ts: union the AuditEntry operation set — keep main's superset
  (adds relation_create/relation_update/retention_score/skill_extract/
  core_add/core_remove/auto_page). Drop HEAD's duplicate source key
  on RetentionScore (already present).
- functions/retention.ts: keep main's cleaner score assembly that
  uses pre-computed temporalDecay / reinforcementBoost / logs-based
  lastAccessed and accessCount. Keep main's evictedIds /
  evictedEpisodic / evictedSemantic counters. Adopt main's batched
  audit emission (single record per invocation with reason) from
  after the loop — HEAD's per-candidate audit emission fails the
  'single batched audit record' test and would also flood the audit
  log during normal eviction sweeps. Preserve HEAD's null-guarded
  continue when neither the episodic nor the semantic store has the
  candidate, so legacy rows with a missing source field still evict
  cleanly without a spurious delete against the wrong scope.
- test/retention.test.ts: drop HEAD's smaller test in favor of
  main's more comprehensive suite (scores tag rows with source /
  evicts from mem:semantic / batched audit on success / no audit
  when evicted=0 / score emits per-rescore audit / probes namespaces
  for legacy semantic rows / routes legacy missing-source episodic
  rows to mem:memories). Main's suite is a strict superset of HEAD's
  original assertion.
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/types.ts (1)

796-810: ⚠️ Potential issue | 🟠 Major

Duplicate source field in RetentionScore.

The source?: "episodic" | "semantic" property is declared twice — once at line 802 and again at line 809. TypeScript will accept this (identical signatures), but it's clearly unintended and will confuse readers/tooling. Drop the second declaration.

🔧 Proposed fix
   lastAccessed: string;
   accessCount: number;
-  source?: "episodic" | "semantic";
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/types.ts` around lines 796 - 810, The RetentionScore interface declares
the optional property source twice; remove the duplicate declaration so
RetentionScore only has a single source?: "episodic" | "semantic" entry. Locate
the RetentionScore interface in src/types.ts and delete the redundant source
line (the second occurrence after accessCount) so the interface contains one
definitive source field and the existing comment above it remains accurate.
🧹 Nitpick comments (1)
src/types.ts (1)

798-801: Consider trimming the inline comment.

The rationale about pre-0.8.10 backward-compat is useful, but the first two lines describe WHAT the field is (already evident from the name/type). Per coding guidelines, prefer clear naming over WHAT-comments; keep only the backward-compat/"unknown" note that conveys non-obvious WHY.

As per coding guidelines: "Avoid code comments explaining WHAT — use clear naming instead".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/types.ts` around lines 798 - 801, Trim the three-line inline comment
above the KV scope field to remove the WHAT-description and leave only the
backward-compatibility note: keep the sentence about pre-0.8.10 rows and that
callers must treat `undefined` as "unknown" and probe both scopes (referencing
`#124`), and delete the first two lines that restate the field's purpose; this
comment sits with the field used by mem::retention-evict that routes to
KV.memories or KV.semantic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/types.ts`:
- Around line 796-810: The RetentionScore interface declares the optional
property source twice; remove the duplicate declaration so RetentionScore only
has a single source?: "episodic" | "semantic" entry. Locate the RetentionScore
interface in src/types.ts and delete the redundant source line (the second
occurrence after accessCount) so the interface contains one definitive source
field and the existing comment above it remains accurate.

---

Nitpick comments:
In `@src/types.ts`:
- Around line 798-801: Trim the three-line inline comment above the KV scope
field to remove the WHAT-description and leave only the backward-compatibility
note: keep the sentence about pre-0.8.10 rows and that callers must treat
`undefined` as "unknown" and probe both scopes (referencing `#124`), and delete
the first two lines that restate the field's purpose; this comment sits with the
field used by mem::retention-evict that routes to KV.memories or KV.semantic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 05a9c404-aafe-4e06-b721-cb4f91691309

📥 Commits

Reviewing files that changed from the base of the PR and between 0166149 and 8207502.

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

@rohitg00 rohitg00 merged commit 78aefce into rohitg00:main Apr 18, 2026
3 checks passed
rohitg00 added a commit that referenced this pull request Apr 18, 2026
Bump version + ship CHANGELOG covering everything that merged since
v0.8.13:

- #118 security advisory drafts for v0.8.2 CVEs
- #132 semantic eviction routing + batched retention audit
- #157 iii console docs + vendored screenshots in README
- #160 (#158) health gated on RSS floor
- #161 (#159) standalone MCP proxies to the running server
- #162 (#125) mem::forget audit coverage + policy doc
- #163 (#62) @agentmemory/fs-watcher filesystem connector
- #164 Next.js website (website/ root, ship to Vercel)

Version bumps (8 files):
- package.json / package-lock.json (top + packages[''])
- plugin/.claude-plugin/plugin.json
- packages/mcp/package.json (self + ~0.9.0 dep pin)
- src/version.ts (union extended, assigned 0.9.0)
- src/types.ts (ExportData.version union)
- src/functions/export-import.ts (supportedVersions set)
- test/export-import.test.ts (export assertion)

Tests: 777 passing. Build clean.
@rohitg00 rohitg00 mentioned this pull request Apr 18, 2026
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.

2 participants