Skip to content

fix: retention scoring (#119) + npx agentmemory-mcp 404 (#120), release 0.8.3#123

Merged
rohitg00 merged 2 commits intomainfrom
fix/retention-access-tracking-119-120
Apr 13, 2026
Merged

fix: retention scoring (#119) + npx agentmemory-mcp 404 (#120), release 0.8.3#123
rohitg00 merged 2 commits intomainfrom
fix/retention-access-tracking-119-120

Conversation

@rohitg00
Copy link
Copy Markdown
Owner

@rohitg00 rohitg00 commented Apr 13, 2026

Closes #119, closes #120. Ships as v0.8.3.

Summary

Two bug fixes reported in the public issue tracker, plus the fallout from a full specialist review.

#119 — retention score was a dead path

mem::retention-score hardcoded accessCount=0 and accessTimestamps=[] for episodic memories and only used a single-sample lastAccessedAt for semantic ones. Reads from mem::search, mem::smart-search, mem::context, mem::timeline, mem::file-context were never recorded, so the time-frequency decay formula never saw any data.

Fix:

  • New src/functions/access-tracker.tsrecordAccess, recordAccessBatch, getAccessLog, deleteAccessLog, normalizeAccessLog, emptyAccessLog. Bounded ring buffer (20 timestamps). Keyed-mutex wrapped RMW so concurrent reads on the same id don't lose increments. Promise.allSettled across ids so one slow lock doesn't block the batch.
  • Every read endpoint now records access fire-and-forget: mem::search, mem::smart-search (compact + expand), mem::context (observation blocks), mem::timeline, mem::file-context, mem::get-related, mem::working-context (archival pages).
  • mem::retention-score pre-fetches the full access log in one kv.list call, normalizes every row defensively, and builds an O(1) lookup Map — no N+1.
  • reinforcementBoost is reported as the raw formula output via an extracted computeReinforcementBoost helper, not the clamped residual.
  • Corrupted sem.lastAccessedAt is NaN-safe via Date.parse + Number.isFinite.
  • Backwards-compat: pre-0.8.3 semantic memories with only the legacy sem.lastAccessedAt still score correctly. The fallback kicks in only when the fresh access log is empty.
  • Leak plugged: six memory-deletion sites (retention-evict, evict, auto-forget, governance-delete, governance-bulk, forget, import replace) now call deleteAccessLog so mem:access doesn't accumulate zombies.
  • Data-loss plugged: mem::export/mem::import and mem::snapshot-create/mem::snapshot-restore round-trip the new namespace. Backup/restore no longer zeroes reinforcement signals.

#120 — npx agentmemory-mcp returned npm registry 404

The README told users to run npx agentmemory-mcp, but that was only a bin entry inside @agentmemory/agentmemory, not a real registry package. npx resolves against the registry, so it 404'd every first-time user.

Fix — both a shim package and a canonical CLI subcommand:

  • New sibling package packages/agentmemory-mcp/ — thin shim depending on @agentmemory/agentmemory (~0.8.3, tilde to block 0.9.x supply-chain drift) that forwards to dist/standalone.mjs via the new exports field on the main package.
  • Canonical npx @agentmemory/agentmemory mcp subcommand on the CLI for users who already have the main package.
  • Removed the dead agentmemory-mcp bin from the main package so the shim owns the name unambiguously.
  • .github/workflows/publish.yml publishes both packages in order, with idempotent guards (skip if version already published) and npm view polling instead of a fixed sleep, so retrying a failed workflow doesn't leave the registry in a half state.
  • All MCP install snippets in README.md, integrations/openclaw/README.md, integrations/hermes/README.md, and packages/agentmemory-mcp/README.md use npx -y agentmemory-mcp now (skips the first-run confirmation prompt).

Review passes

This branch went through the full pipeline in one session: /simplify (three review agents — reuse, quality, efficiency) → /review (five specialists — testing, security, api-contract, maintainability, red-team). Every critical finding was fixed in the same branch before landing:

Source Finding Fix
/simplify efficiency N+1 in retention.ts per-memory getAccessLog calls Batched kv.list(KV.accessLog) + Map lookup
/simplify quality ContextBlock & { sourceIds } structural extension Added sourceIds? to the canonical type
/review critical pass package-lock.json stale at 0.8.2 Regenerated
/review critical pass publish.yml only publishes root package Two-stage publish with polling
api-contract mem::access-record/get registered but never wired to MCP/REST Removed the dead SDK registration
api-contract packages/agentmemory-mcp caret ^0.8.3 allows 0.9.x Pinned to ~0.8.3 + publishConfig
api-contract mem::export/mem::import dropped mem:access namespace Round-trip added
security NaN propagation from corrupt sem.lastAccessedAt Date.parse + isFinite guard
maintainability reinforcementBoost reported clamped residual not raw Extracted computeReinforcementBoost helper
maintainability Integration READMEs + shim README missing -y flag Updated all 9 snippets
red-team Memory-deletion sites leak access log entries deleteAccessLog wired into 6 sites
red-team mem::snapshot-create/restore bypasses mem:access Round-trip added
red-team bin collision on agentmemory-mcp name Removed from main package
red-team Workflow fixed sleep 30 has no recovery on slow propagation npm view polling + idempotent guards
red-team Semantic merge count/timestamp drift Restructured: fresh log XOR legacy fallback
red-team mem::get-related, mem::working-context don't reinforce Wired through recordAccessBatch
red-team retention.ts bulk list path bypasses defensive normalize Extracted normalizeAccessLog helper, used in both paths

Tests

  • 670 / 670 passing (was 665 before this branch)
  • 5 new tests:
    • test/access-tracker.test.ts — 8 tests (was 7) including recordAccessBatch resilience with a failing id
    • test/retention-access.test.ts — 7 tests (was 4) including paired with-vs-without legacy lastAccessedAt, NaN-safe corrupt input, kv.list failure path
    • test/smart-search.test.ts — 7 tests (was 5) including the end-to-end integration test that proves mem::smart-search actually populates mem:access for returned ids (the whole fix for BUG: Retention Score Formula is Broken #119 was unguarded against silent regression before)

Test plan

  • npm test — 670 / 670 passing
  • npm run build — clean
  • import("@agentmemory/agentmemory/dist/standalone.mjs") smoke-tested via npm install ~/agentmemory in a temp dir → resolves through the new exports field and prints the standalone banner
  • Verify the shim publishes correctly on first release by watching the publish.yml run
  • After merge + publish, npx agentmemory-mcp from a clean /tmp directory should boot the MCP stdio server without 404

Reviewer notes

  • Skipped refactors deferred as out-of-scope for a patch release: standalone.ts top-level side effects (should live behind a start() function), mem::context summary blocks not reinforcing (needs SessionSummary schema change), input validation on memoryId in the access tracker (in-process callers are trusted), lock-map exhaustion hardening (not exploitable today).
  • Pre-existing oddity I did NOT touch: src/version.ts VERSION union, src/types.ts ExportData.version union, and src/functions/export-import.ts supportedVersions Set each have slightly different shapes (missing 0.7.7 / 0.7.9 / 0.8.0 depending on which one). Pre-existing drift worth fixing separately.

Summary by CodeRabbit

  • New Features

    • Memory access tracking added to improve retention scoring and export/import of access logs.
    • Published new standalone agentmemory-mcp shim and added an official MCP subcommand.
  • Bug Fixes

    • Fixed 404 for npx agentmemory-mcp.
  • Documentation

    • Updated MCP setup examples to include -y and document canonical mcp invocation.
  • Chores

    • CI publishing updated to publish version-aware packages in order.
  • Tests

    • Added comprehensive tests for access tracking and retention behavior.

…se 0.8.3

Issue #119 — retention score ignored agent-side reads

`mem::retention-score` previously hardcoded `accessCount=0` and
`accessTimestamps=[]` for episodic memories and used only a single-sample
`lastAccessedAt` for semantic memories, so the time-frequency decay
formula was a dead path. All read endpoints now record access events to
a new `mem:access` KV namespace via a keyed-mutex'd access tracker:

  - mem::search, mem::smart-search (compact + expand), mem::context,
    mem::timeline, mem::file-context, mem::get-related, mem::working-context
  - retention.ts pre-fetches the full access log in one kv.list call,
    normalizes every row defensively, and builds an O(1) lookup Map
  - recordAccessBatch uses Promise.allSettled so a slow keyed-lock on one
    id does not block the rest of the batch
  - Bounded ring buffer of the last 20 access timestamps per memory
  - Backwards-compat fallback: pre-0.8.3 semantic memories with only the
    legacy sem.lastAccessedAt still score correctly
  - Corrupt lastAccessedAt values are NaN-safe via Date.parse + isFinite
  - reinforcementBoost is reported as the raw formula output, not the
    clamped residual

Memory deletion sites (retention-evict, evict, auto-forget,
governance-delete, governance-bulk, forget, import replace) now call
deleteAccessLog to prevent mem:access from accumulating zombie entries.
mem::export / mem::import and mem::snapshot-create / mem::snapshot-restore
round-trip the new namespace so backup/restore cycles no longer zero out
reinforcement signals.

Issue #120 — npx agentmemory-mcp returned npm registry 404

The README instructed users to run `npx agentmemory-mcp`, but
`agentmemory-mcp` only existed as a `bin` entry inside
`@agentmemory/agentmemory`, not a real registry package. Two-part fix:

  - New sibling package `packages/agentmemory-mcp/` — a thin shim that
    depends on `@agentmemory/agentmemory` (~0.8.3, tilde to block 0.9.x
    supply-chain drift) and forwards to dist/standalone.mjs via the new
    `exports` field on the main package
  - Canonical `npx @agentmemory/agentmemory mcp` subcommand on the CLI
    for users who already have the main package installed
  - Removed the dead `agentmemory-mcp` bin from the main package so the
    shim owns the name unambiguously
  - publish.yml publishes both packages in order with idempotent
    guards and npm-view registry polling (no more fixed sleep)
  - All MCP install snippets in README + integrations/openclaw + hermes
    + shim README use `npx -y agentmemory-mcp` to skip install prompts

Release 0.8.3

  - 670 tests passing (665 + 11 new: access tracker, retention,
    smart-search integration, corrupt-input, kv.list failure path,
    Promise.allSettled resilience)
  - Version bumped across package.json, src/version.ts, types.ts,
    export-import supportedVersions, plugin.json
  - package-lock.json regenerated
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 13, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f04b583e-fe3b-4411-b9f4-26f0eeb8f080

📥 Commits

Reviewing files that changed from the base of the PR and between 89ebc63 and d1e3829.

📒 Files selected for processing (7)
  • .github/workflows/publish.yml
  • CHANGELOG.md
  • README.md
  • src/functions/access-tracker.ts
  • src/functions/export-import.ts
  • test/access-tracker.test.ts
  • test/retention-access.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • README.md
  • .github/workflows/publish.yml
  • src/functions/export-import.ts

📝 Walkthrough

Walkthrough

Added a new mem:access KV-based access-tracking system and integrated it across read/search/context flows; introduced an agentmemory-mcp shim package and CLI wiring; made publish workflow version-aware with propagation polling; bumped package/plugin/version to 0.8.3 and added export/import/snapshot support for access logs.

Changes

Cohort / File(s) Summary
Version & Package Metadata
package.json, src/version.ts, plugin/.claude-plugin/plugin.json, CHANGELOG.md
Bumped version to 0.8.3, added exports map, updated plugin manifest and changelog entries.
Publish Workflow
.github/workflows/publish.yml
Replaced unconditional npm publish with version-aware checks per-package, conditional publish, and registry-propagation polling (24 attempts × 5s). Applies to both @agentmemory/agentmemory and agentmemory-mcp.
New MCP Shim Package
packages/agentmemory-mcp/package.json, packages/agentmemory-mcp/bin.mjs, packages/agentmemory-mcp/README.md, packages/agentmemory-mcp/LICENSE
Added a standalone agentmemory-mcp package (CLI shim that imports @agentmemory/agentmemory), README, and Apache-2.0 license.
CLI & Docs
src/cli.ts, README.md, integrations/hermes/README.md, integrations/openclaw/README.md
Added mcp CLI command and run handler; updated examples to prefer npx -y @agentmemory/agentmemory mcp or npx -y agentmemory-mcp and prefixed MCP args with -y in integration docs.
KV Schema & Types
src/state/schema.ts, src/types.ts
Added KV.accessLog = "mem:access", new AccessLogExport interface, extended ExportData with accessLogs and added ContextBlock.sourceIds.
Access-Tracker Implementation
src/functions/access-tracker.ts
New module implementing access-log shape, normalization, get/record/delete APIs, keyed mutexing, capped recent timestamps, and batch recording with Promise.allSettled.
Access Recording Integrations
src/functions/search.ts, src/functions/smart-search.ts, src/functions/file-index.ts, src/functions/context.ts, src/functions/relations.ts, src/functions/timeline.ts, src/functions/working-memory.ts
Inserted calls to recordAccessBatch to record accessed memory/observation IDs after building results across search, smart-search, file-index, context assembly, relations, timeline, and working-memory flows.
Access Log Cleanup
src/functions/auto-forget.ts, src/functions/evict.ts, src/functions/remember.ts, src/functions/governance.ts
On memory deletion paths (TTL/evict/forget/governance), added deleteAccessLog calls to remove corresponding mem:access entries.
Retention & Eviction
src/functions/retention.ts
Rewrote retention scoring to incorporate access-log-based reinforcement (computeReinforcementBoost + combined retention), use access logs for accessCount/lastAccessed, maintain backward compatibility with legacy lastAccessedAt, and delete access logs during eviction.
Export/Import & Snapshot
src/functions/export-import.ts, src/functions/snapshot.ts
Extended export/import and snapshot save/restore to include accessLogs, added import strategy handling (replace/skip/merge) and a MAX_ACCESS_LOGS cap, and added "0.8.3" to supported versions.
Tests
test/access-tracker.test.ts, test/retention-access.test.ts, test/smart-search.test.ts, test/export-import.test.ts
Added comprehensive tests for access-tracker (concurrency, normalization, error resilience), retention scoring using access logs, smart-search access recording, and updated export version expectation to 0.8.3.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~55 minutes

Possibly related PRs

Poem

🐰 I nibble logs in mem:access rows,

Timestamps hop where memory grows.
Scores bounce up with every read,
MCP shim hops to do the deed.
Hooray — small paws, big thoughtful seeds.

🚥 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 accurately summarizes the two main bug fixes (#119 retention scoring, #120 npx agentmemory-mcp 404) and the release version, directly reflecting the changeset's primary objectives.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/retention-access-tracking-119-120

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: 9

Caution

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

⚠️ Outside diff range comments (2)
src/functions/export-import.ts (1)

177-254: ⚠️ Potential issue | 🟠 Major

Validate accessLogs before importing them.

accessLogs now bypasses all of the import limits and shape checks that protect the other top-level collections. A corrupted export can send arbitrarily many logs or huge recent arrays, and this code will persist them verbatim. Please cap the collection, require a valid memoryId, and clamp/normalize count and recent before the kv.set() loop.
As per coding guidelines, "TypeScript functions must use sdk.registerFunction() pattern with input validation, state access via kv.get/kv.set/kv.list, and audit recording via recordAudit()".

Also applies to: 526-533

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

In `@src/functions/export-import.ts` around lines 177 - 254, The accessLogs array
in importData must be validated and normalized before any kv.set() persistence:
enforce a MAX_ACCESS_LOGS cap (e.g., 50_000) and reject/trim if exceeded; for
each accessLog entry (accessLogs[i]) require a valid memoryId that exists in
importData.memories (or else skip entry), ensure memoryId is a non-empty string,
normalize/clamp count to a safe non-negative integer (e.g., Math.min(Math.max(0,
parsedCount), MAX_COUNT_PER_LOG)), and normalize recent to an array of strings
with a MAX_RECENT_ITEMS cap (e.g., 100) and per-item length limit (trim or drop
oversized items); also limit total size of recent arrays to avoid huge payloads;
perform these checks in the same function that handles importData (where
importData, MAX_SESSIONS, kv.set loop are referenced) before the kv.set loop,
and follow the sdk.registerFunction() input/state access pattern and call
recordAudit() for the import operation.
src/functions/retention.ts (1)

254-258: ⚠️ Potential issue | 🟠 Major

Semantic memories are never actually evicted here.

mem::retention-score creates RetentionScore entries for both regular memories and semantic memories (Lines 120-188), but this loop always deletes from KV.memories. For semantic candidates, that removes only the score/access log; the row in KV.semantic survives and will be scored again on the next run. Please carry the source scope/type into the score entry, or otherwise branch the delete target before reporting success.

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

In `@src/functions/retention.ts` around lines 254 - 258, The retention loop that
iterates over candidates currently always deletes from KV.memories (in the for
loop handling candidates and calling kv.delete(KV.memories, candidate.memoryId),
kv.delete(KV.retentionScores, ...), and deleteAccessLog) which removes only
scores/access logs for semantic entries while leaving KV.semantic rows; fix this
by making the RetentionScore entries include the original source/scope/type when
created (see mem::retention-score and the RetentionScore construction) or by
branching in the deletion loop using that stored type: if candidate.source ===
"semantic" (or the stored scope/type field) delete from KV.semantic instead of
KV.memories, then remove the retentionScore and access log and only report
success after the correct storage row is deleted. Ensure you update the
RetentionScore shape and code paths that write/read it so the delete-branch has
the needed type information.
🧹 Nitpick comments (5)
src/functions/auto-forget.ts (1)

50-51: Run independent delete operations in parallel.

The memory delete and access-log delete are independent; execute them with Promise.all to reduce latency in the cleanup loop.

♻️ Suggested refactor
-              await kv.delete(KV.memories, mem.id);
-              await deleteAccessLog(kv, mem.id);
+              await Promise.all([
+                kv.delete(KV.memories, mem.id),
+                deleteAccessLog(kv, mem.id),
+              ]);
As per coding guidelines, perform parallel operations where possible using `Promise.all` for independent KV writes/reads in TypeScript.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/functions/auto-forget.ts` around lines 50 - 51, The two independent
deletes—kv.delete(KV.memories, mem.id) and deleteAccessLog(kv, mem.id) inside
the auto-forget cleanup loop—should be executed in parallel to reduce latency;
replace the sequential awaits with a single await Promise.all([...]) that runs
both operations for the given mem.id so both complete concurrently while
preserving error propagation.
src/functions/remember.ts (1)

127-128: Parallelize memory and access-log deletion.

These operations are independent and can be executed concurrently.

♻️ Suggested refactor
-        await kv.delete(KV.memories, data.memoryId);
-        await deleteAccessLog(kv, data.memoryId);
+        await Promise.all([
+          kv.delete(KV.memories, data.memoryId),
+          deleteAccessLog(kv, data.memoryId),
+        ]);
As per coding guidelines, perform parallel operations where possible using `Promise.all` for independent KV writes/reads in TypeScript.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/functions/remember.ts` around lines 127 - 128, The two independent
deletions (await kv.delete(KV.memories, data.memoryId) and await
deleteAccessLog(kv, data.memoryId)) should be run in parallel; replace the
sequential awaits by invoking both operations and awaiting Promise.all([...]) so
both delete calls execute concurrently (reference the kv.delete call,
deleteAccessLog function, KV.memories and data.memoryId).
packages/agentmemory-mcp/bin.mjs (1)

2-10: Optional: include direct fallback command in failure guidance.

This would make recovery clearer when deep import resolution fails.

Suggested tweak
 import("@agentmemory/agentmemory/dist/standalone.mjs").catch((err) => {
   console.error(
     "[agentmemory-mcp] Failed to load standalone entrypoint from `@agentmemory/agentmemory`.",
   );
   console.error(
     "[agentmemory-mcp] Try installing manually: npm i -g `@agentmemory/agentmemory`",
   );
+  console.error(
+    "[agentmemory-mcp] Or run directly: npx `@agentmemory/agentmemory` mcp",
+  );
   console.error(err instanceof Error ? err.stack || err.message : String(err));
   process.exit(1);
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/agentmemory-mcp/bin.mjs` around lines 2 - 10, The catch handler for
the dynamic import (the .catch callback handling
import("@agentmemory/agentmemory/dist/standalone.mjs")) currently logs a generic
install suggestion; update that error path to append a clear, copy‑paste
fallback command to the console.error output (e.g., an explicit "npm i -g
`@agentmemory/agentmemory`" or equivalent) so users can recover quickly, and
ensure the existing err output (err.stack || err.message) and the final
process.exit(1) remain unchanged; modify the messages printed by console.error
in that catch block to include the direct fallback command text.
test/access-tracker.test.ts (1)

30-142: Add direct coverage for deleteAccessLog() and normalizeAccessLog().

This suite locks in the write path, but the PR also depends on stale-log cleanup and defensive normalization. A focused unit test for each helper would catch the regressions most likely to break retention and backup round-trips.

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

In `@test/access-tracker.test.ts` around lines 30 - 142, Add unit tests that
directly exercise deleteAccessLog() and normalizeAccessLog(): write a test that
seeds the mockKV with an access log entry, calls deleteAccessLog(kv, memoryId)
and asserts the entry is removed (and that other keys remain), and write tests
for normalizeAccessLog() feeding malformed/partial log objects (missing count,
lastAt, recent; wrong types; extra entries) and asserting it returns a
well-formed AccessLog with bounded recent length and ISO lastAt; use the same
import pattern as the other tests to import these functions from
"../src/functions/access-tracker.js" and the existing mockKV helper to set up
and inspect kv.store.
test/retention-access.test.ts (1)

98-269: Add a malformed mem:access fixture case.

The new scoring path normalizes raw access-log records before using them, but this suite only covers corrupted SemanticMemory.lastAccessedAt. Please add a case with bad count/recent values in mem:access so that defensive normalization stays covered.

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

In `@test/retention-access.test.ts` around lines 98 - 269, Add a new test in
test/retention-access.test.ts that injects a malformed mem:access record into
the mock KV and asserts retention scoring normalizes it: use mockSdk() and
mockKV(...) as in other tests, call registerRetentionFunctions(sdk, kv), then
insert a bad access entry into the kv for scope "mem:access" (e.g., invalid
count/recent fields like strings, nulls, negatives or NaN) rather than using
recordAccess, trigger the "mem::retention-score" via sdk.trigger, and assert the
resulting score entry for that memory has a finite numeric reinforcementBoost
and accessCount is treated defensively (e.g., coerced to 0 or a sane number) to
cover the normalization path.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/publish.yml:
- Around line 50-58: The publish step for the "Publish agentmemory-mcp shim"
currently publishes but doesn't wait for registry propagation; add the same
polling/verification loop used for the `@agentmemory/agentmemory` step: after npm
publish in the agentmemory-mcp step, repeatedly run npm view
"agentmemory-mcp@$SHIM_VERSION" version with a sleep/retry loop until it returns
successfully (or times out), so that consumers like npx agentmemory-mcp no
longer see transient 404s; use the existing SHIM_VERSION variable and the
package name "agentmemory-mcp" to locate where to add this check.

In `@CHANGELOG.md`:
- Line 7: Add a reference-style link definition for the new release token
[0.8.3] so the heading resolves like earlier entries; locate the pattern used
for other release link targets (e.g., the existing [0.8.2] / footer definitions)
and append a matching definition for [0.8.3] with the appropriate URL or compare
link and date information in the changelog footer.

In `@README.md`:
- Around line 589-594: Update the two example invocations so they include the
automatic-yes flag to avoid the install prompt: change "npx
`@agentmemory/agentmemory` mcp" to "npx -y `@agentmemory/agentmemory` mcp" and "npx
agentmemory-mcp" to "npx -y agentmemory-mcp" in the README snippet so the
standalone npx examples behave like the rest of the document.

In `@src/functions/access-tracker.ts`:
- Around line 86-93: deleteAccessLog races with recordAccess because it deletes
mem:access/<id> outside the keyed lock; fix by performing the delete inside the
same withKeyedLock used by recordAccess. In deleteAccessLog (and where
KV.accessLog is referenced), call withKeyedLock(memoryId, async () => { await
kv.delete(KV.accessLog, memoryId); }) (keeping the early return if !memoryId)
and preserve error handling (log/throw as appropriate) so deletes and
recordAccess updates are serialized for the same memoryId.
- Around line 19-29: normalizeAccessLog currently allows negative/fractional
counts and arbitrarily long recent arrays; update it so count is coerced to an
integer, clamped to the 0..20 range, and recent is filtered to only finite
numbers and truncated to the last 20 entries (preserving order), then ensure the
returned count is at least recent.length and at most 20 (e.g., count =
Math.max(truncatedRecent.length, Math.min(20, Math.floor(parsedCount)))) — make
these changes inside normalizeAccessLog to reapply the 20-entry/access-count
invariants.

In `@src/functions/auto-forget.ts`:
- Around line 49-52: The TTL-driven deletion branch in auto-forget.ts deletes
persistent state (await kv.delete(KV.memories, mem.id) and await
deleteAccessLog(kv, mem.id)) without recording an audit; add a call to
recordAudit(...) after successful deletions (only when dryRun is false) that
records the deletion event, the resource id (mem.id), actor/context info from
the current request/kv context, and a descriptive reason (e.g., "TTL
expiration"); ensure recordAudit is awaited and placed so it runs only when both
delete and deleteAccessLog succeed to preserve audit consistency.

In `@src/functions/evict.ts`:
- Around line 139-142: The access-log cleanup should only run if the memory
deletion succeeds and its errors must be caught; update both places where you
call kv.delete(KV.memories, mem.id) followed by deleteAccessLog(kv, mem.id) (the
blocks guarded by the dryRun check) to first perform the delete and confirm
success (e.g., await and check result or catch delete error), and only then call
deleteAccessLog inside its own try/catch so any deleteAccessLog failure is
swallowed/logged and does not abort eviction; ensure you apply this change to
both occurrences referencing kv.delete(KV.memories, mem.id) and
deleteAccessLog(kv, mem.id).

In `@src/functions/remember.ts`:
- Around line 126-129: In the mem::forget branch where you call await
kv.delete(KV.memories, data.memoryId) and await deleteAccessLog(kv,
data.memoryId), add a call to recordAudit(...) to emit an audit record for this
structural deletion; invoke recordAudit with the same kv instance, an action
name like "memory.forget" (or "forget"), and include the memoryId (and any
relevant actor/context) so the deletion is recorded; update the block containing
data.memoryId in src/functions/remember.ts (the if (data.memoryId) branch) to
call recordAudit before or after the deletes and keep incrementing deleted.

In `@src/functions/snapshot.ts`:
- Around line 55-57: The snapshot code silently swallows failures when reading
access logs (the kv.list<AccessLogExport>(KV.accessLog).catch(() => [] as
AccessLogExport[])) causing snapshots to succeed with an empty accessLogs array;
change this to fail-fast and surface the error instead of replacing with []:
remove the silent .catch or replace it with a try/catch that rethrows a wrapped
Error (including KV.accessLog and context) or returns an explicit
partial-snapshot error result so snapshot creation fails (or returns an error
state) when kv.list for KV.accessLog fails; locate the accessLogs variable and
the kv.list call in snapshot creation to implement this.

---

Outside diff comments:
In `@src/functions/export-import.ts`:
- Around line 177-254: The accessLogs array in importData must be validated and
normalized before any kv.set() persistence: enforce a MAX_ACCESS_LOGS cap (e.g.,
50_000) and reject/trim if exceeded; for each accessLog entry (accessLogs[i])
require a valid memoryId that exists in importData.memories (or else skip
entry), ensure memoryId is a non-empty string, normalize/clamp count to a safe
non-negative integer (e.g., Math.min(Math.max(0, parsedCount),
MAX_COUNT_PER_LOG)), and normalize recent to an array of strings with a
MAX_RECENT_ITEMS cap (e.g., 100) and per-item length limit (trim or drop
oversized items); also limit total size of recent arrays to avoid huge payloads;
perform these checks in the same function that handles importData (where
importData, MAX_SESSIONS, kv.set loop are referenced) before the kv.set loop,
and follow the sdk.registerFunction() input/state access pattern and call
recordAudit() for the import operation.

In `@src/functions/retention.ts`:
- Around line 254-258: The retention loop that iterates over candidates
currently always deletes from KV.memories (in the for loop handling candidates
and calling kv.delete(KV.memories, candidate.memoryId),
kv.delete(KV.retentionScores, ...), and deleteAccessLog) which removes only
scores/access logs for semantic entries while leaving KV.semantic rows; fix this
by making the RetentionScore entries include the original source/scope/type when
created (see mem::retention-score and the RetentionScore construction) or by
branching in the deletion loop using that stored type: if candidate.source ===
"semantic" (or the stored scope/type field) delete from KV.semantic instead of
KV.memories, then remove the retentionScore and access log and only report
success after the correct storage row is deleted. Ensure you update the
RetentionScore shape and code paths that write/read it so the delete-branch has
the needed type information.

---

Nitpick comments:
In `@packages/agentmemory-mcp/bin.mjs`:
- Around line 2-10: The catch handler for the dynamic import (the .catch
callback handling import("@agentmemory/agentmemory/dist/standalone.mjs"))
currently logs a generic install suggestion; update that error path to append a
clear, copy‑paste fallback command to the console.error output (e.g., an
explicit "npm i -g `@agentmemory/agentmemory`" or equivalent) so users can recover
quickly, and ensure the existing err output (err.stack || err.message) and the
final process.exit(1) remain unchanged; modify the messages printed by
console.error in that catch block to include the direct fallback command text.

In `@src/functions/auto-forget.ts`:
- Around line 50-51: The two independent deletes—kv.delete(KV.memories, mem.id)
and deleteAccessLog(kv, mem.id) inside the auto-forget cleanup loop—should be
executed in parallel to reduce latency; replace the sequential awaits with a
single await Promise.all([...]) that runs both operations for the given mem.id
so both complete concurrently while preserving error propagation.

In `@src/functions/remember.ts`:
- Around line 127-128: The two independent deletions (await
kv.delete(KV.memories, data.memoryId) and await deleteAccessLog(kv,
data.memoryId)) should be run in parallel; replace the sequential awaits by
invoking both operations and awaiting Promise.all([...]) so both delete calls
execute concurrently (reference the kv.delete call, deleteAccessLog function,
KV.memories and data.memoryId).

In `@test/access-tracker.test.ts`:
- Around line 30-142: Add unit tests that directly exercise deleteAccessLog()
and normalizeAccessLog(): write a test that seeds the mockKV with an access log
entry, calls deleteAccessLog(kv, memoryId) and asserts the entry is removed (and
that other keys remain), and write tests for normalizeAccessLog() feeding
malformed/partial log objects (missing count, lastAt, recent; wrong types; extra
entries) and asserting it returns a well-formed AccessLog with bounded recent
length and ISO lastAt; use the same import pattern as the other tests to import
these functions from "../src/functions/access-tracker.js" and the existing
mockKV helper to set up and inspect kv.store.

In `@test/retention-access.test.ts`:
- Around line 98-269: Add a new test in test/retention-access.test.ts that
injects a malformed mem:access record into the mock KV and asserts retention
scoring normalizes it: use mockSdk() and mockKV(...) as in other tests, call
registerRetentionFunctions(sdk, kv), then insert a bad access entry into the kv
for scope "mem:access" (e.g., invalid count/recent fields like strings, nulls,
negatives or NaN) rather than using recordAccess, trigger the
"mem::retention-score" via sdk.trigger, and assert the resulting score entry for
that memory has a finite numeric reinforcementBoost and accessCount is treated
defensively (e.g., coerced to 0 or a sane number) to cover the normalization
path.
🪄 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: c5bbc242-6d39-489a-8fa8-b556505c0399

📥 Commits

Reviewing files that changed from the base of the PR and between fa2aa98 and 89ebc63.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (34)
  • .github/workflows/publish.yml
  • CHANGELOG.md
  • README.md
  • integrations/hermes/README.md
  • integrations/openclaw/README.md
  • package.json
  • packages/agentmemory-mcp/LICENSE
  • packages/agentmemory-mcp/README.md
  • packages/agentmemory-mcp/bin.mjs
  • packages/agentmemory-mcp/package.json
  • plugin/.claude-plugin/plugin.json
  • src/cli.ts
  • src/functions/access-tracker.ts
  • src/functions/auto-forget.ts
  • src/functions/context.ts
  • src/functions/evict.ts
  • src/functions/export-import.ts
  • src/functions/file-index.ts
  • src/functions/governance.ts
  • src/functions/relations.ts
  • src/functions/remember.ts
  • src/functions/retention.ts
  • src/functions/search.ts
  • src/functions/smart-search.ts
  • src/functions/snapshot.ts
  • src/functions/timeline.ts
  • src/functions/working-memory.ts
  • src/state/schema.ts
  • src/types.ts
  • src/version.ts
  • test/access-tracker.test.ts
  • test/export-import.test.ts
  • test/retention-access.test.ts
  • test/smart-search.test.ts

Comment thread .github/workflows/publish.yml
Comment thread CHANGELOG.md
Comment thread README.md
Comment on lines +19 to +29
export function normalizeAccessLog(raw: unknown): AccessLog {
const r = (raw ?? {}) as Partial<AccessLog>;
return {
memoryId: typeof r.memoryId === "string" ? r.memoryId : "",
count: typeof r.count === "number" && Number.isFinite(r.count) ? r.count : 0,
lastAt: typeof r.lastAt === "string" ? r.lastAt : "",
recent: Array.isArray(r.recent)
? r.recent.filter((x): x is number => typeof x === "number" && Number.isFinite(x))
: [],
};
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Reapply the 20-entry/access-count invariants during normalization.

normalizeAccessLog() is the defensive read path, but it currently preserves negative/fractional count values and arbitrarily long recent arrays. retention.ts trusts both fields, so malformed imported data can underflow salience or massively overboost retention while defeating the bounded ring-buffer guarantee.

Suggested hardening
 export function normalizeAccessLog(raw: unknown): AccessLog {
   const r = (raw ?? {}) as Partial<AccessLog>;
   return {
     memoryId: typeof r.memoryId === "string" ? r.memoryId : "",
-    count: typeof r.count === "number" && Number.isFinite(r.count) ? r.count : 0,
+    count:
+      typeof r.count === "number" && Number.isFinite(r.count)
+        ? Math.max(0, Math.trunc(r.count))
+        : 0,
     lastAt: typeof r.lastAt === "string" ? r.lastAt : "",
     recent: Array.isArray(r.recent)
-      ? r.recent.filter((x): x is number => typeof x === "number" && Number.isFinite(x))
+      ? r.recent
+          .filter((x): x is number => typeof x === "number" && Number.isFinite(x))
+          .slice(-RECENT_CAP)
       : [],
   };
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/functions/access-tracker.ts` around lines 19 - 29, normalizeAccessLog
currently allows negative/fractional counts and arbitrarily long recent arrays;
update it so count is coerced to an integer, clamped to the 0..20 range, and
recent is filtered to only finite numbers and truncated to the last 20 entries
(preserving order), then ensure the returned count is at least recent.length and
at most 20 (e.g., count = Math.max(truncatedRecent.length, Math.min(20,
Math.floor(parsedCount)))) — make these changes inside normalizeAccessLog to
reapply the 20-entry/access-count invariants.

Comment thread src/functions/access-tracker.ts
Comment on lines 49 to 52
if (!dryRun) {
await kv.delete(KV.memories, mem.id);
await deleteAccessLog(kv, mem.id);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add audit recording for TTL-driven deletions.

Line 50 and Line 51 mutate persistent state, but this branch does not record an audit event. That weakens deletion traceability for structural mutations.

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/auto-forget.ts` around lines 49 - 52, The TTL-driven deletion
branch in auto-forget.ts deletes persistent state (await kv.delete(KV.memories,
mem.id) and await deleteAccessLog(kv, mem.id)) without recording an audit; add a
call to recordAudit(...) after successful deletions (only when dryRun is false)
that records the deletion event, the resource id (mem.id), actor/context info
from the current request/kv context, and a descriptive reason (e.g., "TTL
expiration"); ensure recordAudit is awaited and placed so it runs only when both
delete and deleteAccessLog succeed to preserve audit consistency.

Comment thread src/functions/evict.ts
Comment on lines 139 to 142
if (!dryRun) {
await kv.delete(KV.memories, mem.id).catch(() => {});
await deleteAccessLog(kv, mem.id);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Guard access-log deletion on successful memory delete and handle cleanup errors.

At Line 141 and Line 156, access-log cleanup is unconditional even though memory deletion at Line 140/155 can fail silently. Also, uncaught deleteAccessLog failures can terminate eviction mid-run.

Suggested fix
-              await kv.delete(KV.memories, mem.id).catch(() => {});
-              await deleteAccessLog(kv, mem.id);
+              const deleted = await kv
+                .delete(KV.memories, mem.id)
+                .then(() => true)
+                .catch(() => false);
+              if (deleted) {
+                await deleteAccessLog(kv, mem.id).catch(() => {});
+              }

...

-              await kv.delete(KV.memories, mem.id).catch(() => {});
-              await deleteAccessLog(kv, mem.id);
+              const deleted = await kv
+                .delete(KV.memories, mem.id)
+                .then(() => true)
+                .catch(() => false);
+              if (deleted) {
+                await deleteAccessLog(kv, mem.id).catch(() => {});
+              }

Also applies to: 154-157

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

In `@src/functions/evict.ts` around lines 139 - 142, The access-log cleanup should
only run if the memory deletion succeeds and its errors must be caught; update
both places where you call kv.delete(KV.memories, mem.id) followed by
deleteAccessLog(kv, mem.id) (the blocks guarded by the dryRun check) to first
perform the delete and confirm success (e.g., await and check result or catch
delete error), and only then call deleteAccessLog inside its own try/catch so
any deleteAccessLog failure is swallowed/logged and does not abort eviction;
ensure you apply this change to both occurrences referencing
kv.delete(KV.memories, mem.id) and deleteAccessLog(kv, mem.id).

Comment thread src/functions/remember.ts
Comment on lines 126 to 129
if (data.memoryId) {
await kv.delete(KV.memories, data.memoryId);
await deleteAccessLog(kv, data.memoryId);
deleted++;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Audit trail is missing for explicit forget deletions.

Line 127 and Line 128 both mutate persistent state in mem::forget; this branch should emit an audit record for the structural deletion action.

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/remember.ts` around lines 126 - 129, In the mem::forget branch
where you call await kv.delete(KV.memories, data.memoryId) and await
deleteAccessLog(kv, data.memoryId), add a call to recordAudit(...) to emit an
audit record for this structural deletion; invoke recordAudit with the same kv
instance, an action name like "memory.forget" (or "forget"), and include the
memoryId (and any relevant actor/context) so the deletion is recorded; update
the block containing data.memoryId in src/functions/remember.ts (the if
(data.memoryId) branch) to call recordAudit before or after the deletes and keep
incrementing deleted.

Comment thread src/functions/snapshot.ts
Comment on lines +55 to +57
const accessLogs = await kv
.list<AccessLogExport>(KV.accessLog)
.catch(() => [] as AccessLogExport[]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't silently drop mem:access from a successful snapshot.

If kv.list(KV.accessLog) fails here, snapshot creation still succeeds with accessLogs: []. Restoring that snapshot later erases the retention history for every memory. Backup paths should fail fast or surface a partial-snapshot result instead of silently zeroing a namespace.

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

In `@src/functions/snapshot.ts` around lines 55 - 57, The snapshot code silently
swallows failures when reading access logs (the
kv.list<AccessLogExport>(KV.accessLog).catch(() => [] as AccessLogExport[]))
causing snapshots to succeed with an empty accessLogs array; change this to
fail-fast and surface the error instead of replacing with []: remove the silent
.catch or replace it with a try/catch that rethrows a wrapped Error (including
KV.accessLog and context) or returns an explicit partial-snapshot error result
so snapshot creation fails (or returns an error state) when kv.list for
KV.accessLog fails; locate the accessLogs variable and the kv.list call in
snapshot creation to implement this.

Real issues caught and fixed:

- access-tracker: deleteAccessLog now takes the same mem:access:<id>
  keyed mutex used by recordAccess, eliminating the race where a
  concurrent delete could drop an in-flight RMW update and leave a
  stale access log for an evicted memory
- access-tracker: normalizeAccessLog now coerces count to a
  non-negative integer, drops non-finite recent[] entries, truncates
  recent[] to the last 20 on read (enforcing the ring-buffer invariant),
  and guarantees count >= recent.length so a malformed row can't produce
  NaN downstream. count is NOT capped at 20 — it is the lifetime access
  counter, while recent[] is the 20-entry window
- export-import: import path now rejects accessLogs arrays above
  MAX_ACCESS_LOGS (50_000), normalizes each entry via
  normalizeAccessLog, and drops entries whose memoryId is not in the
  imported memories set, so a crafted export can't blow up memory or
  resurrect zombie access rows for memories that don't exist
- publish.yml: added a post-publish npm view polling loop for
  agentmemory-mcp mirroring the one that already guards the main
  package, so the workflow only succeeds when `npx agentmemory-mcp` is
  actually visible on the registry
- CHANGELOG.md: added the missing [0.8.3] reference-style link target
  at the bottom to match the [0.8.2]/[0.8.1]/[0.8.0] pattern
- README.md: standalone MCP inline examples now use `npx -y` to match
  the MCP config snippets and skip the install confirmation prompt

Tests: 680/680 (+10 new — deleteAccessLog behavior, normalizeAccessLog
invariants, malformed mem:access row handling in retention scoring).

Findings deliberately not applied:

- auto-forget.ts / remember.ts / evict.ts recordAudit calls: existing
  pattern in the codebase only audits governance deletes (user-initiated
  admin actions), not automatic eviction. Adding audit everywhere is a
  policy change, out of scope for this PR
- evict.ts gating deleteAccessLog on memory delete success: current
  order is defensible, deleteAccessLog is already best-effort with its
  own try/catch, and calling it after a failed memory delete is harmless
- snapshot.ts fail-fast on kv.list failure: inconsistent with the
  existing .catch(() => []) pattern used for observations in the same
  function
- retention.ts semantic evict only deletes KV.memories rows: real bug
  but pre-existing on main (RetentionScore has no source/type field).
  Tracked separately — out of scope for this PR
- bin.mjs fallback command: already present in the existing catch
  handler
- Promise.all parallelization of delete+deleteAccessLog: micro
  optimization not worth the readability cost
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.

MCP Install Failed BUG: Retention Score Formula is Broken

1 participant