loki-mode 6.60.0 → 6.62.0

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.
Files changed (64) hide show
  1. package/SKILL.md +2 -2
  2. package/VERSION +1 -1
  3. package/autonomy/app-runner.sh +34 -8
  4. package/autonomy/completion-council.sh +70 -32
  5. package/autonomy/issue-parser.sh +4 -7
  6. package/autonomy/loki +238 -119
  7. package/autonomy/notification-checker.py +49 -23
  8. package/autonomy/run.sh +162 -79
  9. package/autonomy/sandbox.sh +91 -24
  10. package/bin/loki-mode.js +1 -2
  11. package/bin/postinstall.js +10 -4
  12. package/dashboard/__init__.py +1 -1
  13. package/dashboard/control.py +46 -36
  14. package/dashboard/database.py +21 -4
  15. package/dashboard/server.py +107 -78
  16. package/docs/BUG-AUDIT-v6.61.0.md +957 -0
  17. package/docs/INSTALLATION.md +2 -2
  18. package/events/bus.py +129 -28
  19. package/events/bus.ts +41 -27
  20. package/events/emit.sh +1 -1
  21. package/integrations/openclaw/README.md +139 -0
  22. package/integrations/openclaw/SKILL.md +88 -0
  23. package/integrations/openclaw/bridge/__init__.py +1 -0
  24. package/integrations/openclaw/bridge/__main__.py +88 -0
  25. package/integrations/openclaw/bridge/schema_map.py +180 -0
  26. package/integrations/openclaw/bridge/watcher.py +100 -0
  27. package/integrations/openclaw/scripts/format-progress.sh +80 -0
  28. package/integrations/openclaw/scripts/poll-status.sh +74 -0
  29. package/integrations/vibe-kanban.md +289 -0
  30. package/mcp/__init__.py +1 -1
  31. package/mcp/server.py +96 -73
  32. package/memory/consolidation.py +21 -6
  33. package/memory/engine.py +53 -26
  34. package/memory/layers/index_layer.py +16 -3
  35. package/memory/layers/timeline_layer.py +16 -3
  36. package/memory/retrieval.py +4 -1
  37. package/memory/schemas.py +4 -2
  38. package/memory/storage.py +25 -4
  39. package/memory/token_economics.py +9 -2
  40. package/memory/vector_index.py +2 -2
  41. package/package.json +3 -1
  42. package/providers/cline.sh +5 -4
  43. package/providers/codex.sh +27 -5
  44. package/providers/gemini.sh +59 -23
  45. package/providers/loader.sh +3 -2
  46. package/skills/parallel-workflows.md +9 -7
  47. package/state/__init__.py +10 -0
  48. package/state/index.ts +18 -0
  49. package/state/manager.py +1801 -0
  50. package/state/manager.ts +1774 -0
  51. package/state/sqlite_backend.py +188 -0
  52. package/state/test_manager.py +703 -0
  53. package/state/test_manager.ts +366 -0
  54. package/templates/README.md +19 -4
  55. package/templates/dashboard.md +45 -0
  56. package/templates/data-pipeline.md +45 -0
  57. package/templates/game.md +48 -0
  58. package/templates/microservice.md +49 -0
  59. package/templates/npm-library.md +42 -0
  60. package/templates/rest-api.md +170 -33
  61. package/templates/slack-bot.md +48 -0
  62. package/templates/web-scraper.md +45 -0
  63. package/web-app/server.py +360 -191
  64. package/templates/saas-app.md +0 -42
