-
Notifications
You must be signed in to change notification settings - Fork 2.1k
fix: post-tool-use skips self-capturing memory_* MCP tools (#993) #1044
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
4e2303e
020f897
1036034
43a1b26
60ab6bd
74e0627
025e80c
60577ad
5816f79
7c41c5b
2b57037
5d6e4cd
67e56c3
b5a2323
6909c26
193c0f8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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"; | ||||||||||||||||||||
|
|
@@ -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; | ||||||||||||||||||||
|
|
@@ -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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🛡️ 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 |
||||||||||||||||||||
|
|
||||||||||||||||||||
| return withKeyedLock("mem:update", async () => { | ||||||||||||||||||||
| const existing = await kv.get<Memory>(KV.memories, data.memoryId); | ||||||||||||||||||||
|
Comment on lines
+197
to
+198
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift Lock key mismatch lets
🔒 Suggested direction- return withKeyedLock("mem:update", async () => {
+ return withKeyedLock("mem:remember", async () => {Using the same lock name serializes both mutation paths against 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Because 🐛 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 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win Redundant/wasted KV write:
♻️ 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||
|
|
||||||||||||||||||||
| // 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.tsRepository: 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. 🤖 Prompt for AI Agents |
||||||||||||||||||||
|
|
||||||||||||||||||||
| 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; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -15,6 +15,7 @@ import { | |||||||||||||||||||
|
|
||||||||||||||||||||
| const IMPLEMENTED_TOOLS = new Set([ | ||||||||||||||||||||
| "memory_save", | ||||||||||||||||||||
| "memory_update", | ||||||||||||||||||||
| "memory_recall", | ||||||||||||||||||||
| "memory_smart_search", | ||||||||||||||||||||
| "memory_sessions", | ||||||||||||||||||||
|
|
@@ -96,6 +97,7 @@ function textResponse(payload: unknown, pretty = false): { | |||||||||||||||||||
| interface Validated { | ||||||||||||||||||||
| tool: string; | ||||||||||||||||||||
| content?: string; | ||||||||||||||||||||
| memoryId?: string; | ||||||||||||||||||||
| type?: string; | ||||||||||||||||||||
| concepts?: string[]; | ||||||||||||||||||||
| files?: string[]; | ||||||||||||||||||||
|
|
@@ -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"]; | ||||||||||||||||||||
|
|
@@ -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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.tsRepository: 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 -nRepository: rohitg00/agentmemory Length of output: 8062 Preserve omitted 🤖 Prompt for AI Agents |
||||||||||||||||||||
| case "memory_recall": { | ||||||||||||||||||||
| const body: Record<string, unknown> = { | ||||||||||||||||||||
|
|
@@ -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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win Same self-referential This local fallback keeps 🐛 Proposed fix- supersedes: [
- ...(Array.isArray(existing["supersedes"])
- ? (existing["supersedes"] as string[])
- : []),
- v.memoryId!,
- ],
+ supersedes: Array.isArray(existing["supersedes"])
+ ? (existing["supersedes"] as string[])
+ : [],📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||
| }; | ||||||||||||||||||||
| 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(); | ||||||||||||||||||||
|
|
||||||||||||||||||||
There was a problem hiding this comment.
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/agentmemoryfor installation and theagentmemoryCLI command. With the rebrand toziiagentmemoryand bin keyziiagentmemory, 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:
Also applies to: 17-17
🤖 Prompt for AI Agents
🗄️ Data Integrity & Integration | 🔴 Critical | 🏗️ Heavy lift
CI publish workflow still references the old
@agentmemory/agentmemorypackage name.The
.github/workflows/publish.ymlworkflow checksnpm view "@agentmemory/agentmemory@$(node -p "require('./package.json').version")"before publishing and again during registry propagation verification. With the package renamed toziiagentmemory, 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 publishstep itself will publishziiagentmemory(read frompackage.json), so the check/publish mismatch is the core defect.Update the workflow to reference
ziiagentmemory:And in the propagation wait step:
🤖 Prompt for AI Agents