evOo-local/packages/adapters/src/executor-effect-idempotency.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

550 lines
17 KiB
TypeScript

import type { AgentRunRequest } from "@rakazo/adapter-kit";
import {
legacyScopedToolEffectIdempotencyKey,
toolEffectIdempotencyKey,
} from "@rakazo/core/node/approval-effect-key";
import { describe, expect, it, vi } from "vitest";
import type * as AutoReviewModule from "./auto-review.js";
import type * as ComputerLifecycleModule from "./computer-lifecycle.js";
import { createRunExecutor } from "./executor.js";
vi.mock("./computer-lifecycle.js", async (importOriginal) => ({
...(await importOriginal<typeof ComputerLifecycleModule>()),
acquireComputerExecutionLease: async () => null,
provisionComputer: async () => ({ id: "computer-1", kind: "desktop" }),
}));
vi.mock("./auto-review.js", async (importOriginal) => ({
...(await importOriginal<typeof AutoReviewModule>()),
resolveAutoReviewChecker: () => ({ provider: "scripted", model: "checker" }),
isAutoReviewCheckerConfigured: () => true,
runAutoReviewJudge: vi.fn(),
}));
type Effect = {
id: string;
runId?: string;
kind: string;
idempotencyKey: string;
status: string;
request: unknown;
result?: unknown;
reviewDecision?: string;
};
type ToolCall = {
name: string;
args: Record<string, unknown>;
executionId: string;
};
function fixture(runId = "run-1") {
const effects: Effect[] = [];
const results: unknown[] = [];
const scratchpadRows: Array<{
id: string;
spaceId: string;
botId: string;
userId: string;
title: string;
status: string;
notes: string;
createdAt: Date;
updatedAt: Date;
}> = [];
const run = {
id: runId,
botId: "bot-1",
threadId: "thread-1",
taskId: "task-1",
spaceId: "space-1",
userId: "user-1",
status: "queued",
trigger: "user",
leaseFence: 0,
};
const memoryCommit = vi.fn(async () => ({ revision: "rev-1" }));
const externalEffect = {
findMany: vi.fn(
async ({
where,
}: {
where?: { id?: string; runId?: string; status?: string; kind?: string };
} = {}) =>
effects.filter((effect) => {
if (where?.status && effect.status !== where.status) return false;
if (where?.kind && effect.kind !== where.kind) return false;
if (where?.runId && effect.runId && effect.runId !== where.runId) return false;
if (where?.id && effect.id !== where.id) return false;
return true;
}),
),
findUnique: vi.fn(
async ({ where }: { where: { id?: string; idempotencyKey?: string } }) =>
effects.find((effect) =>
where.id ? effect.id === where.id : effect.idempotencyKey === where.idempotencyKey,
) ?? null,
),
create: vi.fn(async ({ data }: { data: Omit<Effect, "id"> }) => {
const effect = { ...data, id: `effect-${effects.length + 1}` };
effects.push(effect);
return { ...effect };
}),
update: vi.fn(async ({ where, data }: { where: { id: string }; data: Partial<Effect> }) => {
Object.assign(effects.find((effect) => effect.id === where.id)!, data);
}),
updateMany: vi.fn(
async ({ where, data }: { where: { id: string; status: string }; data: Partial<Effect> }) => {
const effect = effects.find(
(effect) => effect.id === where.id && effect.status === where.status,
);
if (!effect) return { count: 0 };
Object.assign(effect, data);
return { count: 1 };
},
),
};
const prisma = {
run: {
findUnique: vi.fn(async () => run),
findUniqueOrThrow: vi.fn(async () => run),
updateMany: vi.fn(async ({ data }: { data: Record<string, unknown> }) => {
Object.assign(run, data);
return { count: 1 };
}),
},
bot: {
findUniqueOrThrow: vi.fn(async () => ({
id: run.botId,
name: "Assistant",
title: "Assistant",
description: "Test assistant",
computerId: "computer-1",
computer: { id: "computer-1", scope: "dedicated" },
})),
findMany: vi.fn(async () => []),
},
attempt: {
create: vi.fn(async () => ({ id: "attempt-1" })),
update: vi.fn(),
updateMany: vi.fn(async () => ({ count: 1 })),
},
thread: { findUniqueOrThrow: vi.fn(async () => ({ id: run.threadId, groupId: null })) },
message: { findMany: vi.fn(async () => []) },
task: {
findUniqueOrThrow: vi.fn(async () => ({ id: run.taskId, prompt: "Update shared state" })),
},
connection: { findMany: vi.fn(async () => []) },
spaceModelPreference: { findFirst: vi.fn(async () => null) },
userModelCredential: { findFirst: vi.fn(async () => null) },
deploymentSettings: {
findUnique: vi.fn(async () => ({
defaultModelProvider: "scripted",
defaultModelId: "scripted",
})),
},
taughtSkill: { findMany: vi.fn(async () => []) },
agentSecret: { findMany: vi.fn(async () => []) },
user: { findUnique: vi.fn(async () => null) },
userProfile: { findUnique: vi.fn(async () => null) },
userFact: { findMany: vi.fn(async () => []) },
agentSkill: { findMany: vi.fn(async () => []) },
scratchpadItem: {
findMany: vi.fn(async () => scratchpadRows),
create: vi.fn(
async ({
data,
}: {
data: {
spaceId: string;
botId: string;
userId: string;
title: string;
status: string;
notes: string;
};
}) => {
const now = new Date("2026-09-14T00:00:00.000Z");
const row = {
id: `scratch-${scratchpadRows.length + 1}`,
...data,
createdAt: now,
updatedAt: now,
};
scratchpadRows.push(row);
return row;
},
),
},
actionApprovalRule: { findMany: vi.fn(async () => []) },
actionAutoReviewPreference: { findUnique: vi.fn(async () => ({ enabled: false })) },
externalEffect,
};
const pauseRunForInput = vi.fn(async () => {
run.status = "waiting_input";
return true;
});
const finalizeRun = vi.fn(async () => ({ continuationRunId: null }));
let calls: ToolCall[] = [];
const runtimeRun = vi.fn(async function* (request: AgentRunRequest) {
for (const call of calls) {
const result = await request.executeTool!(call.name, call.args, call.executionId);
results.push(result);
}
yield { type: "done" as const, text: "Done" };
});
const executor = createRunExecutor({
prisma,
runtime: { describe: () => ({ capabilities: { scripted: false } }), run: runtimeRun },
connector: {
discoverTools: async () => [],
resolveCall: async () => undefined,
execute: async function* () {},
},
sandbox: { describe: () => ({ capabilities: { graphical: false } }) },
memory: {
describe: () => ({ capabilities: {} }),
read: async () => ({ documents: [] }),
search: async () => [],
commit: memoryCommit,
exportMarkdown: async function* () {},
},
memoryProviders: { resolve: async () => null },
events: { append: vi.fn(async () => undefined), pauseRunForInput, finalizeRun },
jobs: { enqueue: vi.fn(async () => undefined) },
secrets: [],
} as unknown as Parameters<typeof createRunExecutor>[0]);
return {
effects,
results,
scratchpadRows,
memoryCommit,
setCalls(next: ToolCall[]) {
calls = next;
},
async run() {
run.status = "queued";
await executor.continueRun(run.id, "worker-1");
expect(runtimeRun).toHaveBeenCalled();
expect(prisma.attempt.update).not.toHaveBeenCalled();
expect(finalizeRun).not.toHaveBeenCalledWith(expect.objectContaining({ outcome: "failed" }));
},
};
}
describe("mutating tool effect idempotency keys", () => {
it("executes different tools that share a reused provider tool-call id", async () => {
const f = fixture("run-a");
f.setCalls([
{
name: "remember",
args: { path: "MEMORY.md", content: "team preference" },
executionId: "call_0",
},
{
name: "scratchpad_add",
args: { title: "follow up with design" },
executionId: "call_0",
},
]);
await f.run();
expect(f.memoryCommit).toHaveBeenCalledOnce();
expect(f.scratchpadRows).toHaveLength(1);
expect(f.effects.map((effect) => effect.idempotencyKey)).toEqual([
toolEffectIdempotencyKey("run-a", "remember", {
path: "MEMORY.md",
content: "team preference",
}),
toolEffectIdempotencyKey("run-a", "scratchpad_add", {
title: "follow up with design",
}),
]);
expect(f.effects.every((effect) => effect.status === "completed")).toBe(true);
expect(f.results[0]).toEqual({ ok: true });
expect(f.results[1]).toEqual(
expect.objectContaining({
item: expect.objectContaining({ title: "follow up with design" }),
}),
);
});
it("executes the same provider tool-call id on different runs", async () => {
const first = fixture("run-1");
first.setCalls([
{
name: "remember",
args: { path: "MEMORY.md", content: "first bot note" },
executionId: "call_0",
},
]);
await first.run();
const second = fixture("run-2");
// Shared ExternalEffect table: prior completed rows remain visible by idempotency key.
second.effects.push(...first.effects);
second.setCalls([
{
name: "remember",
args: { path: "MEMORY.md", content: "second bot note" },
executionId: "call_0",
},
]);
await second.run();
expect(second.memoryCommit).toHaveBeenCalledOnce();
expect(second.effects.map((effect) => effect.idempotencyKey)).toEqual([
toolEffectIdempotencyKey("run-1", "remember", {
path: "MEMORY.md",
content: "first bot note",
}),
toolEffectIdempotencyKey("run-2", "remember", {
path: "MEMORY.md",
content: "second bot note",
}),
]);
expect(second.results[0]).toEqual({ ok: true });
});
it("replays a true retry of the same effect without re-executing", async () => {
const f = fixture("run-retry");
f.setCalls([
{
name: "remember",
args: { path: "MEMORY.md", content: "durable fact" },
executionId: "call_0",
},
]);
await f.run();
expect(f.memoryCommit).toHaveBeenCalledOnce();
// remember appends, so only the effect record keeps a retry from adding the note twice.
expect(f.memoryCommit).toHaveBeenCalledWith(
expect.objectContaining({
mode: "append",
content: expect.stringMatching(/^- \d{4}-\d{2}-\d{2}: durable fact$/),
}),
expect.anything(),
);
expect(f.effects).toHaveLength(1);
f.setCalls([
{
name: "remember",
args: { path: "MEMORY.md", content: "durable fact" },
executionId: "call_0",
},
]);
await f.run();
expect(f.memoryCommit).toHaveBeenCalledOnce();
expect(f.effects).toHaveLength(1);
expect(f.effects[0]?.idempotencyKey).toBe(
toolEffectIdempotencyKey("run-retry", "remember", {
path: "MEMORY.md",
content: "durable fact",
}),
);
expect(f.results[1]).toEqual({ ok: true });
});
it("replays the same logical effect when restart assigns a new tool-call id", async () => {
const f = fixture("run-replay-id");
f.setCalls([
{
name: "remember",
args: { path: "MEMORY.md", content: "durable fact" },
executionId: "toolu_abc",
},
]);
await f.run();
expect(f.memoryCommit).toHaveBeenCalledOnce();
expect(f.effects).toHaveLength(1);
f.setCalls([
{
name: "remember",
args: { path: "MEMORY.md", content: "durable fact" },
executionId: "toolu_xyz",
},
]);
await f.run();
expect(f.memoryCommit).toHaveBeenCalledOnce();
expect(f.effects).toHaveLength(1);
expect(f.effects[0]?.idempotencyKey).toBe(
toolEffectIdempotencyKey("run-replay-id", "remember", {
path: "MEMORY.md",
content: "durable fact",
}),
);
expect(f.results[1]).toEqual({ ok: true });
});
it("executes the same tool twice in one run when args differ but the provider id is reused", async () => {
const f = fixture("run-args");
f.setCalls([
{
name: "remember",
args: { path: "MEMORY.md", content: "first fact" },
executionId: "call_0",
},
{
name: "remember",
args: { path: "MEMORY.md", content: "second fact" },
executionId: "call_0",
},
]);
await f.run();
expect(f.memoryCommit).toHaveBeenCalledTimes(2);
expect(f.effects).toHaveLength(2);
expect(f.effects[0]?.idempotencyKey).not.toBe(f.effects[1]?.idempotencyKey);
expect(f.results).toEqual([{ ok: true }, { ok: true }]);
});
it("replays a legacy bare provider idempotency key for the same run and tool", async () => {
const f = fixture("run-legacy");
f.effects.push({
id: "legacy-1",
runId: "run-legacy",
kind: "remember",
idempotencyKey: "call_0",
status: "completed",
request: { path: "MEMORY.md", content: "legacy fact" },
result: { ok: true, legacy: true },
});
f.setCalls([
{
name: "remember",
args: { path: "MEMORY.md", content: "legacy fact" },
executionId: "call_0",
},
]);
await f.run();
expect(f.memoryCommit).not.toHaveBeenCalled();
expect(f.effects).toHaveLength(1);
expect(f.results[0]).toEqual({ ok: true, legacy: true });
});
it("replays a legacy scoped key that included the provider tool-call id", async () => {
const args = { path: "MEMORY.md", content: "legacy scoped fact" };
const f = fixture("run-legacy-scoped");
f.effects.push({
id: "legacy-scoped-1",
runId: "run-legacy-scoped",
kind: "remember",
idempotencyKey: legacyScopedToolEffectIdempotencyKey(
"run-legacy-scoped",
"remember",
"call_0",
args,
),
status: "completed",
request: args,
result: { ok: true, legacy: true },
});
f.setCalls([
{
name: "remember",
args,
executionId: "call_0",
},
]);
await f.run();
expect(f.memoryCommit).not.toHaveBeenCalled();
expect(f.effects).toHaveLength(1);
expect(f.results[0]).toEqual({ ok: true, legacy: true });
});
it("does not reuse an incomplete legacy effect when the request differs", async () => {
const f = fixture("run-legacy-mismatch");
f.effects.push({
id: "legacy-open",
runId: "run-legacy-mismatch",
kind: "remember",
idempotencyKey: "call_0",
status: "intended",
request: { path: "MEMORY.md", content: "old fact" },
});
f.setCalls([
{
name: "remember",
args: { path: "MEMORY.md", content: "new fact" },
executionId: "call_0",
},
]);
await f.run();
expect(f.memoryCommit).toHaveBeenCalledOnce();
expect(f.effects).toHaveLength(2);
expect(f.effects[1]?.idempotencyKey).toBe(
toolEffectIdempotencyKey("run-legacy-mismatch", "remember", {
path: "MEMORY.md",
content: "new fact",
}),
);
expect(f.results[0]).toEqual({ ok: true });
});
it("executes two identical-args mutating calls in one live run", async () => {
const args = { path: "MEMORY.md", content: "same fact" };
const f = fixture("run-live-repeat");
f.setCalls([
{ name: "remember", args, executionId: "call_a" },
{ name: "remember", args, executionId: "call_b" },
]);
await f.run();
expect(f.memoryCommit).toHaveBeenCalledTimes(2);
expect(f.effects).toHaveLength(2);
expect(f.effects[0]?.idempotencyKey).toBe(
toolEffectIdempotencyKey("run-live-repeat", "remember", args),
);
expect(f.effects[1]?.idempotencyKey).toBe(
toolEffectIdempotencyKey("run-live-repeat", "remember", args, 1),
);
expect(f.results).toEqual([{ ok: true }, { ok: true }]);
});
it("replays a legacy scoped key when restart assigns a new tool-call id", async () => {
const args = { path: "MEMORY.md", content: "legacy scoped fact" };
const f = fixture("run-legacy-new-id");
f.effects.push({
id: "legacy-scoped-old-id",
runId: "run-legacy-new-id",
kind: "remember",
idempotencyKey: legacyScopedToolEffectIdempotencyKey(
"run-legacy-new-id",
"remember",
"call_old",
args,
),
status: "completed",
request: args,
result: { ok: true, legacy: true },
});
f.setCalls([
{
name: "remember",
args,
executionId: "call_new",
},
]);
await f.run();
expect(f.memoryCommit).not.toHaveBeenCalled();
expect(f.effects).toHaveLength(1);
expect(f.results[0]).toEqual({ ok: true, legacy: true });
});
});