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.
- package/LICENSE +21 -0
- package/README.md +235 -0
- package/dist/chunk-34TEZ5U4.js +1676 -0
- package/dist/chunk-34TEZ5U4.js.map +1 -0
- package/dist/chunk-RITPZHIB.js +475 -0
- package/dist/chunk-RITPZHIB.js.map +1 -0
- package/dist/engine-ZNZRRSBV.js +25 -0
- package/dist/engine-ZNZRRSBV.js.map +1 -0
- package/dist/index.js +12625 -0
- package/dist/index.js.map +1 -0
- package/dist/sdk.js +41 -0
- package/dist/sdk.js.map +1 -0
- package/dist/storage/schema.sql +292 -0
- package/package.json +82 -0
- package/scripts/bench-all.sh +147 -0
- package/scripts/cast-to-mp4.py +264 -0
- package/scripts/loop-bench.sh +179 -0
- package/scripts/loop-longmemeval.sh +159 -0
- package/scripts/loop-mem0-bench.sh +310 -0
- package/scripts/merge-casts.py +131 -0
- package/scripts/postinstall.js +51 -0
- package/skills/remem-mcp/SKILL.md +562 -0
|
@@ -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
|