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
97 lines
3.0 KiB
TypeScript
97 lines
3.0 KiB
TypeScript
import type { AdapterContext, MemoryCommitRequest } from "@rakazo/adapter-kit";
|
|
import { MemoryDocumentTooLargeError } from "@rakazo/adapter-kit";
|
|
import { describe, expect, it, vi } from "vitest";
|
|
import { datedMemoryNote, rememberFromTool } from "./remember.js";
|
|
|
|
const context: AdapterContext = {
|
|
operationId: "run-1",
|
|
traceId: "run-1",
|
|
spaceId: "space-1",
|
|
userId: "user-1",
|
|
signal: new AbortController().signal,
|
|
};
|
|
|
|
function remember(args: Record<string, unknown>, commit = vi.fn(async () => ({}) as never)) {
|
|
return {
|
|
commit,
|
|
result: rememberFromTool({ commit }, context, {
|
|
botId: "bot-1",
|
|
runId: "run-1",
|
|
threadId: "thread-1",
|
|
date: "2026-10-03",
|
|
args,
|
|
}),
|
|
};
|
|
}
|
|
|
|
describe("datedMemoryNote", () => {
|
|
it("is one dated bullet whose later lines stay inside it", () => {
|
|
expect(datedMemoryNote(" Prefers tea ", "2026-10-03")).toBe("- 2026-10-03: Prefers tea");
|
|
expect(datedMemoryNote("Trip plan\nBerlin first\n\nthen Vienna", "2026-10-03")).toBe(
|
|
"- 2026-10-03: Trip plan\n Berlin first\n\n then Vienna",
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("rememberFromTool", () => {
|
|
it("appends a dated note to MEMORY.md by default", async () => {
|
|
const { commit, result } = remember({ content: "Prefers tea" });
|
|
await expect(result).resolves.toEqual({ ok: true });
|
|
expect(commit).toHaveBeenCalledWith(
|
|
{
|
|
scope: "bot",
|
|
botId: "bot-1",
|
|
path: "MEMORY.md",
|
|
content: "- 2026-10-03: Prefers tea",
|
|
mode: "append",
|
|
sourceRunId: "run-1",
|
|
sourceThreadId: "thread-1",
|
|
} satisfies MemoryCommitRequest,
|
|
context,
|
|
);
|
|
});
|
|
|
|
it("rewrites the whole document only when asked to", async () => {
|
|
const { commit, result } = remember({
|
|
content: "# Chief\n\n- Prefers tea\n",
|
|
path: "notes.md",
|
|
mode: "replace",
|
|
});
|
|
await expect(result).resolves.toEqual({ ok: true });
|
|
expect(commit).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
path: "notes.md",
|
|
content: "# Chief\n\n- Prefers tea\n",
|
|
mode: "replace",
|
|
}),
|
|
context,
|
|
);
|
|
});
|
|
|
|
it("refuses an empty note without writing", async () => {
|
|
const { commit, result } = remember({ content: " " });
|
|
await expect(result).resolves.toEqual({ error: "content is empty" });
|
|
expect(commit).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("tells the bot to condense a full document", async () => {
|
|
const commit = vi.fn(async () => {
|
|
throw new MemoryDocumentTooLargeError("MEMORY.md");
|
|
});
|
|
const { result } = remember({ content: "One more fact" }, commit);
|
|
await expect(result).resolves.toEqual({
|
|
error:
|
|
'MEMORY.md is full (16 KB). Condense it with mode "replace", then save this note again.',
|
|
});
|
|
});
|
|
|
|
it("lets other storage failures fail the call", async () => {
|
|
const commit = vi.fn(async () => {
|
|
throw new Error("database unavailable");
|
|
});
|
|
await expect(remember({ content: "fact" }, commit).result).rejects.toThrow(
|
|
"database unavailable",
|
|
);
|
|
});
|
|
});
|