.maestro/playbooks/Wizard-2026-02-22/2026-02-22-LoCoMo-Eval/LOCOMO-EVAL-01.md
This phase sets up the LoCoMo evaluation project within the claude-mem codebase, downloads the benchmark dataset, builds the ingestion adapter that transforms multi-session conversations into claude-mem observations via the worker API, and ingests one complete conversation as a proof of concept. By the end, real LoCoMo dialog data will be stored as searchable claude-mem observations — demonstrating the full memory pipeline working end-to-end.
Methodology note: This eval uses dual scoring — token-level F1 (for comparison with the original LoCoMo paper, ACL 2024) and LLM-as-a-Judge (for comparison with modern memory systems: Mem0 at 66.88%, Zep at 65.99%, OpenAI Memory at 52.90%, full-context ceiling at 72.90%). The adversarial QA category is excluded from J-score comparison per Mem0's methodology (no available ground truth). See the Mem0 paper (arXiv 2504.19413) for baseline details.
Create eval project structure:
evals/locomo/src/ingestion/evals/locomo/src/qa/evals/locomo/src/scoring/evals/locomo/data/evals/locomo/results/checkpoints/evals/locomo/scripts/evals/locomo/tests/evals/locomo/tsconfig.json with strict TypeScript settings (module: esnext, target: esnext, moduleResolution: bundler, strict: true) — Bun runs TS natively so this is primarily for IDE supportevals/locomo/data/locomo-repo/ to the project's root .gitignore fileDownload LoCoMo dataset and create TypeScript type definitions:
https://github.com/snap-research/locomo into evals/locomo/data/locomo-repo/data/locomo10.json within the cloned repo — inspect the repo structure to confirm the exact path)locomo10.json to understand the exact field names and structure before writing typesevals/locomo/src/types.ts with interfaces based on the actual data:
LoCoMoConversation — sample_id, speaker_a, speaker_b, conversation (array of sessions), qa (array of questions)LoCoMoSession — session_id, date, turns array, plus an index signature [key: string]: any for dynamic keys like session_1_observation, session_1_summary, events_session_1LoCoMoTurn — speaker ("A" | "B"), dia_id (number), text (string), optional img_url and blip_captionLoCoMoQA — question, answer, category (string), evidence_dialog_ids (number array)IngestionProgress — sample_id, total_sessions, sessions_ingested, observations_queued, statusQAResult — question, predicted_answer, ground_truth_answer, category, f1_score, judge_scores (optional JudgeAggregation), search_results_used (number), search_latency_ms (number), answer_latency_ms (number), answer_input_tokens (number), answer_output_tokens (number)JudgeResult — score (number 0-100), explanation (string)JudgeAggregation — mean_score (number), std_dev (number), run_count (number), individual_scores (number array)LatencyStats — search_p50_ms, search_p95_ms, answer_p50_ms, answer_p95_ms, total_p50_ms, total_p95_msEvalReport — results (QAResult array), per_category_f1_scores (map of category to {mean_f1, count, min_f1, max_f1}), overall_f1, per_category_judge_scores (map of category to {mean_j, std_dev, count}), overall_judge_score (JudgeAggregation), latency_stats (LatencyStats), token_stats ({total_input_tokens, total_output_tokens, mean_tokens_per_question}), metadata (model, judge_model, timestamp, total_questions, scoring_methods: string[])Build dataset loader module with validation:
evals/locomo/src/dataset-loader.tsloadDataset() — reads and parses locomo10.json from the cloned repo, returns typed array of conversationsgetConversation(sampleId) — returns a single conversation by sample_idgetSessionsForConversation(conversation) — extracts sessions and resolves dynamic keys for each session (e.g., for session_id=1, look up session_1_observation, session_1_summary, events_session_1), returning an enriched session object with observation, summary, and events fieldsgetQuestionsForConversation(conversation, options?) — returns typed QA questions array. Accept an optional excludeCategories array parameter (default: exclude "adversarial" for J-score comparison). When called without options, exclude adversarial. Provide a separate getAllQuestionsForConversation(conversation) that returns all categories including adversarial (for F1-only analysis)getDatasetStats() — returns object with: conversation_count, total_sessions, total_qa_questions, qa_by_category (map of category string to count), qa_excluding_adversarial (total count without adversarial)evals/locomo/scripts/validate-dataset.ts — loads the dataset, calls getDatasetStats(), prints a formatted stats table showing conversation count, session counts, and QA question counts grouped by category. Clearly show the adversarial count separately and note it's excluded from J-score comparisonbun evals/locomo/scripts/validate-dataset.ts and verify the output shows 10 conversations with reasonable session and QA countsBuild worker API client module:
src/services/worker/http/routes/SessionRoutes.ts and src/services/worker/http/routes/SearchRoutes.ts for endpoint paths, request/response formats, and authentication requirementsevals/locomo/src/ingestion/worker-client.ts~/.claude-mem/.env (look for variables like AUTH_TOKEN or CLAUDE_MEM_AUTH_TOKEN — read the file to find the exact variable name used)http://localhost:37777initSession(contentSessionId: string, project: string, userPrompt: string) — POST to session init endpointqueueObservation(contentSessionId: string, toolName: string, toolInput: string, toolResponse: string, promptNumber: number) — POST to observations endpointcompleteSession(contentSessionId: string) — POST to session complete endpointgetSessionStatus(contentSessionId: string) — GET session statuswaitForProcessing(contentSessionId: string, timeoutMs: number) — poll getSessionStatus every 2 seconds until queue is empty or timeout reachedsearch(query: string, project: string, limit: number) — GET search endpoint. Instrument with timing: record search_latency_ms from request start to response receivedBuild ingestion adapter:
evals/locomo/src/ingestion/adapter.tsgenerateContentSessionId(sampleId: string, sessionId: number) — returns deterministic ID like locomo-{sampleId}-s{sessionId}generateProjectName(sampleId: string) — returns locomo-eval-{sampleId} (one project per conversation for isolated search during QA)formatSessionAsToolExecution(conversation, session, enrichedSession) — transforms a LoCoMo session into worker API parameters:
toolName: "Read"toolInput: JSON.stringify({file_path: "conversation-transcript/session-" + sessionId + ".txt"})toolResponse: formatted dialog transcript like:
[Session {N} — {date}]
[Conversation between {speaker_a} and {speaker_b}]
{speaker_a}: {turn 1 text}
{speaker_b}: {turn 2 text}
...
userPrompt: "Conversation between {speaker_a} and {speaker_b} on {date}"Create and run prototype ingestion script for one conversation:
evals/locomo/scripts/ingest-one.tshttp://localhost:37777/api/health (or any known endpoint) — if connection refused, print a message asking the user to start the worker with bun plugin/scripts/worker-service.cjs start, then exit with code 1"Session {N}/{total} ingested — processing took {seconds}s"bun evals/locomo/scripts/ingest-one.tsVerify ingested data via search and display results:
evals/locomo/scripts/verify-ingestion.tsQ: {question text}
Category: {category}
Ground Truth: {answer}
Top Search Results:
1. {observation title} — {first 80 chars of narrative}
2. {observation title} — {first 80 chars of narrative}
3. {observation title} — {first 80 chars of narrative}
bun evals/locomo/scripts/verify-ingestion.ts