Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@agentmemory/agentmemory",
"version": "0.9.27",
"description": "Persistent memory for AI coding agents, powered by iii-engine's three primitives",
"name": "ziiagentmemory",
"version": "0.3.0",
"description": "Persistent memory for AI coding agents — CJK-aware dedup, memory_update versioning, standalone MCP server",
Comment on lines +2 to +4

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.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

README.md still references the old package name and CLI command.

README.md (lines 88-104) references @agentmemory/agentmemory for installation and the agentmemory CLI command. With the rebrand to ziiagentmemory and bin key ziiagentmemory, users following README instructions will install the wrong package or get a "command not found" error.

Run the following script to find all stale references:

#!/bin/bash
# Search for old package name and CLI command references across the repo
rg -n 'agentmemory' --glob '!package.json' --glob '!node_modules' --glob '!.git' | head -50

Also applies to: 17-17

🤖 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 `@package.json` around lines 2 - 4, Update README.md to use the rebranded
package and CLI names instead of the old agentmemory references. Replace the
installation example that mentions `@agentmemory/agentmemory` with ziiagentmemory,
and change any CLI usage/examples from agentmemory to ziiagentmemory so users
install and run the correct command. Also scan the repo for stale agentmemory
references and update any user-facing docs or examples that still point to the
old package/bin names.

🗄️ Data Integrity & Integration | 🔴 Critical | 🏗️ Heavy lift

CI publish workflow still references the old @agentmemory/agentmemory package name.

The .github/workflows/publish.yml workflow checks npm view "@agentmemory/agentmemory@$(node -p "require('./package.json').version")" before publishing and again during registry propagation verification. With the package renamed to ziiagentmemory, these checks will query the wrong package — the pre-publish existence check will always report "not found" (causing re-publishes) and the propagation wait will never succeed, failing the job.

The npm publish step itself will publish ziiagentmemory (read from package.json), so the check/publish mismatch is the core defect.

Update the workflow to reference ziiagentmemory:

# .github/workflows/publish.yml

       - name: Publish `@agentmemory/agentmemory`
+      - name: Publish ziiagentmemory
         run: |
-          if npm view "`@agentmemory/agentmemory`@$(node -p "require('./package.json').version")" version >/dev/null 2>&1; then
+          if npm view "ziiagentmemory@$(node -p "require('./package.json').version")" version >/dev/null 2>&1; then
             echo "Version already published, skipping"
           else
             npm publish --provenance --access public
           fi

And in the propagation wait step:

-          if npm view "`@agentmemory/agentmemory`@$VERSION" version >/dev/null 2>&1; then
+          if npm view "ziiagentmemory@$VERSION" version >/dev/null 2>&1; then
🤖 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 `@package.json` around lines 2 - 4, The publish workflow is still checking the
old npm package name, causing the pre-publish existence check and registry
propagation wait to query the wrong package. Update the package references in
`.github/workflows/publish.yml` to use the renamed package from `package.json`
(`ziiagentmemory`) in both the publish guard and the propagation verification
step, keeping the `npm publish` flow aligned with the actual package name.

