Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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.1.0",
"description": "Persistent memory for AI coding agents — CJK-aware dedup, memory_update versioning, standalone MCP server",
"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
123 changes: 123 additions & 0 deletions src/functions/remember.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,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 +176 to +194

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


return withKeyedLock("mem:update", async () => {
const existing = await kv.get<Memory>(KV.memories, data.memoryId);
if (!existing) {
return { success: false, error: "memory not found" };
}

Comment on lines +196 to +201

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].

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();
Comment on lines +216 to +219

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.


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],

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.

sourceObservationIds: existing.sourceObservationIds,
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 +250 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.

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

// 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",
});

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
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;
}
Comment on lines +129 to +146

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.

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);
}
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.

🎯 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.

};
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
51 changes: 50 additions & 1 deletion src/triggers/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1001,7 +1001,56 @@ export function registerApiTriggers(
config: { api_path: "/agentmemory/remember", http_method: "POST" },
});

sdk.registerFunction("api::forget",
sdk.registerFunction("api::memory-update",
async (
req: ApiRequest<{
memoryId: string;
content: string;
type?: string;
concepts?: string[];
files?: string[];
ttlDays?: number;
agentId?: string;
}>,
): Promise<Response> => {
const authErr = checkAuth(req, secret);
if (authErr) return authErr;
if (!req.body?.memoryId) {
return { status_code: 400, body: { error: "memoryId is required" } };
}
if (
!req.body?.content ||
typeof req.body.content !== "string" ||
!req.body.content.trim()
) {
return { status_code: 400, body: { error: "content is required" } };
}
const result = await sdk.trigger({
function_id: "mem::update",
payload: {
memoryId: req.body.memoryId,
content: req.body.content,
...(req.body.type !== undefined && { type: req.body.type }),
...(req.body.concepts !== undefined && { concepts: req.body.concepts }),
...(req.body.files !== undefined && { files: req.body.files }),
...(req.body.ttlDays !== undefined && { ttlDays: req.body.ttlDays }),
...(req.body.agentId !== undefined && { agentId: req.body.agentId }),
},
});
const r = result as { success: boolean; error?: string };
if (!r.success && r.error === "memory not found") {
return { status_code: 404, body: result };
}
return { status_code: 200, body: result };
},
);
sdk.registerTrigger({
type: "http",
function_id: "api::memory-update",
config: { api_path: "/agentmemory/memories/update", http_method: "PUT" },
});

sdk.registerFunction("api::forget",
async (
req: ApiRequest<{
sessionId?: string;
Expand Down
Loading