@@ -0,0 +1,289 @@
1
+ # Vibe Kanban Integration
2
+
3
+ Loki Mode can optionally integrate with [Vibe Kanban](https://github.com/BloopAI/vibe-kanban) to provide a visual dashboard for monitoring autonomous execution.
4
+
5
+ ## Why Use Vibe Kanban with Loki Mode?
6
+
7
+ | Feature | Loki Mode Alone | + Vibe Kanban |
8
+ |---------|-----------------|---------------|
9
+ | Task visualization | File-based queues | Visual kanban board |
10
+ | Progress monitoring | Log files | Real-time dashboard |
11
+ | Manual intervention | Edit queue files | Drag-and-drop tasks |
12
+ | Code review | Automated 3-reviewer | + Visual diff review |
13
+ | Parallel agents | Background subagents | Isolated git worktrees |
14
+
15
+ ## Quick Start Guide
16
+
17
+ ### Step 1: Start Vibe Kanban
18
+
19
+ ```bash
20
+ npx vibe-kanban
21
+ ```
22
+
23
+ This will:
24
+ - Start the Vibe Kanban server
25
+ - Automatically open the UI in your browser
26
+ - Keep the server running (leave this terminal open)
27
+
28
+ ### Step 2: Run Loki Mode
29
+
30
+ Open a NEW terminal in your project directory:
31
+
32
+ ```bash
33
+ # Option A: Using Autonomy Runner (Recommended)
34
+ ./autonomy/run.sh ./prd.md
35
+
36
+ # Option B: Manual Mode via Claude Code
37
+ claude --dangerously-skip-permissions
38
+ # Then: "Loki Mode with PRD at ./prd.md"
39
+ ```
40
+
41
+ ### Step 3: Sync Tasks to Vibe Kanban (One Script)
42
+
43
+ Open another terminal in the same project directory:
44
+
45
+ ```bash
46
+ # One-time sync
47
+ ./scripts/sync-to-vibe-kanban.sh
48
+
49
+ # Or continuous sync (watches for changes)
50
+ ./scripts/vibe-sync-watcher.sh
51
+ ```
52
+
53
+ You should see output like:
54
+ ```
55
+ [INFO] Project: your-project-name
56
+ [INFO] Path: /Users/username/git/your-project
57
+ [INFO] Database: /Users/username/Library/Application Support/ai.bloop.vibe-kanban/db.sqlite
58
+ [INFO] Project ID: A1B2C3D4E5F6...
59
+ [INFO] Phase: DEVELOPMENT
60
+ [INFO] pending: 5 tasks
61
+ [INFO] in-progress: 3 tasks
62
+ [INFO] Synced 8 tasks to Vibe Kanban
63
+ ```
64
+
65
+ ### Step 4: View Tasks in Vibe Kanban
66
+
67
+ Tasks appear immediately in Vibe Kanban (no refresh needed). All synced tasks have `[Loki]` prefix for identification.
68
+
69
+ ## Setup
70
+
71
+ ### 1. Install Vibe Kanban
72
+
73
+ ```bash
74
+ npx vibe-kanban
75
+ ```
76
+
77
+ ### 2. Enable Integration in Loki Mode
78
+
79
+ Set environment variable before running:
80
+
81
+ ```bash
82
+ export LOKI_VIBE_KANBAN=true
83
+ ./scripts/loki-wrapper.sh ./docs/requirements.md
84
+ ```
85
+
86
+ Or create `.loki/config/integrations.yaml`:
87
+
88
+ ```yaml
89
+ vibe-kanban:
90
+ enabled: true
91
+ sync_interval: 30 # seconds
92
+ export_path: ~/.vibe-kanban/loki-tasks/
93
+ ```
94
+
95
+ ## How It Works
96
+
97
+ ### Direct SQLite Sync (v2.37.1+)
98
+
99
+ The sync script writes directly to Vibe Kanban's SQLite database:
100
+
101
+ ```
102
+ Loki Mode (.loki/queue/) sync-to-vibe-kanban.sh Vibe Kanban (SQLite)
103
+ │ │ │
104
+ ├─ pending.json ────────────►├─────────────────────────►│ todo
105
+ ├─ in-progress.json ────────►├─────────────────────────►│ inprogress
106
+ ├─ completed.json ──────────►├─────────────────────────►│ done
107
+ └─ failed.json ─────────────►├─────────────────────────►│ cancelled
108
+ ```
109
+
110
+ **Database Location:**
111
+ - macOS: `~/Library/Application Support/ai.bloop.vibe-kanban/db.sqlite`
112
+ - Linux: `~/.local/share/ai.bloop.vibe-kanban/db.sqlite` or `~/.config/ai.bloop.vibe-kanban/db.sqlite`
113
+
114
+ ### Status Mapping
115
+
116
+ | Loki Status | Vibe Kanban Status |
117
+ |-------------|-------------------|
118
+ | pending | todo |
119
+ | in-progress | inprogress |
120
+ | completed | done |
121
+ | failed | cancelled |
122
+
123
+ ### Task Identification
124
+
125
+ All synced tasks use `[Loki]` prefix in title for safe identification. On each sync:
126
+ 1. Delete all `[Loki]` tasks for the project
127
+ 2. Re-insert current tasks from queue files
128
+
129
+ This ensures clean sync without duplicates.
130
+
131
+ ## Export Script
132
+
133
+ Add this to export Loki Mode tasks to Vibe Kanban:
134
+
135
+ ```bash
136
+ #!/bin/bash
137
+ # scripts/export-to-vibe-kanban.sh
138
+
139
+ LOKI_DIR=".loki"
140
+ EXPORT_DIR="${VIBE_KANBAN_DIR:-~/.vibe-kanban/loki-tasks}"
141
+
142
+ mkdir -p "$EXPORT_DIR"
143
+
144
+ # Export pending tasks
145
+ if [ -f "$LOKI_DIR/queue/pending.json" ]; then
146
+ python3 << EOF
147
+ import json
148
+ import os
149
+
150
+ with open("$LOKI_DIR/queue/pending.json") as f:
151
+ tasks = json.load(f)
152
+
153
+ export_dir = os.path.expanduser("$EXPORT_DIR")
154
+
155
+ for task in tasks:
156
+ vibe_task = {
157
+ "id": f"loki-{task['id']}",
158
+ "title": task.get('payload', {}).get('description', task['type']),
159
+ "description": json.dumps(task.get('payload', {}), indent=2),
160
+ "status": "todo",
161
+ "agent": "claude-code",
162
+ "tags": [task['type'], f"priority-{task.get('priority', 5)}"],
163
+ "metadata": {
164
+ "lokiTaskId": task['id'],
165
+ "lokiType": task['type'],
166
+ "createdAt": task.get('createdAt', '')
167
+ }
168
+ }
169
+
170
+ with open(f"{export_dir}/{task['id']}.json", 'w') as out:
171
+ json.dump(vibe_task, out, indent=2)
172
+
173
+ print(f"Exported {len(tasks)} tasks to {export_dir}")
174
+ EOF
175
+ fi
176
+ ```
177
+
178
+ ## Real-Time Sync (Advanced)
179
+
180
+ For real-time sync, run the watcher alongside Loki Mode:
181
+
182
+ ```bash
183
+ #!/bin/bash
184
+ # scripts/vibe-sync-watcher.sh
185
+
186
+ LOKI_DIR=".loki"
187
+
188
+ # Watch for queue changes and sync
189
+ while true; do
190
+ # Use fswatch on macOS, inotifywait on Linux
191
+ if command -v fswatch &> /dev/null; then
192
+ fswatch -1 "$LOKI_DIR/queue/"
193
+ else
194
+ inotifywait -e modify,create "$LOKI_DIR/queue/" 2>/dev/null
195
+ fi
196
+
197
+ ./scripts/export-to-vibe-kanban.sh
198
+ sleep 2
199
+ done
200
+ ```
201
+
202
+ ## Benefits of Combined Usage
203
+
204
+ ### 1. Visual Progress Tracking
205
+ See all active Loki agents as tasks moving across your kanban board.
206
+
207
+ ### 2. Safe Isolation
208
+ Vibe Kanban runs each agent in isolated git worktrees, perfect for Loki's parallel development.
209
+
210
+ ### 3. Human-in-the-Loop Option
211
+ Pause autonomous execution, review changes visually, then resume.
212
+
213
+ ### 4. Multi-Project Dashboard
214
+ If running Loki Mode on multiple projects, see all in one Vibe Kanban instance.
215
+
216
+ ## Comparison: When to Use What
217
+
218
+ | Scenario | Recommendation |
219
+ |----------|----------------|
220
+ | Fully autonomous, no monitoring | Loki Mode + Wrapper only |
221
+ | Need visual progress dashboard | Add Vibe Kanban |
222
+ | Want manual task prioritization | Use Vibe Kanban to reorder |
223
+ | Code review before merge | Use Vibe Kanban's diff viewer |
224
+ | Multiple concurrent PRDs | Vibe Kanban for project switching |
225
+
226
+ ## Troubleshooting
227
+
228
+ ### Issue: "Exported 0 tasks total"
229
+
230
+ **Cause:** No tasks in `.loki/queue/` yet.
231
+
232
+ **Solutions:**
233
+ 1. Make sure Loki Mode is actually running and has created tasks
234
+ 2. Check if `.loki/queue/` directory exists: `ls -la .loki/queue/`
235
+ 3. Verify queue files have content: `cat .loki/queue/pending.json`
236
+ 4. If running manual mode, Loki creates tasks as it works - give it time to start
237
+
238
+ ### Issue: "AttributeError: 'str' object has no attribute 'get'"
239
+
240
+ **Cause:** Task payload was a string instead of expected JSON object (fixed in v2.35.1).
241
+
242
+ **Solution:** Update to latest version or apply the fix from PR #9.
243
+
244
+ ### Issue: "STATUS.txt does not exist"
245
+
246
+ **Cause:** `.loki/STATUS.txt` is only created when using the autonomy runner.
247
+
248
+ **Solutions:**
249
+ - Use autonomy runner: `./autonomy/run.sh ./prd.md` instead of manual Claude Code
250
+ - Or check task queues directly: `ls -la .loki/queue/`
251
+ - Monitor orchestrator state: `cat .loki/state/orchestrator.json | jq`
252
+
253
+ ### Issue: "Tasks not appearing in Vibe Kanban"
254
+
255
+ **Checklist:**
256
+ 1. Is Vibe Kanban running? Check http://127.0.0.1:53380
257
+ 2. Did you run the export script? `./scripts/export-to-vibe-kanban.sh`
258
+ 3. Check export directory has files: `ls ~/.vibe-kanban/loki-tasks/`
259
+ 4. Refresh the Vibe Kanban browser window
260
+ 5. Check Vibe Kanban is configured to watch that directory
261
+
262
+ ### Issue: "No real-time updates"
263
+
264
+ **Explanation:** The export script runs on-demand, not automatically.
265
+
266
+ **Solutions:**
267
+ 1. Run `./scripts/vibe-sync-watcher.sh` for automatic sync
268
+ 2. Or manually run export script periodically: `watch -n 10 ./scripts/export-to-vibe-kanban.sh`
269
+ 3. Or refresh manually when you want to check progress
270
+
271
+ ## Expected Workflow
272
+
273
+ **Important:** This is a manual integration, not automatic. Here's what to expect:
274
+
275
+ 1. Start Vibe Kanban (terminal 1, keeps running)
276
+ 2. Start Loki Mode (terminal 2, keeps running)
277
+ 3. Wait for Loki to create some tasks
278
+ 4. Run export script (terminal 3, one-time or via watcher)
279
+ 5. Refresh Vibe Kanban in browser to see tasks
280
+
281
+ **Loki does NOT automatically push to Vibe Kanban.** You must run the export script.
282
+
283
+ ## Future Integration Ideas
284
+
285
+ - [ ] Bidirectional sync (Vibe → Loki)
286
+ - [ ] Automatic background sync without watcher script
287
+ - [ ] Vibe Kanban MCP server for agent communication
288
+ - [ ] Shared agent profiles between tools
289
+ - [ ] Unified logging dashboard
package/mcp/__init__.py CHANGED
@@ -57,4 +57,4 @@ try:
57
57
  except ImportError:
58
58
  __all__ = ['mcp']
59
59
 
60
- __version__ = '6.60.0'
60
+ __version__ = '6.62.0'
package/mcp/server.py CHANGED
@@ -592,6 +592,10 @@ async def loki_memory_store_pattern(
592
592
  Returns:
593
593
  Pattern ID if successful
594
594
  """
595
+ # Validate confidence range
596
+ if not (0.0 <= confidence <= 1.0):
597
+ return json.dumps({"success": False, "error": "confidence must be between 0.0 and 1.0"})
598
+
595
599
  _emit_tool_event_async(
596
600
  'loki_memory_store_pattern', 'start',
597
601
  parameters={'pattern': pattern, 'category': category, 'confidence': confidence}
@@ -646,8 +650,10 @@ async def loki_task_queue_list() -> str:
646
650
  if manager and STATE_MANAGER_AVAILABLE:
647
651
  queue = manager.get_state("state/task-queue.json")
648
652
  if queue:
653
+ # Strip internal fields before returning
654
+ response = {k: v for k, v in queue.items() if k not in ("_next_id", "version")}
649
655
  _emit_tool_event_async('loki_task_queue_list', 'complete', result_status='success')
650
- return json.dumps(queue, default=str)
656
+ return json.dumps(response, default=str)
651
657
  # If no queue found via StateManager, return empty
652
658
  result = json.dumps({"tasks": [], "message": "No task queue found"})
653
659
  _emit_tool_event_async('loki_task_queue_list', 'complete', result_status='success')
@@ -663,8 +669,10 @@ async def loki_task_queue_list() -> str:
663
669
  with safe_open(queue_path, 'r') as f:
664
670
  queue = json.load(f)
665
671
 
672
+ # Strip internal fields before returning
673
+ response = {k: v for k, v in queue.items() if k not in ("_next_id", "version")}
666
674
  _emit_tool_event_async('loki_task_queue_list', 'complete', result_status='success')
667
- return json.dumps(queue, default=str)
675
+ return json.dumps(response, default=str)
668
676
  except PathTraversalError as e:
669
677
  logger.error(f"Path traversal attempt blocked: {e}")
670
678
  _emit_tool_event_async('loki_task_queue_list', 'complete', result_status='error', error='Access denied')
@@ -694,6 +702,14 @@ async def loki_task_queue_add(
694
702
  Returns:
695
703
  Task ID if successful
696
704
  """
705
+ # Validate priority and phase enums
706
+ valid_priorities = {"low", "medium", "high", "critical"}
707
+ valid_phases = {"discovery", "architecture", "development", "testing", "deployment"}
708
+ if priority not in valid_priorities:
709
+ return json.dumps({"success": False, "error": f"priority must be one of: {', '.join(sorted(valid_priorities))}"})
710
+ if phase not in valid_phases:
711
+ return json.dumps({"success": False, "error": f"phase must be one of: {', '.join(sorted(valid_phases))}"})
712
+
697
713
  _emit_tool_event_async(
698
714
  'loki_task_queue_add', 'start',
699
715
  parameters={'title': title, 'priority': priority, 'phase': phase}
@@ -716,7 +732,17 @@ async def loki_task_queue_add(
716
732
  queue = {"tasks": [], "version": "1.0"}
717
733
 
718
734
  # Create new task with monotonic counter to avoid ID collisions after deletions
719
- next_id = queue.get("_next_id", len(queue['tasks']) + 1)
735
+ # When _next_id is missing, scan existing IDs to find the max and use max+1
736
+ if "_next_id" not in queue:
737
+ existing_ids = []
738
+ for t in queue.get("tasks", []):
739
+ try:
740
+ existing_ids.append(int(t["id"].replace("task-", "")))
741
+ except (ValueError, KeyError):
742
+ pass
743
+ next_id = (max(existing_ids) + 1) if existing_ids else 1
744
+ else:
745
+ next_id = queue["_next_id"]
720
746
  task_id = f"task-{next_id:04d}"
721
747
  queue["_next_id"] = next_id + 1
722
748
  task = {
@@ -768,6 +794,14 @@ async def loki_task_queue_update(
768
794
  Returns:
769
795
  Updated task if successful
770
796
  """
797
+ # Validate status and priority enums when provided
798
+ valid_statuses = {"pending", "in_progress", "completed", "blocked"}
799
+ valid_priorities = {"low", "medium", "high", "critical"}
800
+ if status is not None and status not in valid_statuses:
801
+ return json.dumps({"success": False, "error": f"status must be one of: {', '.join(sorted(valid_statuses))}"})
802
+ if priority is not None and priority not in valid_priorities:
803
+ return json.dumps({"success": False, "error": f"priority must be one of: {', '.join(sorted(valid_priorities))}"})
804
+
771
805
  _emit_tool_event_async(
772
806
  'loki_task_queue_update', 'start',
773
807
  parameters={'task_id': task_id, 'status': status, 'priority': priority}
@@ -793,9 +827,9 @@ async def loki_task_queue_update(
793
827
  # Find and update task
794
828
  for task in queue["tasks"]:
795
829
  if task["id"] == task_id:
796
- if status:
830
+ if status is not None:
797
831
  task["status"] = status
798
- if priority:
832
+ if priority is not None:
799
833
  task["priority"] = priority
800
834
  task["updated_at"] = datetime.now(timezone.utc).isoformat()
801
835
 
@@ -850,7 +884,7 @@ async def loki_state_get() -> str:
850
884
  state["autonomy_state"] = autonomy_data
851
885
  else:
852
886
  # Fallback to direct file read
853
- state_path = safe_path_join('.loki', 'state', 'autonomy-state.json')
887
+ state_path = safe_path_join('.loki', 'autonomy-state.json')
854
888
  if os.path.exists(state_path):
855
889
  with safe_open(state_path, 'r') as f:
856
890
  state["autonomy_state"] = json.load(f)
@@ -1056,8 +1090,15 @@ async def loki_start_project(prd_content: str = "", prd_path: str = "") -> str:
1056
1090
  if not content:
1057
1091
  return json.dumps({"error": "No PRD content or path provided"})
1058
1092
 
1059
- # Initialize project state
1060
- os.makedirs('.loki/state', exist_ok=True)
1093
+ # Initialize project state using safe path operations
1094
+ state_dir = safe_path_join('.loki', 'state')
1095
+ safe_makedirs(state_dir, exist_ok=True)
1096
+
1097
+ # Persist PRD content so downstream tools can access it
1098
+ prd_dest = safe_path_join('.loki', 'state', 'prd.md')
1099
+ with safe_open(prd_dest, 'w') as f:
1100
+ f.write(content)
1101
+
1061
1102
  project = {
1062
1103
  "status": "initialized",
1063
1104
  "prd_length": len(content),
@@ -1201,10 +1242,11 @@ async def loki_checkpoint_restore(checkpoint_id: str = "") -> str:
1201
1242
  if not target:
1202
1243
  return json.dumps({"error": f"Checkpoint not found: {checkpoint_id}"})
1203
1244
 
1204
- # Write checkpoint state as current state
1245
+ # Write checkpoint state as current state, stripping the injected "id" field
1246
+ restored_state = {k: v for k, v in target.items() if k != "id"}
1205
1247
  state_path = safe_path_join('.loki', 'state', 'orchestrator.json')
1206
1248
  with safe_open(state_path, 'w') as f:
1207
- json.dump(target, f, indent=2)
1249
+ json.dump(restored_state, f, indent=2)
1208
1250
 
1209
1251
  _emit_tool_event_async('loki_checkpoint_restore', 'complete', result_status='success')
1210
1252
  return json.dumps({"restored": True, "checkpoint_id": checkpoint_id})
@@ -1310,7 +1352,10 @@ async def loki_code_search(
1310
1352
  file_filter: Filter by file path substring (e.g., "autonomy/", "dashboard/") (optional)
1311
1353
  type_filter: Filter by chunk type: "function", "class", "header", "section", "file" (optional)
1312
1354
  """
1313
- _emit_tool_event_async('loki_code_search', 'start', query=query)
1355
+ _emit_tool_event_async('loki_code_search', 'start',
1356
+ parameters={'query': query, 'n_results': n_results,
1357
+ 'language': language, 'file_filter': file_filter,
1358
+ 'type_filter': type_filter})
1314
1359
 
1315
1360
  collection = _get_chroma_collection()
1316
1361
  if collection is None:
@@ -1362,7 +1407,7 @@ async def loki_code_search(
1362
1407
  "name": meta.get("name", ""),
1363
1408
  "type": meta.get("type", ""),
1364
1409
  "language": meta.get("language", ""),
1365
- "relevance": round(1 - dist, 4), # Convert distance to similarity
1410
+ "relevance": round(max(0.0, 1.0 - dist / 2.0), 4), # L2 distance to similarity
1366
1411
  "preview": preview,
1367
1412
  })
1368
1413
 
@@ -1390,6 +1435,17 @@ async def loki_code_search_stats() -> str:
1390
1435
 
1391
1436
  try:
1392
1437
  count = collection.count()
1438
+
1439
+ # Short-circuit on empty collection to avoid limit=0 error
1440
+ if count == 0:
1441
+ return json.dumps({
1442
+ "total_chunks": 0,
1443
+ "unique_files": 0,
1444
+ "by_language": {},
1445
+ "by_type": {},
1446
+ "reindex_command": "python3.12 tools/index-codebase.py --reset",
1447
+ })
1448
+
1393
1449
  results = collection.get(limit=count, include=["metadatas"])
1394
1450
 
1395
1451
  langs = {}
@@ -1448,19 +1504,13 @@ async def mem_search(
1448
1504
  _emit_tool_event_async('mem_search', 'complete', result_status='success')
1449
1505
  return result
1450
1506
 
1451
- # Try SQLite backend first (has FTS5), fall back to keyword search
1452
- try:
1453
- from memory.sqlite_storage import SQLiteMemoryStorage
1454
- storage = SQLiteMemoryStorage(base_path)
1455
- results = storage.search_fts(query, collection=collection, limit=limit)
1456
- except (ImportError, Exception):
1457
- # Fall back to retrieval-based search
1458
- from memory.retrieval import MemoryRetrieval
1459
- from memory.storage import MemoryStorage
1460
- storage = MemoryStorage(base_path)
1461
- retriever = MemoryRetrieval(storage)
1462
- context = {"goal": query, "task_type": "exploration"}
1463
- results = retriever.retrieve_task_aware(context, top_k=limit)
1507
+ # Use retrieval-based search
1508
+ from memory.retrieval import MemoryRetrieval
1509
+ from memory.storage import MemoryStorage
1510
+ storage = MemoryStorage(base_path)
1511
+ retriever = MemoryRetrieval(storage)
1512
+ context = {"goal": query, "task_type": "exploration"}
1513
+ results = retriever.retrieve_task_aware(context, top_k=limit)
1464
1514
 
1465
1515
  # Compact results for token efficiency
1466
1516
  compact = []
@@ -1530,48 +1580,25 @@ async def mem_timeline(
1530
1580
  from datetime import timedelta
1531
1581
  cutoff = datetime.now(timezone.utc) - timedelta(hours=since_hours)
1532
1582
 
1533
- try:
1534
- from memory.sqlite_storage import SQLiteMemoryStorage
1535
- storage = SQLiteMemoryStorage(base_path)
1536
-
1537
- # Get timeline actions
1538
- timeline = storage.get_timeline()
1539
- actions = timeline.get("recent_actions", [])[:limit]
1540
-
1541
- # Get recent episodes for richer context
1542
- episode_ids = storage.list_episodes(since=cutoff, limit=limit)
1543
- episodes = []
1544
- for eid in episode_ids:
1545
- ep = storage.load_episode(eid)
1546
- if ep:
1547
- episodes.append({
1548
- "id": ep.get("id"),
1549
- "timestamp": ep.get("timestamp"),
1550
- "phase": ep.get("phase"),
1551
- "goal": (ep.get("goal", "") or "")[:150],
1552
- "outcome": ep.get("outcome"),
1553
- "duration_seconds": ep.get("duration_seconds"),
1554
- "files_modified": ep.get("files_modified", [])[:5],
1555
- })
1556
-
1557
- except (ImportError, Exception):
1558
- from memory.storage import MemoryStorage
1559
- storage = MemoryStorage(base_path)
1560
- timeline = storage.get_timeline()
1561
- actions = timeline.get("recent_actions", [])[:limit]
1562
-
1563
- episode_ids = storage.list_episodes(since=cutoff, limit=limit)
1564
- episodes = []
1565
- for eid in episode_ids:
1566
- ep = storage.load_episode(eid)
1567
- if ep:
1568
- episodes.append({
1569
- "id": ep.get("id"),
1570
- "timestamp": ep.get("timestamp"),
1571
- "phase": ep.get("phase"),
1572
- "goal": (ep.get("goal", "") or "")[:150],
1573
- "outcome": ep.get("outcome"),
1574
- })
1583
+ from memory.storage import MemoryStorage
1584
+ storage = MemoryStorage(base_path)
1585
+ timeline = storage.get_timeline()
1586
+ actions = timeline.get("recent_actions", [])[:limit]
1587
+
1588
+ episode_ids = storage.list_episodes(since=cutoff, limit=limit)
1589
+ episodes = []
1590
+ for eid in episode_ids:
1591
+ ep = storage.load_episode(eid)
1592
+ if ep:
1593
+ episodes.append({
1594
+ "id": ep.get("id"),
1595
+ "timestamp": ep.get("timestamp"),
1596
+ "phase": ep.get("phase"),
1597
+ "goal": (ep.get("goal", "") or "")[:150],
1598
+ "outcome": ep.get("outcome"),
1599
+ "duration_seconds": ep.get("duration_seconds"),
1600
+ "files_modified": ep.get("files_modified", [])[:5],
1601
+ })
1575
1602
 
1576
1603
  result = json.dumps({
1577
1604
  "actions": actions,
@@ -1624,12 +1651,8 @@ async def mem_get(
1624
1651
  # Cap at 20 to prevent abuse
1625
1652
  id_list = id_list[:20]
1626
1653
 
1627
- try:
1628
- from memory.sqlite_storage import SQLiteMemoryStorage
1629
- storage = SQLiteMemoryStorage(base_path)
1630
- except (ImportError, Exception):
1631
- from memory.storage import MemoryStorage
1632
- storage = MemoryStorage(base_path)
1654
+ from memory.storage import MemoryStorage
1655
+ storage = MemoryStorage(base_path)
1633
1656
 
1634
1657
  entries = {}
1635
1658
  for mem_id in id_list:
@@ -225,6 +225,9 @@ class ConsolidationPipeline:
225
225
  if not merged:
226
226
  self.storage.save_pattern(anti_pattern)
227
227
  all_patterns.append(anti_pattern)
228
+ # Add to existing_patterns so subsequent anti-patterns in this
229
+ # run are checked against it, preventing current-run duplicates.
230
+ existing_patterns.append(anti_pattern)
228
231
  result.anti_patterns_created += 1
229
232
 
230
233
  # 7. Create Zettelkasten links
@@ -294,6 +297,7 @@ class ConsolidationPipeline:
294
297
  label=self._generate_cluster_label([episode])
295
298
  )
296
299
  used.add(i)
300
+ member_indices = [i]
297
301
 
298
302
  # Find similar episodes
299
303
  for j, other_episode in enumerate(episodes):
@@ -304,10 +308,11 @@ class ConsolidationPipeline:
304
308
  if similarity >= threshold:
305
309
  cluster.episodes.append(other_episode)
306
310
  used.add(j)
311
+ member_indices.append(j)
307
312
 
308
- # Update centroid
313
+ # Update centroid using tracked indices (avoids O(n) list.index())
309
314
  if len(cluster.episodes) > 1:
310
- cluster_embeddings = [embeddings[episodes.index(ep)] for ep in cluster.episodes]
315
+ cluster_embeddings = [embeddings[idx] for idx in member_indices]
311
316
  cluster.centroid = np.mean(cluster_embeddings, axis=0)
312
317
  cluster.label = self._generate_cluster_label(cluster.episodes)
313
318
 
@@ -408,13 +413,23 @@ class ConsolidationPipeline:
408
413
  """Convert episode to text for embedding."""
409
414
  parts = [episode.goal]
410
415
 
411
- # Add action summaries
416
+ # Add action summaries (handle both ActionEntry objects and dicts)
412
417
  for action in episode.action_log[:5]: # Limit to first 5 actions
413
- parts.append(f"{action.tool}: {action.input[:100]}")
418
+ if isinstance(action, dict):
419
+ tool = action.get("tool", action.get("action", ""))
420
+ inp = action.get("input", action.get("target", ""))
421
+ else:
422
+ tool = action.tool
423
+ inp = action.input
424
+ parts.append(f"{tool}: {str(inp)[:100]}")
414
425
 
415
- # Add error types
426
+ # Add error types (handle both ErrorEntry objects and dicts)
416
427
  for error in episode.errors_encountered:
417
- parts.append(f"Error: {error.error_type}")
428
+ if isinstance(error, dict):
429
+ err_type = error.get("error_type", error.get("type", ""))
430
+ else:
431
+ err_type = error.error_type
432
+ parts.append(f"Error: {err_type}")
418
433
 
419
434
  return " ".join(parts)
420
435