Skip to content

feat: add memory_update tool for memory versioning (#1018)#1038

Open
ziishanahmad wants to merge 2 commits into
rohitg00:mainfrom
ziishanahmad:feat/1018-memory-update-tool
Open

feat: add memory_update tool for memory versioning (#1018)#1038
ziishanahmad wants to merge 2 commits into
rohitg00:mainfrom
ziishanahmad:feat/1018-memory-update-tool

Conversation

@ziishanahmad

@ziishanahmad ziishanahmad commented Jul 9, 2026

Copy link
Copy Markdown

Problem

The data model supports version and supersedes fields, but there's no tool to update existing memories. Calling memory_save twice with the same concepts creates separate memories (both v1, both isLatest=true) instead of versioning. Users who update memories get duplicates instead of versions, requiring manual cleanup via memory_governance_delete.

Fix

1. Engine: mem::update function (src/functions/remember.ts)

  • Takes memoryId + content (required), plus optional type, concepts, files, ttlDays, agentId
  • Loads the existing memory by ID, returns memory not found if missing
  • Increments version, updates content/title, preserves all fields not provided (concepts, files, project, etc.)
  • Overwrites type/concepts/files only when explicitly provided
  • Re-indexes BM25 (remove old content, add new) and vector index
  • Records an audit trail entry (operation: "update")
  • Uses withKeyedLock for concurrency safety
  • No duplicate memory created — the memory is updated in-place (same ID, new version)

2. REST API: PUT /agentmemory/memories/update (src/triggers/api.ts)

  • Validates memoryId and content are present
  • Returns 404 if memory not found, 200 on success

3. MCP tool: memory_update (src/mcp/tools-registry.ts)

  • Registered as a CORE_TOOL alongside memory_save
  • Description guides agents: "Use this instead of calling memory_save twice when you want to revise a memory"
  • Required fields: memoryId, content

4. Standalone server (src/mcp/standalone.ts)

  • Added to IMPLEMENTED_TOOLS set
  • Validation case in validate()
  • Local fallback handler in handleLocal() (works without engine connection)
  • Proxy handler in handleProxy() (calls PUT endpoint when engine is connected)

Tests

test/memory-update.test.ts (9 tests):

  • Updates existing memory and increments version (v1→v2)
  • Preserves original memory ID (in-place, no new ID)
  • Preserves fields not provided in update (type, concepts, files, project)
  • Overwrites type and concepts when provided
  • Returns error for non-existent memory ID
  • Returns error when memoryId is missing
  • Returns error when content is missing
  • Supports multiple updates (v2→v3, version keeps incrementing)
  • Does not create a duplicate memory (count stays at 1)

Updated existing tests:

  • mcp-standalone.test.ts: CORE_TOOLS count 14→15
  • mcp-standalone-proxy.test.ts: IMPLEMENTED_TOOLS count 7→8, added memory_update to expected tool list

All 65 tests across 5 test files pass. Build succeeds.

Closes #1018

Summary by CodeRabbit

  • New Features

    • Added a new “memory update” action across the API, MCP toolset, and standalone tools, enabling editing an existing memory while keeping its versioned history.
    • Exposed the update action via the new PUT /agentmemory/memories/update endpoint.
    • Updated the CLI entry point and package metadata.
  • Bug Fixes

    • Memory updates now correctly increment versions, keep the same memory identity, and refresh search indexing.
    • Preserve unchanged metadata unless explicitly overwritten.
  • Tests

    • Expanded MCP tool-count expectations and added a full test suite for update/versioning and validation errors.

- Add mem::update engine function: updates existing memory by ID,
  increments version, preserves fields not provided, re-indexes
  BM25 + vector index, records audit trail
- Add REST API endpoint PUT /agentmemory/memories/update
- Add memory_update MCP tool definition in tools-registry.ts
- Add memory_update to standalone server (validate, handleLocal,
  handleProxy) so it works in both connected and fallback modes
- Add 9 tests covering: version increment, in-place update (no
  duplicate), field preservation, type/concept overwrite, not-found
  error, missing memoryId, missing content, multi-update versioning,
  no-duplicate guarantee
- Update existing standalone test counts (CORE_TOOLS 14→15,
  IMPLEMENTED_TOOLS 7→8)
@vercel

vercel Bot commented Jul 9, 2026

Copy link
Copy Markdown

@ziishanahmad 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 Jul 9, 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: c14eae7f-28ae-4dfc-b297-e0da4e5bdd64

📥 Commits

Reviewing files that changed from the base of the PR and between 43a1b26 and 60ab6bd.

📒 Files selected for processing (1)
  • package.json

📝 Walkthrough

Walkthrough

Adds a mem::update path for versioned memory updates, wires it through the authenticated HTTP API and MCP tool surface, and adds tests covering update behavior, tool counts, and local fallback handling.

Changes

memory_update feature

Layer / File(s) Summary
mem::update SDK function core logic
src/functions/remember.ts
Registers mem::update, validates inputs, computes the new version, builds the updated memory while preserving selected fields, persists old/new records, re-indexes search, and records an audit entry.
api::memory-update HTTP trigger
src/triggers/api.ts
Adds an authenticated PUT /agentmemory/memories/update endpoint that validates inputs, forwards optional update fields to mem::update, and maps not-found responses to 404.
memory_update MCP tool registration and standalone handling
src/mcp/tools-registry.ts, src/mcp/standalone.ts
Adds memory_update to the tool registry and standalone shim, extends validation, and implements proxy and local update handling.
Tests for memory_update tool and versioning
test/memory-update.test.ts, test/mcp-standalone.test.ts, test/mcp-standalone-proxy.test.ts
Adds update/versioning coverage and adjusts tool-count expectations to include memory_update.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant API as api::memory-update
  participant MemUpdate as mem::update
  participant KV as KV Store
  participant Search as BM25/Vector Index
  Client->>API: PUT /agentmemory/memories/update
  API->>API: validate memoryId, content
  API->>MemUpdate: forward update payload
  MemUpdate->>KV: load existing memory
  MemUpdate->>KV: save old (isLatest=false) and updated memory
  MemUpdate->>Search: remove old entry, re-index updated memory
  MemUpdate-->>API: success, updated memory
  API-->>Client: 200 with updated memory (or 404 if not found)
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning package.json metadata and publish settings were changed, which are unrelated to the memory_update/versioning work. Remove the package metadata and publish configuration edits unless they are required by a separate documented release task.
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 (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly states the main change: adding memory_update for memory versioning.
Linked Issues check ✅ Passed Implements #1018 by adding memory_update, version increments, supersession handling, and duplicate-free updates.
✨ 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.

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

Caution

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

⚠️ Outside diff range comments (1)
src/mcp/tools-registry.ts (1)

962-971: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

memory_update missing from ESSENTIAL_TOOLS.

memory_update is registered in CORE_TOOLS and IMPLEMENTED_TOOLS, but ESSENTIAL_TOOLS (used to filter tools when AGENTMEMORY_TOOLS=core) still omits it. Users running in core/minimal mode won't have access to this tool and will fall back to the exact save+delete workaround this PR is meant to eliminate (per issue #1018).

🛡️ Proposed fix
 export const ESSENTIAL_TOOLS = new Set([
   "memory_save",
+  "memory_update",
   "memory_recall",
🤖 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/mcp/tools-registry.ts` around lines 962 - 971, Add `memory_update` to
`ESSENTIAL_TOOLS` so core/minimal tool filtering includes it; update the
`ESSENTIAL_TOOLS` set in `tools-registry.ts` to match the already-registered
`CORE_TOOLS`/`IMPLEMENTED_TOOLS` entries. Use the `ESSENTIAL_TOOLS` symbol to
locate the set and ensure `memory_update` is included alongside the other memory
tools.
🧹 Nitpick comments (1)
test/memory-update.test.ts (1)

1-247: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Well-structured test suite for mem::update versioning.

The 9 test cases comprehensively cover version increment, in-place ID preservation, field preservation/overwrite, all three error paths (matching upstream error strings), repeated updates, and duplicate prevention. The mock strategy is sound — all mocked modules (logger, keyed-mutex, audit, access-tracker, config) are runtime dependencies of remember.ts, and the DI approach via makeMockSdk()/makeMockKV() is valid since registerRememberFunction accepts sdk/kv as parameters. Based on learnings, vi.mock is only effective for modules used at runtime; here the mocked modules are all runtime deps, so the mocks are correctly applied.

Consider adding coverage for:

  • ttlDays update — the upstream code computes a new forgetAfter timestamp, which is currently untested.
  • supersedes array — each update appends existing.id to supersedes; verifying this after multiple updates would catch potential duplication in the array.
  • agentId override — the upstream code uses data.agentId when provided, falling back to getAgentId().
🤖 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 `@test/memory-update.test.ts` around lines 1 - 247, The mem::update test suite
is missing coverage for a few behaviors in registerRememberFunction/update
handling. Add tests that verify ttlDays produces the expected updated
forgetAfter value, that repeated updates append the previous ID into the
supersedes array without breaking existing history, and that providing agentId
in the update payload overrides the getAgentId fallback. Use the existing
mem::update trigger flow and the mem::remember/mem::update symbols to locate the
relevant path.

Source: Learnings

🤖 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/functions/remember.ts`:
- Around line 216-219: The agentId handling in remember/update paths can
overwrite or drop the existing owner tag when data.agentId is missing, because
it falls back to getAgentId() instead of preserving existing.agentId. Update the
logic around callAgentId in remember.ts so memory_update keeps the prior agentId
unless an explicit new agentId is provided, matching the preservation behavior
used for fields like sessionIds, strength, and project. Apply the same fix
anywhere the update flow reuses this fallback so isolated agent scope does not
accidentally reassign or hide memories.
- Around line 250-257: The version update in remember.ts writes the same memory
twice through kv.set, so the pre-write on existing.id is redundant and can
briefly expose a stale isLatest state. Remove the first write that persists
existing after setting isLatest to false and updatedAt, and keep only the final
kv.set for updated.id in the update flow around existing and updated so the
latest record is stored once atomically from the caller’s perspective.
- Around line 196-201: The memory write paths are using different lock keys even
though they both mutate the same KV.memories record, so concurrent
remember/update operations can interleave and clobber versioning state. Update
the locking in the mem:update flow inside remember.ts to use the same shared
lock as the mem:remember path (or a single common lock helper) so both
read-modify-write sequences serialize on the same key. Keep the fix localized
around withKeyedLock and the memory write methods that touch KV.memories[id].
- Line 234: The supersedes chain in the in-place update path is incorrectly
appending the current record’s ID, creating a self-reference and distorting
lineage. Update the logic in remember.ts where the object is rebuilt so that the
`supersedes` field preserves the existing chain only, without adding
`existing.id` again; use the update flow around `updated`/`existing` to locate
it.
- Around line 176-194: The mem::update handler in remember.ts validates memoryId
and content but does not verify concepts or files before persisting them. Add
the same array-type checks used in mem::remember inside the update function so
api::memory-update rejects non-array payloads for concepts/files early. Keep the
validation alongside the existing input guards in the async handler to ensure
only arrays are stored.

In `@src/mcp/standalone.ts`:
- Around line 129-146: The memory_update validation is always populating
concepts and files with empty arrays, which causes preserved values to be wiped
on every update. In validate() for the memory_update case in standalone.ts, only
assign v.concepts and v.files when the corresponding args["concepts"] /
args["files"] are actually provided, matching the existing conditional handling
used for v.type. Then ensure handleProxy and handleLocal can distinguish “not
provided” from “provided empty list” so existing memory fields are preserved
unless the caller explicitly sends values.
- Around line 322-327: The local fallback in standalone.ts is adding the updated
record’s own identifier to its supersedes list, causing a self-referential
entry. Update the logic around the object built from existing["supersedes"] so
it excludes v.memoryId! when the record is being written back under the same
key, and only append prior version IDs or other distinct superseded IDs. Check
the fallback update path in the same area that mirrors the mem::update behavior
from remember.ts.

---

Outside diff comments:
In `@src/mcp/tools-registry.ts`:
- Around line 962-971: Add `memory_update` to `ESSENTIAL_TOOLS` so core/minimal
tool filtering includes it; update the `ESSENTIAL_TOOLS` set in
`tools-registry.ts` to match the already-registered
`CORE_TOOLS`/`IMPLEMENTED_TOOLS` entries. Use the `ESSENTIAL_TOOLS` symbol to
locate the set and ensure `memory_update` is included alongside the other memory
tools.

---

Nitpick comments:
In `@test/memory-update.test.ts`:
- Around line 1-247: The mem::update test suite is missing coverage for a few
behaviors in registerRememberFunction/update handling. Add tests that verify
ttlDays produces the expected updated forgetAfter value, that repeated updates
append the previous ID into the supersedes array without breaking existing
history, and that providing agentId in the update payload overrides the
getAgentId fallback. Use the existing mem::update trigger flow and the
mem::remember/mem::update symbols to locate the relevant 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: 9b3c7ddc-ff08-4a33-9bd4-04c42c799b5c

📥 Commits

Reviewing files that changed from the base of the PR and between 93ae9bc and 43a1b26.

📒 Files selected for processing (7)
  • src/functions/remember.ts
  • src/mcp/standalone.ts
  • src/mcp/tools-registry.ts
  • src/triggers/api.ts
  • test/mcp-standalone-proxy.test.ts
  • test/mcp-standalone.test.ts
  • test/memory-update.test.ts

Comment thread src/functions/remember.ts
Comment on lines +176 to +194
async (data: {
memoryId: string;
content: string;
type?: string;
concepts?: string[];
files?: string[];
ttlDays?: number;
agentId?: string;
}) => {
if (!data.memoryId || typeof data.memoryId !== "string") {
return { success: false, error: "memoryId is required" };
}
if (
!data.content ||
typeof data.content !== "string" ||
!data.content.trim()
) {
return { success: false, error: "content is required" };
}

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== src/functions/remember.ts outline ==\n'
ast-grep outline src/functions/remember.ts --view expanded || true

printf '\n== src/triggers/api.ts outline ==\n'
ast-grep outline src/triggers/api.ts --view expanded || true

printf '\n== search for memoryToObservation and normalizeList ==\n'
rg -n "memoryToObservation|normalizeList|api::memory-update|mem::remember|mem::update|concepts|files" src/functions/remember.ts src/triggers/api.ts || true

printf '\n== relevant slices from src/functions/remember.ts ==\n'
sed -n '1,280p' src/functions/remember.ts | cat -n | sed -n '1,280p'

printf '\n== relevant slices from src/triggers/api.ts ==\n'
sed -n '1,260p' src/triggers/api.ts | cat -n | sed -n '1,260p'

Repository: rohitg00/agentmemory

Length of output: 27900


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== locate memoryToObservation ==\n'
rg -n "function memoryToObservation|const memoryToObservation|memoryToObservation\\(" src || true

printf '\n== locate array iteration/join on concepts/files ==\n'
rg -n "concepts\\.|files\\.|join\\(|forEach\\(|map\\(" src/functions src/triggers || true

Repository: rohitg00/agentmemory

Length of output: 23234


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== src/state/memory-utils.ts ==\n'
wc -l src/state/memory-utils.ts
sed -n '1,220p' src/state/memory-utils.ts | cat -n

printf '\n== api::memory-remember and api::memory-update slice ==\n'
sed -n '940,1055p' src/triggers/api.ts | cat -n

Repository: rohitg00/agentmemory

Length of output: 6312


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== CompressedObservation / Memory types ==\n'
rg -n "type CompressedObservation|interface CompressedObservation|type Memory|interface Memory" src/types.* src || true

printf '\n== memory observation consumers ==\n'
rg -n "concepts\\.join\\(|files\\.join\\(|concepts\\?\\.|files\\?\\.|\\.concepts\\b|\\.files\\b" src/state src/functions | sed -n '1,220p'

printf '\n== focused memory-related search consumers ==\n'
sed -n '1,380p' src/state/hybrid-search.ts | cat -n | sed -n '250,340p'
sed -n '1,220p' src/state/memory-utils.ts | cat -n
sed -n '1,260p' src/state/types.* 2>/dev/null | cat -n || true

Repository: rohitg00/agentmemory

Length of output: 9364


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== src/state/search-index.ts slice ==\n'
sed -n '220,280p' src/state/search-index.ts | cat -n

printf '\n== src/types.ts Memory / CompressedObservation ==\n'
sed -n '40,120p' src/types.ts | cat -n

Repository: rohitg00/agentmemory

Length of output: 4441


Validate concepts and files in mem::update src/functions/remember.ts:176-194
Mirror the mem::remember array checks here. api::memory-update forwards concepts/files directly, so a non-array payload can still be persisted and later break search/index behavior.

🤖 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/functions/remember.ts` around lines 176 - 194, The mem::update handler in
remember.ts validates memoryId and content but does not verify concepts or files
before persisting them. Add the same array-type checks used in mem::remember
inside the update function so api::memory-update rejects non-array payloads for
concepts/files early. Keep the validation alongside the existing input guards in
the async handler to ensure only arrays are stored.

Source: Path instructions

Comment thread src/functions/remember.ts
Comment on lines +196 to +201
return withKeyedLock("mem:update", async () => {
const existing = await kv.get<Memory>(KV.memories, data.memoryId);
if (!existing) {
return { success: false, error: "memory not found" };
}

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n "withKeyedLock" src --type=ts -C2

Repository: rohitg00/agentmemory

Length of output: 27870


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the memory function around remember/update and the keyed lock helper.
sed -n '1,260p' src/functions/remember.ts
printf '\n--- keyed mutex ---\n'
sed -n '1,220p' src/state/keyed-mutex.ts

Repository: rohitg00/agentmemory

Length of output: 10354


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the rest of the mem::update implementation and any related helpers in remember.ts.
sed -n '260,420p' src/functions/remember.ts

Repository: rohitg00/agentmemory

Length of output: 4365


Use a shared lock for memory writes
mem::update and mem::remember both read-modify-write KV.memories[id], but they serialize on different lock keys (mem:update vs mem:remember). A concurrent supersede can overwrite the same record’s version/isLatest state and drop a just-written update.

🤖 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/functions/remember.ts` around lines 196 - 201, The memory write paths are
using different lock keys even though they both mutate the same KV.memories
record, so concurrent remember/update operations can interleave and clobber
versioning state. Update the locking in the mem:update flow inside remember.ts
to use the same shared lock as the mem:remember path (or a single common lock
helper) so both read-modify-write sequences serialize on the same key. Keep the
fix localized around withKeyedLock and the memory write methods that touch
KV.memories[id].

Comment thread src/functions/remember.ts
Comment on lines +216 to +219
const callAgentId =
typeof data.agentId === "string" && data.agentId.trim().length > 0
? data.agentId.trim().slice(0, 128)
: getAgentId();

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

agentId fallback can silently reassign/drop the memory's owner tag.

When data.agentId isn't supplied, callAgentId falls back to getAgentId() (the current process's agent identity), not existing.agentId. If the record was tagged with a different agent (or getAgentId() returns undefined), a plain memory_update call will overwrite or entirely drop the original agentId — unlike every other preserved field (sessionIds, strength, project, etc.). In AGENTMEMORY_AGENT_SCOPE=isolated mode this can move a memory out of its owner's visible scope or make it disappear from agent-filtered listings (see api::sessions/api::memories agentId filtering).

🛡️ Proposed fix
         const callAgentId =
           typeof data.agentId === "string" && data.agentId.trim().length > 0
             ? data.agentId.trim().slice(0, 128)
-            : getAgentId();
+            : existing.agentId;

Also applies to: 240-240

🤖 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/functions/remember.ts` around lines 216 - 219, The agentId handling in
remember/update paths can overwrite or drop the existing owner tag when
data.agentId is missing, because it falls back to getAgentId() instead of
preserving existing.agentId. Update the logic around callAgentId in remember.ts
so memory_update keeps the prior agentId unless an explicit new agentId is
provided, matching the preservation behavior used for fields like sessionIds,
strength, and project. Apply the same fix anywhere the update flow reuses this
fallback so isolated agent scope does not accidentally reassign or hide
memories.

Comment thread src/functions/remember.ts
strength: existing.strength,
version: newVersion,
parentId: existing.parentId,
supersedes: [...(existing.supersedes ?? []), existing.id],

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the relevant file and nearby usages of `supersedes`
git ls-files 'src/functions/remember.ts' 'src/**/*.ts' | sed -n '1,20p'
echo '--- remember.ts excerpt ---'
nl -ba src/functions/remember.ts | sed -n '180,280p'
echo '--- search supersedes usages ---'
rg -n "supersedes" src --glob '*.ts'

Repository: rohitg00/agentmemory

Length of output: 776


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- remember.ts lines 210-270 ---'
sed -n '210,270p' src/functions/remember.ts

echo '--- all supersedes usages ---'
grep -RIn --include='*.ts' 'supersedes' src || true

Repository: rohitg00/agentmemory

Length of output: 4942


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- remember.ts lines 80-130 ---'
sed -n '80,130p' src/functions/remember.ts

echo '--- relations.ts lines 130-180 ---'
sed -n '130,180p' src/functions/relations.ts

echo '--- diagnostics.ts supersedes checks ---'
sed -n '320,340p' src/functions/diagnostics.ts

Repository: rohitg00/agentmemory

Length of output: 4670


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- remember.ts update path around in-place save ---'
sed -n '220,255p' src/functions/remember.ts

echo '--- docs/comments mentioning same ID or supersedes semantics ---'
grep -RIn --include='*.ts' -E 'same ID|same id|supersedes.*self|lineage|version' src/functions src/mcp src/types.ts | sed -n '1,200p'

Repository: rohitg00/agentmemory

Length of output: 6279


Avoid appending the current ID to supersedes on in-place updates. updated.id is still existing.id, so this creates a self-reference and distorts lineage; keep the previous chain unchanged instead.

🤖 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/functions/remember.ts` at line 234, The supersedes chain in the in-place
update path is incorrectly appending the current record’s ID, creating a
self-reference and distorting lineage. Update the logic in remember.ts where the
object is rebuilt so that the `supersedes` field preserves the existing chain
only, without adding `existing.id` again; use the update flow around
`updated`/`existing` to locate it.

Comment thread src/functions/remember.ts
Comment on lines +250 to +257
// Mark the old version as no longer latest.
existing.isLatest = false;
existing.updatedAt = now;
await kv.set(KV.memories, existing.id, existing);

// Save the updated memory in-place (same ID, new version).
await kv.set(KV.memories, updated.id, updated);

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.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the target file around the referenced lines.
file='src/functions/remember.ts'
wc -l "$file"
sed -n '200,320p' "$file"

# Find the update function and any lock / search-list code that may observe intermediate state.
rg -n "mem:update|isLatest|updatedAt|kv\.set\(KV\.memories|KV\.memories|search|list" src/functions/remember.ts src -g '*.ts'

Repository: rohitg00/agentmemory

Length of output: 50376


Remove the redundant pre-write. existing.id and updated.id are the same, so the first kv.set() is overwritten immediately. It only adds I/O and briefly exposes a stale isLatest: false record to unlocked readers between awaits.

🤖 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/functions/remember.ts` around lines 250 - 257, The version update in
remember.ts writes the same memory twice through kv.set, so the pre-write on
existing.id is redundant and can briefly expose a stale isLatest state. Remove
the first write that persists existing after setting isLatest to false and
updatedAt, and keep only the final kv.set for updated.id in the update flow
around existing and updated so the latest record is stored once atomically from
the caller’s perspective.

Comment thread src/mcp/standalone.ts
Comment on lines +129 to +146
case "memory_update": {
const memoryId = args["memoryId"];
if (typeof memoryId !== "string" || !memoryId.trim()) {
throw new Error("memoryId is required");
}
const content = args["content"];
if (typeof content !== "string" || !content.trim()) {
throw new Error("content is required");
}
v.memoryId = memoryId;
v.content = content;
if (typeof args["type"] === "string" && args["type"].trim()) {
v.type = args["type"] as string;
}
v.concepts = normalizeList(args["concepts"]);
v.files = normalizeList(args["files"]);
return v;
}

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.

🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win

concepts/files are always sent as [], silently wiping preserved fields on every memory_update call.

normalizeList() returns [] (never undefined) when the argument is missing. In validate(), v.concepts/v.files are unconditionally set via normalizeList(args[...]) for memory_update (unlike v.type, which is only set when actually provided). Downstream:

  • handleProxy checks if (v.concepts) body["concepts"] = v.concepts;[] is truthy, so concepts: [] is always sent to PUT /agentmemory/memories/update, and mem::update's data.concepts ?? existing.concepts won't fall back (since [] isn't nullish) — existing concepts/files get erased on every call unless the caller explicitly repeats them.
  • handleLocal has the same problem: concepts: v.concepts ?? existing["concepts"] never falls back because v.concepts is never undefined.

This defeats the field-preservation behavior that the SDK-layer tests (test/memory-update.test.ts) verify for mem::update itself — the standalone MCP shim doesn't honor the same contract.

🛡️ Proposed fix
     case "memory_update": {
       const memoryId = args["memoryId"];
       if (typeof memoryId !== "string" || !memoryId.trim()) {
         throw new Error("memoryId is required");
       }
       const content = args["content"];
       if (typeof content !== "string" || !content.trim()) {
         throw new Error("content is required");
       }
       v.memoryId = memoryId;
       v.content = content;
       if (typeof args["type"] === "string" && args["type"].trim()) {
         v.type = args["type"] as string;
       }
-      v.concepts = normalizeList(args["concepts"]);
-      v.files = normalizeList(args["files"]);
+      if (args["concepts"] !== undefined) {
+        v.concepts = normalizeList(args["concepts"]);
+      }
+      if (args["files"] !== undefined) {
+        v.files = normalizeList(args["files"]);
+      }
       return v;
     }

Also applies to: 207-220, 317-318

🤖 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/mcp/standalone.ts` around lines 129 - 146, The memory_update validation
is always populating concepts and files with empty arrays, which causes
preserved values to be wiped on every update. In validate() for the
memory_update case in standalone.ts, only assign v.concepts and v.files when the
corresponding args["concepts"] / args["files"] are actually provided, matching
the existing conditional handling used for v.type. Then ensure handleProxy and
handleLocal can distinguish “not provided” from “provided empty list” so
existing memory fields are preserved unless the caller explicitly sends values.

Comment thread src/mcp/standalone.ts
Comment on lines +322 to +327
supersedes: [
...(Array.isArray(existing["supersedes"])
? (existing["supersedes"] as string[])
: []),
v.memoryId!,
],

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Self-referential supersedes in the local fallback (same conceptual bug as mem::update in remember.ts).

The updated record is stored under the same key (v.memoryId!), yet supersedes appends v.memoryId! to its own list — the memory ends up superseding itself.

🛡️ Proposed fix
-        supersedes: [
-          ...(Array.isArray(existing["supersedes"])
-            ? (existing["supersedes"] as string[])
-            : []),
-          v.memoryId!,
-        ],
+        supersedes: Array.isArray(existing["supersedes"])
+          ? (existing["supersedes"] as string[])
+          : [],
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
supersedes: [
...(Array.isArray(existing["supersedes"])
? (existing["supersedes"] as string[])
: []),
v.memoryId!,
],
supersedes: Array.isArray(existing["supersedes"])
? (existing["supersedes"] as string[])
: [],
🤖 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/mcp/standalone.ts` around lines 322 - 327, The local fallback in
standalone.ts is adding the updated record’s own identifier to its supersedes
list, causing a self-referential entry. Update the logic around the object built
from existing["supersedes"] so it excludes v.memoryId! when the record is being
written back under the same key, and only append prior version IDs or other
distinct superseded IDs. Check the fallback update path in the same area that
mirrors the mem::update behavior from remember.ts.

- Package name: ziiagentmemory (was @agentmemory/agentmemory)
- Author: Zeeshan Ahmad
- Repository: github.com/ziishanahmad/ziiagentmemory
- Bin: ziiagentmemory + agentmemory (backward compat)
- Public access, npm registry
ziishanahmad added a commit to ziishanahmad/ziiagentmemory that referenced this pull request Jul 9, 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.

Feature request: Add memory_update Tool for Memory Versioning

1 participant