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
56 lines
1.9 KiB
TypeScript
56 lines
1.9 KiB
TypeScript
import type { AdapterContext, MemoryStore } from "@rakazo/adapter-kit";
|
|
import { MEMORY_DOCUMENT_MAX_BYTES, MemoryDocumentTooLargeError } from "@rakazo/adapter-kit";
|
|
|
|
const DEFAULT_MEMORY_PATH = "MEMORY.md";
|
|
|
|
/** One dated Markdown bullet; later lines are indented so the note stays a single list item. */
|
|
export function datedMemoryNote(content: string, date: string): string {
|
|
const [first = "", ...rest] = content.trim().split(/\r?\n/);
|
|
return [`- ${date}: ${first}`, ...rest.map((line) => (line.trim() ? ` ${line}` : ""))].join(
|
|
"\n",
|
|
);
|
|
}
|
|
|
|
/**
|
|
* The builtin `remember` tool: appends a dated note to the bot's memory document, or rewrites the
|
|
* whole document with `mode: "replace"`. Retries of the same call are replayed from its effect
|
|
* record by the executor, so a note is appended once.
|
|
*/
|
|
export async function rememberFromTool(
|
|
memory: Pick<MemoryStore, "commit">,
|
|
context: AdapterContext,
|
|
input: {
|
|
botId: string;
|
|
runId: string;
|
|
threadId: string;
|
|
/** The user's local date, YYYY-MM-DD. */
|
|
date: string;
|
|
args: Record<string, unknown>;
|
|
},
|
|
): Promise<{ ok: true } | { error: string }> {
|
|
const replace = input.args.mode === "replace";
|
|
const content = String(input.args.content ?? "");
|
|
const path = String(input.args.path ?? "").trim() || DEFAULT_MEMORY_PATH;
|
|
if (!replace && !content.trim()) return { error: "content is empty" };
|
|
try {
|
|
await memory.commit(
|
|
{
|
|
scope: "bot",
|
|
botId: input.botId,
|
|
path,
|
|
content: replace ? content : datedMemoryNote(content, input.date),
|
|
mode: replace ? "replace" : "append",
|
|
sourceRunId: input.runId,
|
|
sourceThreadId: input.threadId,
|
|
},
|
|
context,
|
|
);
|
|
} catch (error) {
|
|
if (!(error instanceof MemoryDocumentTooLargeError)) throw error;
|
|
return {
|
|
error: `${path} is full (${MEMORY_DOCUMENT_MAX_BYTES / 1024} KB). Condense it with mode "replace", then save this note again.`,
|
|
};
|
|
}
|
|
return { ok: true };
|
|
}
|