fix: address code review on merged feature set

Five high-confidence findings from the post-merge reviewer pass:

1. memory.update MCP tool description claimed "Project is upserted if it
   doesn't exist", but the handler used resolveProjectId and would error.
   Matched the description to the actual behavior (call project.identify
   first) — keeps parity with memory.write.

2. updateMemoryAction's UPDATE statement was missing the userId guard.
   The preceding scoped SELECT made it not exploitable in practice, but
   it diverged from deleteMemoryAction's pattern. Added the guard for
   defense in depth.

3. putSnippet's UPDATE statement had the same missing userId guard —
   fixed the same way.

4. MemoryUpdateInput's refine for scope='user' accepted both
   project=undefined AND project=""; the snippets refine only accepted
   undefined. Tightened MemoryUpdateInput to require undefined, matching
   the snippets rule. Web actions already coerce "" → undefined before
   parsing, so no caller is affected.

5. 0002_snippets_scope.sql created two indexes unconditionally —
   replaced with CREATE INDEX IF NOT EXISTS so re-runs after a
   drizzle-kit push won't trip.

Also adds .claude/ to .gitignore so worktree directories from
multi-agent builds aren't accidentally committed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-17 06:53:32 -07:00
co-authored by Claude Opus 4.7
parent 5e57538719
commit 5b2bf7d19d
6 changed files with 15 additions and 9 deletions
+1 -1
View File
@@ -337,7 +337,7 @@ const memoryUpdate: ToolDef = {
project: {
type: "string",
description:
"Project key the memory should attach to (required and only valid when scope='project'). Project is upserted if it doesn't exist.",
"Project key the memory should attach to (required and only valid when scope='project'). The project must already exist — call `project.identify` first if it doesn't.",
},
},
required: ["id"],
+4 -1
View File
@@ -185,7 +185,10 @@ export async function updateMemoryAction(formData: FormData) {
}
}
await db.update(memories).set(update).where(eq(memories.id, parsed.data.id));
await db
.update(memories)
.set(update)
.where(and(eq(memories.id, parsed.data.id), eq(memories.userId, userId)));
const auditFields = Object.keys(update).filter((k) => k !== "updatedAt");
const auditPayload: Record<string, unknown> = { fields: auditFields };
+4 -1
View File
@@ -156,7 +156,10 @@ export async function putSnippet(
updatedAt: new Date(),
};
if (description !== undefined) updateValues.description = description;
await db.update(snippets).set(updateValues).where(eq(snippets.id, existing.id));
await db
.update(snippets)
.set(updateValues)
.where(and(eq(snippets.id, existing.id), eq(snippets.userId, userId)));
const refreshed = await findSnippet(userId, name, scope, projectId);
return { snippet: refreshed!, inserted: false };
}