"type": "module",
"main": "dist/index.mjs",
"types": "dist/index.d.mts",
Expand All @@ -14,6 +14,7 @@
"./package.json": "./package.json"
},
"bin": {
"ziiagentmemory": "dist/cli.mjs",
"agentmemory": "dist/cli.mjs"
},
"scripts": {
Expand Down Expand Up @@ -53,11 +54,16 @@
"README.md",
"AGENTS.md"
],
"author": "Rohit Ghumare <ghumare64@gmail.com>",
"author": "Zeeshan Ahmad <ziishanahmad@gmail.com>",
"license": "Apache-2.0",
"repository": {
"type": "git",
"url": "https://github.com/rohitg00/agentmemory"
"url": "https://github.com/ziishanahmad/ziiagentmemory"
},
"homepage": "https://github.com/ziishanahmad/ziiagentmemory",
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
},
"dependencies": {
"@anthropic-ai/claude-agent-sdk": "^0.3.142",
Expand Down
126 changes: 125 additions & 1 deletion src/functions/remember.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { TriggerAction, type ISdk } from "iii-sdk";
import type { Memory } from "../types.js";
import { KV, generateId, jaccardSimilarity } from "../state/schema.js";
import { isNegationConflict } from "../state/memory-dedup.js";
import { StateKV } from "../state/kv.js";
import { withKeyedLock } from "../state/keyed-mutex.js";
import { memoryToObservation } from "../state/memory-utils.js";
Expand Down Expand Up @@ -78,7 +79,7 @@ export function registerRememberFunction(sdk: ISdk, kv: StateKV): void {
lowerContent,
existing.content.toLowerCase(),
);
if (similarity > 0.7) {
if (similarity > 0.7 && !isNegationConflict(lowerContent, existing.content.toLowerCase())) {
supersededId = existing.id;
supersededVersion = existing.version ?? 1;
supersededMemory = existing;
Expand Down Expand Up @@ -167,6 +168,129 @@ export function registerRememberFunction(sdk: ISdk, kv: StateKV): void {
},
);

// ── mem::update ─────────────────────────────────────────────────────
// Updates an existing memory by ID: sets new content, increments version,
// and marks the old memory as superseded. This fills the gap identified
// in #1018 where calling memory_save twice with the same concepts created
// two separate v1 memories instead of versioning.
sdk.registerFunction("mem::update",
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" };
}
Comment on lines +189 to +195

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

mem::update doesn't validate concepts/files are arrays, unlike mem::remember.

mem::remember explicitly rejects non-array concepts/files (lines 33-38), but mem::update accepts them unchecked and writes data.concepts ?? existing.concepts / data.files ?? existing.files straight into the persisted Memory record (typed string[]). A caller passing a string instead of an array corrupts the stored schema and can break downstream consumers (BM25 indexing, exports) that assume array semantics.

🛡️ Proposed fix
       if (
         !data.content ||
         typeof data.content !== "string" ||
         !data.content.trim()
       ) {
         return { success: false, error: "content is required" };
       }
+      if (data.concepts && !Array.isArray(data.concepts)) {
+        return { success: false, error: "concepts must be an array" };
+      }
+      if (data.files && !Array.isArray(data.files)) {
+        return { success: false, error: "files must be an array" };
+      }

Also applies to: 229-230

🤖 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 189 - 195, The mem::update path in
remember.ts is missing validation for concepts and files, unlike mem::remember,
so add the same array-type checks before persisting updates. Update the
validation logic in the update flow that builds the Memory record so
data.concepts and data.files must be arrays when present, and reject non-array
values before reaching the existing data.concepts ?? existing.concepts /
data.files ?? existing.files assignment. Use mem::remember as the reference
behavior and keep the checks close to the update handler that writes the Memory
object so downstream consumers continue to receive string[] values.


return withKeyedLock("mem:update", async () => {
const existing = await kv.get<Memory>(KV.memories, data.memoryId);
Comment on lines +197 to +198

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 | 🏗️ Heavy lift

Lock key mismatch lets mem::remember and mem::update interleave on the same record.

mem::remember locks on "mem:remember" while mem::update locks on "mem:update". Since both read-then-write the same KV.memories record set (remember can supersede any isLatest memory by similarity, update targets by id directly), a concurrent mem::remember call whose content is jaccard-similar to the memory being updated can read the pre-update state, then both writers race to the same key — last write wins and one update is silently lost.

🔒 Suggested direction
-      return withKeyedLock("mem:update", async () => {
+      return withKeyedLock("mem:remember", async () => {

Using the same lock name serializes both mutation paths against KV.memories. A more scalable fix would be a shared lock keyed by data.memoryId that mem::remember's supersede-scan also acquires once it picks a supersededId.

📝 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
return withKeyedLock("mem:update", async () => {
const existing = await kv.get<Memory>(KV.memories, data.memoryId);
return withKeyedLock("mem:remember", async () => {
const existing = await kv.get<Memory>(KV.memories, data.memoryId);
🤖 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 197 - 198, The mutation paths in
remember.ts use different lock keys, which allows mem::remember and mem::update
to race on the same KV.memories records. Make both write paths serialize through
a shared lock in the remember/update flow by changing withKeyedLock usage in the
mem::update path and the corresponding mem::remember supersede flow to use the
same lock name, ideally keyed by the target memoryId or supersededId so the same
record cannot be read and written concurrently. Ensure the locking logic is
applied around the read-then-write sections in the remember/update functions.

if (!existing) {
return { success: false, error: "memory not found" };
}

const now = new Date().toISOString();
const newVersion = (existing.version ?? 1) + 1;
const validTypes = new Set([
"pattern",
"preference",
"architecture",
"bug",
"workflow",
"fact",
]);
const memType = validTypes.has(data.type || "")
? (data.type as Memory["type"])
: existing.type;

const callAgentId =
typeof data.agentId === "string" && data.agentId.trim().length > 0
? data.agentId.trim().slice(0, 128)
: getAgentId();

const updated: Memory = {
id: existing.id,
createdAt: existing.createdAt,
updatedAt: now,
type: memType,
title: data.content.slice(0, 80),
content: data.content,
concepts: data.concepts ?? existing.concepts,
files: data.files ?? existing.files,
sessionIds: existing.sessionIds,
strength: existing.strength,
version: newVersion,
parentId: existing.parentId,
supersedes: [...(existing.supersedes ?? []), existing.id],
sourceObservationIds: existing.sourceObservationIds,
Comment on lines +233 to +236

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

supersedes becomes self-referential — existing.id always equals updated.id.

Because mem::update is an in-place update (id: existing.id), existing.id and updated.id are the same string. Appending existing.id to existing.supersedes therefore makes the memory list itself as something it supersedes, and every subsequent update appends another duplicate of its own id — the array grows unbounded with no meaningful history (contrast with mem::remember, where supersededId is a genuinely different, prior memory's id).

🐛 Proposed fix
-          supersedes: [...(existing.supersedes ?? []), existing.id],
+          supersedes: existing.supersedes ?? [],

If version history is intended, track it via a separate field (e.g. version log) rather than supersedes, which is otherwise used to reference distinct prior memory ids.

📝 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
version: newVersion,
parentId: existing.parentId,
supersedes: [...(existing.supersedes ?? []), existing.id],
sourceObservationIds: existing.sourceObservationIds,
version: newVersion,
parentId: existing.parentId,
supersedes: existing.supersedes ?? [],
sourceObservationIds: existing.sourceObservationIds,
🤖 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 233 - 236, The `supersedes`
assignment in `remember` is self-referential because `mem::update` keeps the
same `existing.id`, so appending `existing.id` just records the memory as
superseding itself and grows duplicates on every update. Update the logic around
the `mem::update` path to stop adding the current id to `supersedes`; if history
is needed, store it in a separate version-history field instead of reusing
`supersedes`. Keep `mem::remember` behavior unchanged, since it correctly
references a distinct prior memory id.

isLatest: true,
forgetAfter: existing.forgetAfter,
...(existing.imageRef ? { imageRef: existing.imageRef } : {}),
...(existing.imageData ? { imageData: existing.imageData } : {}),
...(callAgentId ? { agentId: callAgentId } : {}),
...(existing.project !== undefined && { project: existing.project }),
};

if (data.ttlDays && typeof data.ttlDays === "number" && data.ttlDays > 0) {
updated.forgetAfter = new Date(
Date.now() + data.ttlDays * 86400000,
).toISOString();
}

// 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);
Comment on lines +251 to +257

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.

🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Redundant/wasted KV write: existing is written then immediately clobbered by updated at the same key.

existing.id === updated.id, so kv.set(KV.memories, existing.id, existing) and the very next kv.set(KV.memories, updated.id, updated) target the identical KV key. The first write's effect (isLatest=false) is never observable in the final persisted state — it's just extra I/O, and it briefly exposes a transient inconsistent state (stale content marked not-latest) to any concurrent reader for no lasting benefit.

♻️ Proposed fix
-        // 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);
📝 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
// 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);
// Save the updated memory in-place (same ID, new version).
await kv.set(KV.memories, updated.id, updated);
🤖 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 251 - 257, The duplicate KV write in
remember.ts is unnecessary because existing.id and updated.id point to the same
record, so the first write is immediately overwritten. In the memory update flow
around the existing/updated kv.set calls, remove the intermediate persist of
existing and write only the final updated object (with the latest version state
applied) once via kv.set(KV.memories, updated.id, updated) to avoid wasted I/O
and transient inconsistent state.


// Re-index: remove old content, add new.
try {
getSearchIndex().remove(existing.id);
getSearchIndex().add(memoryToObservation(updated));
} catch (err) {
logger.warn("Failed to re-index updated memory in BM25", {
memId: updated.id,
error: err instanceof Error ? err.message : String(err),
});
}
await vectorIndexRemove(updated.id);
await vectorIndexAddGuarded(
updated.id,
updated.sessionIds?.[0] ?? "memory",
updated.title + " " + updated.content,
{ kind: "memory", logId: updated.id },
);

await recordAudit(kv, "update", "mem::update", [updated.id], {
memoryId: updated.id,
oldVersion: existing.version ?? 1,
newVersion,
reason: "user-initiated update",
});
Comment on lines +277 to +282

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
rg -n "AuditEntry" -A 20 src/types.ts | head -40
rg -n 'operation:\s*"' src/functions -g '*.ts'

Repository: rohitg00/agentmemory

Length of output: 818


🏁 Script executed:

#!/bin/bash
sed -n '561,610p' src/types.ts
rg -n "recordAudit\\(" src -g '*.ts'
sed -n '1,220p' src/functions/remember.ts

Repository: rohitg00/agentmemory

Length of output: 20632


🏁 Script executed:

#!/bin/bash
sed -n '1,120p' src/functions/audit.ts
sed -n '240,310p' src/functions/remember.ts
rg -n '"update"' src -g '*.ts'

Repository: rohitg00/agentmemory

Length of output: 6558


src/functions/remember.ts:277 — Use a valid audit operation literal here. recordAudit only accepts AuditEntry["operation"], and "update" is not part of the union in src/types.ts. Map this to an existing operation or extend the audit enum if memory updates need their own event.

🤖 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 277 - 282, The audit call in
recordAudit for the memory update flow is using an invalid operation literal, so
fix the remember.ts update path by replacing "update" with one of the existing
AuditEntry["operation"] values used in src/types.ts, or extend the audit
operation union if memory updates need a dedicated event. Keep the change
localized around the recordAudit invocation in the update logic that builds the
mem::update audit entry.


logger.info("Memory updated", {
memId: updated.id,
oldVersion: existing.version ?? 1,
newVersion,
});
return { success: true, memory: updated };
});
},
);

sdk.registerFunction("mem::forget",
async (data: {
sessionId?: string;
Expand Down
2 changes: 1 addition & 1 deletion src/functions/summarize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ export function registerSummarizeFunction(
return { success: false, error: "no_observations" };
}

if (provider.name === "noop") {
if (provider.name === "noop" || provider.name === "resilient(noop)") {
logger.info("Summarize skipped — no LLM provider configured", {
sessionId,
});
Expand Down
22 changes: 22 additions & 0 deletions src/hooks/post-tool-use.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,23 @@ function isSdkChildContext(payload: unknown): boolean {
const REST_URL = process.env["AGENTMEMORY_URL"] || "http://localhost:3111";
const SECRET = process.env["AGENTMEMORY_SECRET"] || "";

// #993: agentmemory's own MCP tools (memory_recall, memory_smart_search,
// etc.) create observations when they run — those observations then surface
// in later recall results, creating a feedback loop that pollutes the corpus.
// Skip capture for any tool whose name starts with "memory_" or "mem::".
const SELF_CAPTURE_PREFIXES = ["memory_", "mem::"];

function isSelfCaptureTool(toolName: unknown): boolean {
if (typeof toolName !== "string") return false;
return SELF_CAPTURE_PREFIXES.some((p) => toolName.startsWith(p));
}

// #993: allow users to disable all post-tool-use observation capture via
// AGENTMEMORY_CAPTURE_TOOL_USE=false. Mirrors the opt-in/opt-out pattern
// used by AGENTMEMORY_INJECT_CONTEXT on pre-tool-use.
const CAPTURE_TOOL_USE =
process.env["AGENTMEMORY_CAPTURE_TOOL_USE"] !== "false";

function authHeaders(): Record<string, string> {
const h: Record<string, string> = { "Content-Type": "application/json" };
if (SECRET) h["Authorization"] = `Bearer ${SECRET}`;
Expand All @@ -35,6 +52,11 @@ async function main() {
const toolName = data.tool_name ?? data.toolName;
const toolInput = data.tool_input ?? data.toolArgs;

// #993: skip self-capturing agentmemory's own MCP tools and allow
// users to disable all post-tool-use observation via env override.
if (!CAPTURE_TOOL_USE) return;
if (isSelfCaptureTool(toolName)) return;

const { imageData, cleanOutput } = extractImageData(toolOutput(data));

fetch(`${REST_URL}/agentmemory/observe`, {
Expand Down
10 changes: 10 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,16 @@ async function main() {
bootLog(
`Git snapshots: ${snapshotConfig.dir} (every ${snapshotConfig.interval}s)`,
);
// #1006: actually schedule the periodic snapshot timer. Previously
// the interval was read and logged but never passed to setInterval,
// so snapshots only happened on manual trigger.
const snapshotIntervalMs = snapshotConfig.interval * 1000;
const snapshotTimer = setInterval(async () => {
try {
await sdk.trigger({ function_id: "mem::snapshot-create", payload: {} });
} catch {}
}, snapshotIntervalMs);
snapshotTimer.unref();
}

const bm25Index = getSearchIndex();
Expand Down
75 changes: 74 additions & 1 deletion src/mcp/standalone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {

const IMPLEMENTED_TOOLS = new Set([
"memory_save",
"memory_update",
"memory_recall",
"memory_smart_search",
"memory_sessions",
Expand Down Expand Up @@ -96,6 +97,7 @@ function textResponse(payload: unknown, pretty = false): {
interface Validated {
tool: string;
content?: string;
memoryId?: string;
type?: string;
concepts?: string[];
files?: string[];
Expand Down Expand Up @@ -124,6 +126,24 @@ function validate(toolName: string, args: Record<string, unknown>): Validated {
v.files = normalizeList(args["files"]);
return v;
}
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;
}
case "memory_recall":
case "memory_smart_search": {
const query = args["query"];
Expand Down Expand Up @@ -182,7 +202,21 @@ async function handleProxy(
files: v.files,
}),
});
return textResponse(result);
return textResponse(result, true);
}
case "memory_update": {
const body: Record<string, unknown> = {
memoryId: v.memoryId,
content: v.content,
};
if (v.type) body["type"] = v.type;
if (v.concepts) body["concepts"] = v.concepts;
if (v.files) body["files"] = v.files;
const result = await handle.call("/agentmemory/memories/update", {
method: "PUT",
body: JSON.stringify(body),
});
return textResponse(result, true);
}
Comment on lines +207 to 220

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 | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n -A 10 'function normalizeList' src/mcp/standalone.ts

Repository: rohitg00/agentmemory

Length of output: 511


🏁 Script executed:

#!/bin/bash
set -e
sed -n '120,180p' src/mcp/standalone.ts | cat -n
printf '\n----\n'
sed -n '200,340p' src/mcp/standalone.ts | cat -n

Repository: rohitg00/agentmemory

Length of output: 8062


Preserve omitted concepts/files on update. normalizeList() turns missing input into [], so memory_update sends empty arrays in the proxy path and ?? keeps them in the local path, which clears existing values on partial updates. Return undefined for absent fields or gate these assignments on key presence.

🤖 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 207 - 220, The memory_update handler is
overwriting existing concepts/files with empty arrays when those fields are
omitted. Update the standalone proxy path in memory_update so it only includes
concepts and files when the input actually provided them, matching the local
path behavior and avoiding accidental clearing; use the existing memory_update
case and normalizeList call sites to ensure absent fields stay undefined instead
of [].

case "memory_recall": {
const body: Record<string, unknown> = {
Expand Down Expand Up @@ -263,6 +297,45 @@ async function handleLocal(
return textResponse({ saved: id });
}

case "memory_update": {
const existing = await kvInstance.get<Record<string, unknown>>(
"mem:memories",
v.memoryId!,
);
if (!existing) {
return textResponse({ success: false, error: "memory not found" });
}
const oldVersion =
typeof existing["version"] === "number" ? existing["version"] : 1;
const newVersion = oldVersion + 1;
const isoNow = new Date().toISOString();
const updated = {
...existing,
type: v.type ?? existing["type"],
title: (v.content || "").slice(0, 80),
content: v.content,
concepts: v.concepts ?? existing["concepts"],
files: v.files ?? existing["files"],
updatedAt: isoNow,
version: newVersion,
isLatest: true,
supersedes: [
...(Array.isArray(existing["supersedes"])
? (existing["supersedes"] as string[])
: []),
v.memoryId!,
],
Comment on lines +322 to +327

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

Same self-referential supersedes issue as mem::update in src/functions/remember.ts.

This local fallback keeps v.memoryId! as the KV key for updated (in-place update), so v.memoryId! equals the record's own id. Appending it to supersedes makes the memory reference itself, growing with a duplicate on every update.

🐛 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 the
update path is adding the record’s own memory id into supersedes, causing
self-references to accumulate on every in-place update. Update the logic in the
standalone memory update flow so the supersedes list only carries prior ids from
the existing record and never appends v.memoryId! when it is the same as the
current record id, mirroring the fix needed in mem::update/remember.ts.

};
await kvInstance.set("mem:memories", v.memoryId!, updated);
kvInstance.persist();
return textResponse({
success: true,
memory: updated,
oldVersion,
newVersion,
});
}

case "memory_recall":
case "memory_smart_search": {
const query = (v.query || "").toLowerCase();
Expand Down
34 changes: 34 additions & 0 deletions src/mcp/tools-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,40 @@ export const CORE_TOOLS: McpToolDef[] = [
required: ["content"],
},
},
{
name: "memory_update",
description:
"Update an existing memory by ID with new content. Increments the version " +
"and supersedes the old content. Use this instead of calling memory_save twice " +
"when you want to revise a memory rather than create a duplicate.",
inputSchema: {
type: "object",
properties: {
memoryId: {
type: "string",
description: "The ID of the memory to update (e.g. mem_xxx)",
},
content: {
type: "string",
description: "The new content for the memory",
},
type: {
type: "string",
description:
"New memory type (optional, keeps existing if omitted): pattern, preference, architecture, bug, workflow, or fact",
},
concepts: {
type: "string",
description: "Comma-separated key concepts (optional, keeps existing if omitted)",
},
files: {
type: "string",
description: "Comma-separated relevant file paths (optional, keeps existing if omitted)",
},
},
required: ["memoryId", "content"],
},
},
{
name: "memory_file_history",
description: "Get past observations about specific files.",
Expand Down
Loading