remem-mcp 0.5.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,264 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Render asciinema cast to MP4 directly using PIL.
4
+ No frame cap — renders at true 30fps.
5
+ """
6
+ import json, sys, os, re
7
+ from PIL import Image, ImageDraw, ImageFont
8
+
9
+ # Config
10
+ FPS = 30
11
+ FONT_SIZE = 18
12
+ COLS = 80
13
+ ROWS = 24
14
+ CHAR_W = 11 # approx for JetBrains Mono 18px
15
+ CHAR_H = 24
16
+ MARGIN = 20
17
+ WIDTH = COLS * CHAR_W + MARGIN * 2
18
+ HEIGHT = ROWS * CHAR_H + MARGIN * 2
19
+
20
+ # Monokai theme colors
21
+ BG = (39, 40, 34)
22
+ FG = (248, 248, 242)
23
+ COLORS = {
24
+ 30: (248, 248, 242), # black
25
+ 31: (255, 85, 85), # red
26
+ 32: (166, 226, 46), # green
27
+ 33: (230, 219, 116), # yellow
28
+ 34: (102, 217, 239), # blue
29
+ 35: (174, 129, 255), # magenta
30
+ 36: (161, 239, 239), # cyan
31
+ 37: (248, 248, 242), # white
32
+ 90: (150, 152, 150), # bright black (gray)
33
+ 91: (255, 85, 85),
34
+ 92: (166, 226, 46),
35
+ 93: (230, 219, 116),
36
+ 94: (102, 217, 239),
37
+ 95: (174, 129, 255),
38
+ 96: (161, 239, 239),
39
+ 97: (248, 248, 242),
40
+ }
41
+
42
+ def load_font():
43
+ # Menlo.ttc is a TrueType Collection — needs index parameter
44
+ # It has full box-drawing char support (┌─┐│└┘ etc.)
45
+ try:
46
+ return ImageFont.truetype("/System/Library/Fonts/Menlo.ttc", FONT_SIZE, index=0)
47
+ except:
48
+ pass
49
+ for p in [
50
+ "/System/Library/Fonts/SFNSMono.ttf",
51
+ "/System/Library/Fonts/Monaco.ttf",
52
+ "/Library/Fonts/JetBrains Mono Regular.ttf",
53
+ ]:
54
+ if os.path.exists(p):
55
+ try:
56
+ return ImageFont.truetype(p, FONT_SIZE)
57
+ except:
58
+ continue
59
+ return ImageFont.load_default()
60
+
61
+ font = load_font()
62
+ # Load bold variant for ANSI bold (code 1)
63
+ try:
64
+ font_bold = ImageFont.truetype("/System/Library/Fonts/Menlo.ttc", FONT_SIZE, index=1)
65
+ except:
66
+ font_bold = font
67
+
68
+ # Measure actual char width from font
69
+ try:
70
+ _bbox = font.getbbox("M")
71
+ CHAR_W = _bbox[2] - _bbox[0] + 1
72
+ _bbox_h = font.getbbox("M|")
73
+ CHAR_H = max(24, (_bbox_h[3] - _bbox_h[1]) + 8)
74
+ except:
75
+ pass
76
+
77
+ def parse_ansi(text):
78
+ """Parse ANSI escape sequences and return list of (char, color, bold) tuples.
79
+ Also returns control commands: ('clear',), ('home',), ('clearend',)"""
80
+ result = []
81
+ i = 0
82
+ color = FG
83
+ bold = False
84
+ while i < len(text):
85
+ if text[i] == '\x1b' and i + 1 < len(text) and text[i+1] == '[':
86
+ # Find end of escape sequence — could end in 'm' (SGR), 'J' (erase), 'H' (cursor), 'K', etc.
87
+ j = i + 2
88
+ while j < len(text) and text[j] not in 'mJHKABCDf':
89
+ j += 1
90
+ if j < len(text):
91
+ cmd = text[j]
92
+ params = text[i+2:j]
93
+ if cmd == 'm':
94
+ # SGR — color/bold
95
+ codes = params.split(';')
96
+ for code in codes:
97
+ if not code:
98
+ continue
99
+ c = int(code)
100
+ if c == 0:
101
+ color = FG
102
+ bold = False
103
+ elif c == 1:
104
+ bold = True
105
+ elif c in COLORS:
106
+ color = COLORS[c]
107
+ elif cmd == 'J':
108
+ # Erase display: 2J = clear all, 0J = clear from cursor to end
109
+ result.append(('clear', color, bold))
110
+ elif cmd == 'H':
111
+ # Cursor home
112
+ result.append(('home', color, bold))
113
+ elif cmd == 'K':
114
+ # Erase line: 0K = cursor to end, 2K = entire line
115
+ result.append(('clearend', color, bold))
116
+ # Ignore other cursor movement (A,B,C,D,f)
117
+ i = j + 1
118
+ continue
119
+ if text[i] == '\r':
120
+ # Carriage return — reset cursor to start of line (for counter animation)
121
+ result.append(('\r', color, bold))
122
+ i += 1
123
+ continue
124
+ if text[i] == '\n':
125
+ result.append(('\n', color, bold))
126
+ i += 1
127
+ continue
128
+ result.append((text[i], color, bold))
129
+ i += 1
130
+ return result
131
+
132
+ def render_frame(screen, cursor_row, cursor_col):
133
+ """Render screen state to PIL Image."""
134
+ img = Image.new('RGB', (WIDTH, HEIGHT), BG)
135
+ draw = ImageDraw.Draw(img)
136
+
137
+ for row_idx, row in enumerate(screen[:ROWS]):
138
+ col_idx = 0
139
+ for char, color, bold in row:
140
+ if char == '\n':
141
+ break
142
+ if col_idx >= COLS:
143
+ break
144
+ x = MARGIN + col_idx * CHAR_W
145
+ y = MARGIN + row_idx * CHAR_H
146
+ if char != ' ':
147
+ f = font_bold if bold else font
148
+ draw.text((x, y), char, fill=color, font=f)
149
+ col_idx += 1
150
+
151
+ return img
152
+
153
+ def update_screen(screen, data, cursor_row, cursor_col):
154
+ """Update screen buffer with new data."""
155
+ chars = parse_ansi(data)
156
+ for item in chars:
157
+ char, color, bold = item[0], item[1], item[2]
158
+ # Handle control commands
159
+ if char == 'clear':
160
+ for r in range(ROWS):
161
+ screen[r] = []
162
+ cursor_row = 0
163
+ cursor_col = 0
164
+ continue
165
+ if char == 'home':
166
+ cursor_row = 0
167
+ cursor_col = 0
168
+ continue
169
+ if char == 'clearend':
170
+ if cursor_row < ROWS:
171
+ screen[cursor_row] = screen[cursor_row][:cursor_col]
172
+ continue
173
+ if char == '\n':
174
+ cursor_row += 1
175
+ cursor_col = 0
176
+ if cursor_row >= ROWS:
177
+ screen.pop(0)
178
+ screen.append([])
179
+ cursor_row = ROWS - 1
180
+ continue
181
+ if char == '\r':
182
+ cursor_col = 0
183
+ continue
184
+ if char == '\x08': # backspace
185
+ cursor_col = max(0, cursor_col - 1)
186
+ continue
187
+ # Ensure row exists
188
+ while len(screen) <= cursor_row:
189
+ screen.append([])
190
+ # Ensure col exists
191
+ while len(screen[cursor_row]) <= cursor_col:
192
+ screen[cursor_row].append((' ', FG, False))
193
+ # Set char
194
+ screen[cursor_row][cursor_col] = (char, color, bold)
195
+ cursor_col += 1
196
+ if cursor_col >= COLS:
197
+ cursor_row += 1
198
+ cursor_col = 0
199
+ if cursor_row >= ROWS:
200
+ screen.pop(0)
201
+ screen.append([])
202
+ cursor_row = ROWS - 1
203
+ return cursor_row, cursor_col
204
+
205
+ def main():
206
+ cast_path = sys.argv[1]
207
+ output_path = sys.argv[2]
208
+
209
+ with open(cast_path) as f:
210
+ lines = f.readlines()
211
+
212
+ events = []
213
+ for line in lines[1:]:
214
+ line = line.strip()
215
+ if not line: continue
216
+ parts = json.loads(line)
217
+ if isinstance(parts, list) and len(parts) >= 3 and parts[1] == 'o':
218
+ events.append((parts[0], parts[2]))
219
+
220
+ total_duration = sum(t for t, _ in events)
221
+ total_frames = int(total_duration * FPS)
222
+ print(f"Duration: {total_duration:.1f}s, Frames: {total_frames}, FPS: {FPS}")
223
+
224
+ # Process events and generate frames
225
+ screen = [[] for _ in range(ROWS)]
226
+ cursor_row, cursor_col = 0, 0
227
+
228
+ frames = []
229
+ event_idx = 0
230
+ current_time = 0.0
231
+ frame_time = 1.0 / FPS
232
+
233
+ for frame_num in range(total_frames):
234
+ # Process all events that occur before this frame
235
+ while event_idx < len(events) and current_time >= sum(e[0] for e in events[:event_idx+1]):
236
+ delay, data = events[event_idx]
237
+ cursor_row, cursor_col = update_screen(screen, data, cursor_row, cursor_col)
238
+ event_idx += 1
239
+
240
+ # Render frame
241
+ img = render_frame(screen, cursor_row, cursor_col)
242
+ frames.append(img)
243
+ current_time += frame_time
244
+
245
+ if frame_num % 100 == 0:
246
+ print(f" Frame {frame_num}/{total_frames}...")
247
+
248
+ # Save as MP4 using ffmpeg
249
+ print(f"Saving {len(frames)} frames to {output_path}...")
250
+ # Save frames as PNG sequence first
251
+ tmp_dir = "/tmp/demo-frames"
252
+ os.makedirs(tmp_dir, exist_ok=True)
253
+ for i, frame in enumerate(frames):
254
+ frame.save(f"{tmp_dir}/frame_{i:05d}.png")
255
+
256
+ # Use ffmpeg to create MP4
257
+ os.system(f"ffmpeg -y -framerate {FPS} -i {tmp_dir}/frame_%05d.png -c:v libx264 -preset fast -pix_fmt yuv420p -movflags faststart {output_path} 2>/dev/null")
258
+
259
+ # Cleanup
260
+ os.system(f"rm -rf {tmp_dir}")
261
+ print(f"Done: {output_path}")
262
+
263
+ if __name__ == "__main__":
264
+ main()
@@ -0,0 +1,179 @@
1
+ #!/bin/bash
2
+ # loop-bench.sh — Autonomous loop: run benchmark → if fail, call devin -p → repeat.
3
+ # Usage: ./scripts/loop-bench.sh [max_iterations]
4
+ #
5
+ # No LoopX. File-based resume via last-iter.txt.
6
+ #
7
+ # Requirements:
8
+ # - devin CLI on PATH
9
+ # - AMB repo at /tmp/amb-repo
10
+ # - LoCoMo data at /tmp/locomo/data/locomo10.json (optional)
11
+
12
+ set -uo pipefail
13
+
14
+ # ─── Config ────────────────────────────────────────────────────
15
+ MAX_ITER="${1:-50}"
16
+ PROJECT_ROOT="/data/projects/tdai-memory-mcp"
17
+ BENCH_SCRIPT="$PROJECT_ROOT/scripts/bench-all.sh"
18
+ LOG_DIR="/tmp/bench-loop-logs"
19
+ PROMPT_FILE="/tmp/bench-loop-prompt.md"
20
+ LAST_ITER_FILE="$LOG_DIR/last-iter.txt"
21
+
22
+ # Targets
23
+ export TARGET_L1="${TARGET_L1:-100}"
24
+ export TARGET_L2="${TARGET_L2:-100}"
25
+ export TARGET_L3="${TARGET_L3:-100}"
26
+ export TARGET_LOCOMO="${TARGET_LOCOMO:-76}"
27
+ export TARGET_PERSONAMEM="${TARGET_PERSONAMEM:-76}"
28
+
29
+ mkdir -p "$LOG_DIR"
30
+ log() { echo "[loop] $(date '+%Y-%m-%d %H:%M:%S') $*"; }
31
+
32
+ # ─── Resume logic (file-based) ─────────────────────────────────
33
+ START_ITER=1
34
+ if [ -f "$LAST_ITER_FILE" ]; then
35
+ START_ITER=$(($(cat "$LAST_ITER_FILE") + 1))
36
+ log "Resuming from iteration $START_ITER"
37
+ fi
38
+
39
+ # ─── Main loop ─────────────────────────────────────────────────
40
+ for iter in $(seq $START_ITER $MAX_ITER); do
41
+ echo "$iter" > "$LAST_ITER_FILE"
42
+ log "══════════════════════════════════════════════════════"
43
+ log " ITERATION $iter / $MAX_ITER"
44
+ log "══════════════════════════════════════════════════════"
45
+
46
+ # 1. Run benchmark (full mode — includes LoCoMo + PersonaMem)
47
+ BENCH_LOG="$LOG_DIR/iter-${iter}-bench.log"
48
+ log "Running benchmark..."
49
+ if bash "$BENCH_SCRIPT" > "$BENCH_LOG" 2>&1; then
50
+ log "ALL PASS ✓"
51
+ RESULT_LINE=$(grep "BENCH_RESULT" "$BENCH_LOG" || echo "")
52
+ log "$RESULT_LINE"
53
+ log "Done."
54
+ exit 0
55
+ fi
56
+
57
+ # 2. Parse scores
58
+ RESULT_LINE=$(grep "BENCH_RESULT" "$BENCH_LOG" || echo "BENCH_RESULT L1=0 L2=0 L3=0 LOCOMO=N/A PERSONAMEM=N/A")
59
+ L1=$(echo "$RESULT_LINE" | grep -oP 'L1=\K[0-9]+' || echo "0")
60
+ L2=$(echo "$RESULT_LINE" | grep -oP 'L2=\K[0-9]+' || echo "0")
61
+ L3=$(echo "$RESULT_LINE" | grep -oP 'L3=\K[0-9]+' || echo "0")
62
+ LOCOMO=$(echo "$RESULT_LINE" | grep -oP 'LOCOMO=\K[0-9]+' || echo "N/A")
63
+ PERSONAMEM=$(echo "$RESULT_LINE" | grep -oP 'PERSONAMEM=\K[0-9]+' || echo "N/A")
64
+
65
+ log "Scores: L1=$L1 L2=$L2 L3=$L3 LOCOMO=$LOCOMO PERSONAMEM=$PERSONAMEM"
66
+
67
+ # 3. Extract failures
68
+ FAILURES=$(grep "^FAIL:" "$BENCH_LOG" || echo "")
69
+ if [ -z "$FAILURES" ]; then
70
+ FAILURES="Benchmark exited non-zero. Check $BENCH_LOG."
71
+ fi
72
+
73
+ # 4. Extract detailed test failures
74
+ AMB_FAILURES=$(grep -E "FAIL|✗|failed|incorrect|wrong|Expected|Got" "$BENCH_LOG" | head -30 || echo "")
75
+
76
+ # 5. Find lowest-scoring layer to focus Devin
77
+ LOWEST_LAYER="L2"
78
+ LOWEST_SCORE=$L2
79
+ if [ "$L3" -lt "$LOWEST_SCORE" ] 2>/dev/null; then LOWEST_LAYER="L3"; LOWEST_SCORE=$L3; fi
80
+ if [ "$LOCOMO" != "N/A" ] && [ "$LOCOMO" -lt "$LOWEST_SCORE" ] 2>/dev/null; then LOWEST_LAYER="LOCOMO"; LOWEST_SCORE=$LOCOMO; fi
81
+ if [ "$PERSONAMEM" != "N/A" ] && [ "$PERSONAMEM" -lt "$LOWEST_SCORE" ] 2>/dev/null; then LOWEST_LAYER="PERSONAMEM"; LOWEST_SCORE=$PERSONAMEM; fi
82
+
83
+ # 6. Build prompt for Devin
84
+ cat > "$PROMPT_FILE" << EOF
85
+ # Benchmark Fix Task — Iteration $iter
86
+
87
+ You are working on tdai-memory-mcp at $PROJECT_ROOT.
88
+ Goal: Pass ALL benchmarks with scores >= TencentDB.
89
+
90
+ ## Current Scores vs Targets
91
+ | Benchmark | Score | Target |
92
+ |------------|-------|--------|
93
+ | AMB L1 | $L1 | $TARGET_L1 |
94
+ | AMB L2 | $L2 | $TARGET_L2 |
95
+ | AMB L3 | $L3 | $TARGET_L3 |
96
+ | LoCoMo | $LOCOMO | $TARGET_LOCOMO |
97
+ | PersonaMem | $PERSONAMEM | $TARGET_PERSONAMEM |
98
+
99
+ ## Focus: $LOWEST_LAYER is lowest at $LOWEST_SCORE / 100
100
+
101
+ ## Failures
102
+ $FAILURES
103
+
104
+ ## Detailed Test Failures
105
+ $AMB_FAILURES
106
+
107
+ ## Full Benchmark Log
108
+ Read: $BENCH_LOG
109
+
110
+ ## Your Task
111
+ 1. Read $BENCH_LOG to see exactly which tests failed and why.
112
+ 2. Read the relevant source code in $PROJECT_ROOT/src/.
113
+ 3. Fix the root cause. Make minimal, surgical changes.
114
+ 4. Run \`npm run build\` — must pass.
115
+ 5. Run \`npm test\` — must pass.
116
+ 6. Do NOT break existing passing tests.
117
+ 7. Focus on $LOWEST_LAYER first (lowest score).
118
+
119
+ ## Architecture Context
120
+ - Memory MCP server with hybrid search: BM25 + vector (sqlite-vec) + RRF fusion.
121
+ - AMB L1 = basic recall (56 tests, 8 categories).
122
+ - AMB L2 = multi-session scenarios (5 scenarios).
123
+ - AMB L3 = scale testing (1K+ memories, distractors).
124
+ - LoCoMo = long conversation QA (19 sessions, 400+ turns, multi-hop questions).
125
+ - PersonaMem = personalization benchmark (588 questions, 20 personas, multiple-choice QA).
126
+ TencentDB scores 76% on PersonaMem. Our adapter at /tmp/personamem/personamem-bench.ts
127
+ ingests conversation context into tdai-memory-mcp, then searches with the question
128
+ and checks if search results contain unique keywords from the correct answer.
129
+ Current score: $PERSONAMEM/100. Target: 76.
130
+ - AtomPipeline exists but only runs for decision/learning/error — NOT conversation.
131
+ - Search does NOT join atoms table — atoms are stored but never searched.
132
+ - TencentDB uses L1/L2/L3 extraction pipeline: extract facts → scenarios → knowledge graph.
133
+ - To beat TencentDB: implement fact extraction for conversations + atom-aware search.
134
+
135
+ ## Key Files
136
+ - src/pipeline/atom.ts — L1 fact extraction (extend to conversation type)
137
+ - src/storage/sqlite.ts — search + vector + RRF (add atom search)
138
+ - src/server.ts — recall/search handlers (join atoms into results)
139
+ - src/pipeline/types.ts — pipeline interfaces
140
+ - /tmp/personamem/personamem-bench.ts — PersonaMem adapter (scoring logic)
141
+ - /tmp/personamem/data/questions_32k.csv — PersonaMem questions
142
+ - /tmp/personamem/data/shared_contexts_32k.jsonl — PersonaMem contexts
143
+
144
+ ## Rules
145
+ - ONE focused fix per iteration.
146
+ - No unrelated refactoring.
147
+ - Run build + tests before finishing.
148
+ - If you're stuck after 3 attempts at the same fix, try a different approach.
149
+ EOF
150
+
151
+ # 7. Call Devin to fix
152
+ DEVIN_LOG="$LOG_DIR/iter-${iter}-devin.log"
153
+ log "Calling Devin (dangerous mode) to fix $LOWEST_LAYER..."
154
+ log "Prompt: $PROMPT_FILE"
155
+ log "Devin log: $DEVIN_LOG"
156
+
157
+ devin -p "$(cat "$PROMPT_FILE")" \
158
+ --permission-mode dangerous \
159
+ > "$DEVIN_LOG" 2>&1 || true
160
+
161
+ log "Devin finished."
162
+
163
+ # 8. Summary
164
+ DEVIN_SUMMARY=$(tail -5 "$DEVIN_LOG" 2>/dev/null || echo "no output")
165
+ log "Devin output (last 5 lines):"
166
+ echo "$DEVIN_SUMMARY" | while read -r line; do log " $line"; done
167
+
168
+ log "Iteration $iter complete."
169
+ log ""
170
+ done
171
+
172
+ # ─── Max iterations reached ────────────────────────────────────
173
+ log "══════════════════════════════════════════════════════"
174
+ log " MAX ITERATIONS ($MAX_ITER) REACHED — target not met"
175
+ log "══════════════════════════════════════════════════════"
176
+ log "Last scores: L1=$L1 L2=$L2 L3=$L3 LOCOMO=$LOCOMO PERSONAMEM=$PERSONAMEM"
177
+ log "Logs: $LOG_DIR/"
178
+ log "Resume: bash scripts/loop-bench.sh $MAX_ITER"
179
+ exit 1
@@ -0,0 +1,159 @@
1
+ #!/bin/bash
2
+ # loop-longmemeval.sh — Autonomous loop: run LongMemEval → if fail, call devin -p → repeat.
3
+ # Usage: ./scripts/loop-longmemeval.sh [max_iterations] [--variant oracle|s]
4
+ #
5
+ # File-based resume via last-iter.txt.
6
+
7
+ set -uo pipefail
8
+
9
+ # ─── Config ────────────────────────────────────────────────────
10
+ MAX_ITER="${1:-30}"
11
+ VARIANT="${2:-oracle}"
12
+ PROJECT_ROOT="/data/projects/tdai-memory-mcp"
13
+ BENCH_SCRIPT="/tmp/longmemeval/longmemeval-bench.ts"
14
+ LOG_DIR="/tmp/longmemeval-loop-logs"
15
+ PROMPT_FILE="/tmp/longmemeval-loop-prompt.md"
16
+ LAST_ITER_FILE="$LOG_DIR/last-iter.txt"
17
+ SAMPLE_SIZE="${SAMPLE_SIZE:-100}"
18
+ TARGET="${TARGET_LONGMEMEVAL:-80}"
19
+
20
+ mkdir -p "$LOG_DIR"
21
+ log() { echo "[loop] $(date '+%Y-%m-%d %H:%M:%S') $*"; }
22
+
23
+ # ─── Resume logic ──────────────────────────────────────────────
24
+ START_ITER=1
25
+ if [ -f "$LAST_ITER_FILE" ]; then
26
+ START_ITER=$(($(cat "$LAST_ITER_FILE") + 1))
27
+ log "Resuming from iteration $START_ITER"
28
+ fi
29
+
30
+ # ─── Main loop ─────────────────────────────────────────────────
31
+ for iter in $(seq $START_ITER $MAX_ITER); do
32
+ echo "$iter" > "$LAST_ITER_FILE"
33
+ log "══════════════════════════════════════════════════════"
34
+ log " ITERATION $iter / $MAX_ITER (variant: $VARIANT)"
35
+ log "══════════════════════════════════════════════════════"
36
+
37
+ # 1. Run benchmark
38
+ BENCH_LOG="$LOG_DIR/iter-${iter}-bench.log"
39
+ log "Running LongMemEval benchmark (sample=$SAMPLE_SIZE, variant=$VARIANT)..."
40
+ cd /tmp/longmemeval
41
+ npx tsx longmemeval-bench.ts --sample=$SAMPLE_SIZE --variant=$VARIANT > "$BENCH_LOG" 2>&1
42
+ BENCH_EXIT=$?
43
+
44
+ # 2. Parse score
45
+ SCORE=$(grep "LONGMEMEVAL_SCORE" "$BENCH_LOG" | grep -oP '\d+' || echo "0")
46
+ log "LongMemEval score: $SCORE / 100 (target: $TARGET)"
47
+
48
+ # 3. Check pass
49
+ if [ "$SCORE" -ge "$TARGET" ]; then
50
+ log "ALL PASS ✓ — LongMemEval=$SCORE >= $TARGET"
51
+ grep "LONGMEMEVAL_SCORE" "$BENCH_LOG"
52
+ log "Done."
53
+ exit 0
54
+ fi
55
+
56
+ # 4. Extract failures
57
+ FAILURES=$(grep "✗" "$BENCH_LOG" | head -20 || echo "")
58
+ BY_TYPE=$(grep "=" "$BENCH_LOG" | grep "%" || echo "")
59
+ RESULT_DETAIL=$(python3 -c "
60
+ import json
61
+ try:
62
+ data = json.load(open('/tmp/longmemeval/results.json'))
63
+ by_type = data.get('byType', {})
64
+ for t, s in sorted(by_type.items()):
65
+ pct = round(s['correct'] / s['total'] * 100) if s['total'] else 0
66
+ print(f' {t}: {s[\"correct\"]}/{s[\"total\"]} = {pct}%')
67
+ # Show failed questions
68
+ failed = [r for r in data.get('results', []) if not r['predicted_correct']]
69
+ print(f'\\nFailed questions ({len(failed)}):')
70
+ for f in failed[:10]:
71
+ print(f' [{f[\"question_type\"]}] Q: {f[\"question\"][:80]}')
72
+ print(f' A: {f[\"answer\"][:80]}')
73
+ print(f' Matched: {f[\"matched_keywords\"]}')
74
+ print()
75
+ except Exception as e:
76
+ print(f'Error: {e}')
77
+ " 2>/dev/null || echo "Could not parse results")
78
+
79
+ # 5. Build prompt
80
+ cat > "$PROMPT_FILE" << EOF
81
+ # LongMemEval Fix Task — Iteration $iter
82
+
83
+ You are working on tdai-memory-mcp at $PROJECT_ROOT.
84
+ Goal: Pass LongMemEval benchmark with score >= $TARGET.
85
+
86
+ ## Current Score
87
+ LongMemEval ($VARIANT variant): $SCORE / 100 (target: $TARGET)
88
+
89
+ ## Score by Question Type
90
+ $BY_TYPE
91
+
92
+ ## Failed Questions Detail
93
+ $RESULT_DETAIL
94
+
95
+ ## Full Benchmark Log
96
+ Read: $BENCH_LOG
97
+
98
+ ## Your Task
99
+ 1. Read $BENCH_LOG to see which questions failed.
100
+ 2. Read /tmp/longmemeval/results.json for detailed results.
101
+ 3. Read the benchmark adapter at /tmp/longmemeval/longmemeval-bench.ts to understand scoring.
102
+ 4. Read the relevant source code in $PROJECT_ROOT/src/.
103
+ 5. Fix the root cause — improve search recall or scoring logic.
104
+ 6. Run \`npm run build\` — must pass.
105
+ 7. Run \`npm test\` — must pass.
106
+ 8. Do NOT break existing passing tests.
107
+
108
+ ## Architecture Context
109
+ - Memory MCP server with hybrid search: BM25 + vector (sqlite-vec) + RRF fusion.
110
+ - LongMemEval tests 5 long-term memory abilities:
111
+ - temporal-reasoning: when did something happen (133 questions)
112
+ - multi-session: combine info across sessions (133 questions)
113
+ - knowledge-update: track changed preferences (78 questions)
114
+ - single-session-user: recall user facts (70 questions)
115
+ - single-session-assistant: recall assistant suggestions (56 questions)
116
+ - single-session-preference: recall preferences (30 questions)
117
+ - Each question has haystack_sessions (chat history) + answer (ground truth).
118
+ - Adapter ingests sessions, searches with question, checks if search results
119
+ contain keywords from the ground-truth answer.
120
+ - Scoring: correct if >= 40% of answer keywords found in top-5 search results.
121
+
122
+ ## Key Files
123
+ - src/storage/sqlite.ts — search + vector + RRF (improve recall)
124
+ - src/server.ts — search handler (check query expansion, filtering)
125
+ - src/pipeline/atom.ts — fact extraction (may help recall)
126
+ - /tmp/longmemeval/longmemeval-bench.ts — benchmark adapter (scoring logic)
127
+ - /tmp/longmemeval/data/longmemeval_${VARIANT} — dataset
128
+
129
+ ## Rules
130
+ - ONE focused fix per iteration.
131
+ - No unrelated refactoring.
132
+ - Run build + tests before finishing.
133
+ - If stuck after 3 attempts, try a different approach.
134
+ - You can modify the benchmark adapter scoring logic AND the search engine.
135
+ EOF
136
+
137
+ # 6. Call Devin
138
+ DEVIN_LOG="$LOG_DIR/iter-${iter}-devin.log"
139
+ log "Calling Devin to fix LongMemEval (score=$SCORE < $TARGET)..."
140
+ cd "$PROJECT_ROOT"
141
+ devin -p "$(cat "$PROMPT_FILE")" \
142
+ --permission-mode dangerous \
143
+ > "$DEVIN_LOG" 2>&1 || true
144
+
145
+ log "Devin finished."
146
+ tail -5 "$DEVIN_LOG" 2>/dev/null | while read -r line; do log " $line"; done
147
+
148
+ log "Iteration $iter complete."
149
+ echo ""
150
+ done
151
+
152
+ # ─── Max iterations ────────────────────────────────────────────
153
+ log "══════════════════════════════════════════════════════"
154
+ log " MAX ITERATIONS ($MAX_ITER) REACHED — target $TARGET not met"
155
+ log "══════════════════════════════════════════════════════"
156
+ log "Last score: $SCORE / 100"
157
+ log "Logs: $LOG_DIR/"
158
+ log "Resume: bash scripts/loop-longmemeval.sh $MAX_ITER $VARIANT"
159
+ exit 1