evOo-local/packages/memory/src/index.test.ts
Claude 4875753d9c
feat(memory): remember appends dated notes instead of replacing memory
The remember tool rewrote the bot's whole MEMORY.md with each call, so every
new fact erased the earlier ones unless the model re-sent the full document.

- MemoryCommitRequest gains mode "append" (the store joins the note onto the
  current content inside the same serializable transaction and still writes a
  revision row) and every commit is capped at 16 KB per document.
- remember appends a dated bullet ("- YYYY-MM-DD: ...", the user's local date)
  by default and rewrites only with mode "replace"; a full document returns an
  error that tells the bot to condense it. Retries of the same tool call stay
  single appends through the existing effect record.
- Scripted runtime memory entries can append too; the scripted "remember"
  script now appends instead of overwriting the bot's memory.
- When memory does not fit the run's 32 KB budget, the prompt keeps a
  document's newest lines instead of its oldest.
- memory.update maps an oversized save to BAD_REQUEST.
- docs/PRODUCT.md records that UserFact is the cross-space user model and the
  space MEMORY.md stays space notes (no migration).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
2026-10-03 13:10:25 +00:00

65 lines
2.1 KiB
TypeScript

import type { AdapterContext } from "@rakazo/adapter-kit";
import { describe, expect, it, vi } from "vitest";
import { appendMarkdown, MarkdownMemoryStore } from "./index.js";
const context: AdapterContext = {
operationId: "read-memory",
traceId: "read-memory",
spaceId: "workspace-1",
userId: "user-1",
signal: new AbortController().signal,
};
describe("memory store contract shape", () => {
it("declares markdown portability", () => {
const store = new MarkdownMemoryStore({} as never);
expect(store.describe().capabilities.markdownPortable).toBe(true);
});
it("reads the most recently updated documents first", async () => {
const updatedAt = new Date("2026-08-16T10:00:00.000Z");
const findMany = vi
.fn()
.mockResolvedValue([
{ id: "memory-1", path: "facts.md", content: "A fact", revision: 3, updatedAt },
]);
const store = new MarkdownMemoryStore({ memoryDocument: { findMany } } as never);
await expect(store.read({ scope: "bot", botId: "bot-1" }, context)).resolves.toEqual({
documents: [
{
id: "memory-1",
path: "facts.md",
content: "A fact",
revision: 3,
updatedAt: updatedAt.toISOString(),
},
],
});
expect(findMany).toHaveBeenCalledWith({
where: {
spaceId: "workspace-1",
userId: "user-1",
scope: "bot",
botId: "bot-1",
},
orderBy: [{ updatedAt: "desc" }, { path: "asc" }],
});
});
});
describe("appendMarkdown", () => {
it("adds the note below a heading or paragraph, and right below a list", () => {
expect(appendMarkdown("# Chief\n\n", "- 2026-10-03: Prefers tea")).toBe(
"# Chief\n\n- 2026-10-03: Prefers tea\n",
);
expect(appendMarkdown("- a\n- b", "- c")).toBe("- a\n- b\n- c\n");
expect(appendMarkdown("- a\n more about a\n", "- c")).toBe("- a\n more about a\n- c\n");
expect(appendMarkdown("", "- first")).toBe("- first\n");
});
it("leaves the document unchanged for an empty note", () => {
expect(appendMarkdown("# Chief\n", " ")).toBe("# Chief\n");
});
});