Review by @kartik-mem0. Root cause across all four: Mem0's hosted add with
infer:true is async and returns a PENDING event before the memory is visible.
- store.ts/tools.ts: add() returns AddResult{status,eventId} via parseAddResult;
the v3 add path returns {status:"PENDING", event_id}, so remember reports
{status:"queued", eventId} for async writes and {status:"saved"} only when
resolved. FAILED/unknown status maps to completed, not queued.
- messages.ts: drop typed non-text parts (reasoning no longer leaks into memory);
bound the captured assistant reply to the current turn and collect all of its
text segments (text->tool->text) instead of a stale prior answer.
- capture.ts + README + eve.mdx: document operationId dedup as best-effort
(lookup+add is not atomic; pending/concurrent windows can double-write);
removed the "restart replays cannot write twice" claim.
Tests: 68 pass (+15). Added reasoning, tool-only/cross-turn, multi-segment,
pending/concurrent capture replay, and PENDING/FAILED add-result cases.
Typecheck + build clean.
142 lines
4.7 KiB
TypeScript
142 lines
4.7 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { captureCompletedTurn } from "../src/capture.js";
|
|
import { createFakeStore } from "./helpers.js";
|
|
|
|
describe("captureCompletedTurn", () => {
|
|
it("adds the current turn under the locked scope", async () => {
|
|
const store = createFakeStore();
|
|
const captured = await captureCompletedTurn({
|
|
store,
|
|
scopeKey: "scope_abc",
|
|
operationId: "op_1",
|
|
infer: false,
|
|
metadata: { source: "eve", operation_id: "op_1" },
|
|
turnInput: [{ role: "user", content: "I am vegetarian" }],
|
|
messages: [
|
|
{ role: "user", content: "I am vegetarian" },
|
|
{ role: "assistant", content: "Got it." },
|
|
],
|
|
});
|
|
|
|
expect(captured).toBe(true);
|
|
expect(store.added).toEqual([
|
|
{
|
|
userId: "scope_abc",
|
|
infer: false,
|
|
metadata: { source: "eve", operation_id: "op_1" },
|
|
messages: [
|
|
{ role: "user", content: "I am vegetarian" },
|
|
{ role: "assistant", content: "Got it." },
|
|
],
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("does not write when the turn has no user text", async () => {
|
|
const store = createFakeStore();
|
|
const captured = await captureCompletedTurn({
|
|
store,
|
|
scopeKey: "scope_abc",
|
|
operationId: "op_1",
|
|
infer: true,
|
|
metadata: { operation_id: "op_1" },
|
|
turnInput: [],
|
|
messages: [],
|
|
});
|
|
expect(captured).toBe(false);
|
|
expect(store.added).toEqual([]);
|
|
});
|
|
|
|
it("dedupes a replay once the prior write is visible", async () => {
|
|
const store = createFakeStore();
|
|
const args = {
|
|
store,
|
|
scopeKey: "scope_abc",
|
|
operationId: "op_dedup",
|
|
infer: true,
|
|
metadata: { source: "eve", operation_id: "op_dedup" },
|
|
turnInput: [{ role: "user", content: "I am vegetarian" }],
|
|
messages: [
|
|
{ role: "user", content: "I am vegetarian" },
|
|
{ role: "assistant", content: "Got it." },
|
|
],
|
|
};
|
|
// First write is immediately visible (completed), so the replay is deduped.
|
|
expect(await captureCompletedTurn(args)).toBe(true);
|
|
expect(await captureCompletedTurn(args)).toBe(false);
|
|
expect(store.added).toHaveLength(1);
|
|
});
|
|
|
|
it("cannot dedupe while the prior write is still pending (best-effort)", async () => {
|
|
// Documents the known limitation: with infer:true the first write is a
|
|
// PENDING event whose memory is not yet visible, so the metadata lookup
|
|
// finds nothing and a restart replay writes the same operation again.
|
|
const store = createFakeStore([], { pendingWrites: true });
|
|
const args = {
|
|
store,
|
|
scopeKey: "scope_abc",
|
|
operationId: "op_pending",
|
|
infer: true,
|
|
metadata: { source: "eve", operation_id: "op_pending" },
|
|
turnInput: [{ role: "user", content: "I am vegetarian" }],
|
|
messages: [
|
|
{ role: "user", content: "I am vegetarian" },
|
|
{ role: "assistant", content: "Got it." },
|
|
],
|
|
};
|
|
expect(await captureCompletedTurn(args)).toBe(true);
|
|
expect(await captureCompletedTurn(args)).toBe(true);
|
|
expect(store.added).toHaveLength(2);
|
|
});
|
|
|
|
it("does not dedupe concurrent replays that both pass the lookup (best-effort)", async () => {
|
|
// Two workers on separate provider instances both clear the metadata check
|
|
// before either write lands, so both capture the same operation.
|
|
const store = createFakeStore([], { pendingWrites: true });
|
|
const args = {
|
|
store,
|
|
scopeKey: "scope_abc",
|
|
operationId: "op_concurrent",
|
|
infer: true,
|
|
metadata: { source: "eve", operation_id: "op_concurrent" },
|
|
turnInput: [{ role: "user", content: "I am vegetarian" }],
|
|
messages: [
|
|
{ role: "user", content: "I am vegetarian" },
|
|
{ role: "assistant", content: "Got it." },
|
|
],
|
|
};
|
|
const [first, second] = await Promise.all([
|
|
captureCompletedTurn(args),
|
|
captureCompletedTurn(args),
|
|
]);
|
|
expect(first).toBe(true);
|
|
expect(second).toBe(true);
|
|
expect(store.added).toHaveLength(2);
|
|
});
|
|
|
|
it("skips add when the operation id was already captured", async () => {
|
|
const store = createFakeStore([
|
|
{
|
|
id: "mem_existing",
|
|
memory: "already stored",
|
|
metadata: { operation_id: "op_replay" },
|
|
},
|
|
]);
|
|
const captured = await captureCompletedTurn({
|
|
store,
|
|
scopeKey: "scope_abc",
|
|
operationId: "op_replay",
|
|
infer: true,
|
|
metadata: { operation_id: "op_replay" },
|
|
turnInput: [{ role: "user", content: "I am vegetarian" }],
|
|
messages: [
|
|
{ role: "user", content: "I am vegetarian" },
|
|
{ role: "assistant", content: "Got it." },
|
|
],
|
|
});
|
|
|
|
expect(captured).toBe(false);
|
|
expect(store.added).toEqual([]);
|
|
});
|
|
});
|