loki-mode 5.32.2 → 5.34.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.
- package/SKILL.md +21 -28
- package/VERSION +1 -1
- package/autonomy/completion-council.sh +371 -7
- package/autonomy/hooks/quality-gate.sh +1 -1
- package/autonomy/hooks/track-metrics.sh +2 -2
- package/autonomy/hooks/validate-bash.sh +24 -24
- package/autonomy/loki +301 -32
- package/autonomy/run.sh +213 -18
- package/dashboard/__init__.py +1 -1
- package/dashboard/control.py +7 -3
- package/dashboard/server.py +149 -4
- package/dashboard/static/index.html +5 -5
- package/docs/INSTALLATION.md +10 -11
- package/docs/TOOL-INTEGRATION.md +1 -1
- package/docs/dashboard-guide.md +1 -1
- package/events/emit.sh +16 -9
- package/memory/engine.py +24 -16
- package/memory/retrieval.py +28 -19
- package/memory/storage.py +29 -3
- package/memory/vector_index.py +5 -5
- package/package.json +1 -1
package/docs/INSTALLATION.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Complete installation instructions for all platforms and use cases.
|
|
4
4
|
|
|
5
|
-
**Version:** v5.
|
|
5
|
+
**Version:** v5.34.0
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -90,7 +90,7 @@ Open VS Code Settings and search for "loki":
|
|
|
90
90
|
| Setting | Default | Description |
|
|
91
91
|
|---------|---------|-------------|
|
|
92
92
|
| `loki.provider` | `claude` | Default AI provider |
|
|
93
|
-
| `loki.apiPort` | `
|
|
93
|
+
| `loki.apiPort` | `57374` | API server port |
|
|
94
94
|
| `loki.apiHost` | `localhost` | API server host |
|
|
95
95
|
| `loki.autoConnect` | `true` | Auto-connect on activation |
|
|
96
96
|
| `loki.showStatusBar` | `true` | Show status bar item |
|
|
@@ -110,10 +110,10 @@ loki start
|
|
|
110
110
|
./autonomy/run.sh
|
|
111
111
|
|
|
112
112
|
# Option C: Direct API server start
|
|
113
|
-
|
|
113
|
+
loki serve
|
|
114
114
|
```
|
|
115
115
|
|
|
116
|
-
The extension will automatically connect when it detects the server is running at `localhost:
|
|
116
|
+
The extension will automatically connect when it detects the server is running at `localhost:57374`.
|
|
117
117
|
|
|
118
118
|
**Troubleshooting:** If you see "API server is not running" errors, make sure you started the server first using one of the commands above.
|
|
119
119
|
|
|
@@ -579,15 +579,14 @@ Loki Mode uses two network ports for different services:
|
|
|
579
579
|
|
|
580
580
|
| Port | Service | Description |
|
|
581
581
|
|------|---------|-------------|
|
|
582
|
-
| **57374** | Dashboard (FastAPI) |
|
|
583
|
-
| **9898** | REST API Server | JSON API used by the VS Code extension, CLI tools, and programmatic access. Serves endpoints like `/api/status`, `/api/tasks`, `/api/memory`, etc. |
|
|
582
|
+
| **57374** | Dashboard + API (FastAPI) | Unified server serving both the web dashboard UI (real-time monitoring, task board, Completion Council, memory browser, log streaming) and the REST API (used by VS Code extension, CLI tools, programmatic access). Served by `dashboard/server.py`. |
|
|
584
583
|
|
|
585
584
|
### When to Use Which Port
|
|
586
585
|
|
|
587
586
|
- **Browser access** (dashboard, monitoring): Use port **57374** -- `http://localhost:57374`
|
|
588
|
-
- **API calls** (REST, programmatic): Use port **
|
|
589
|
-
- **VS Code extension**: Connects to API on port **
|
|
590
|
-
-
|
|
587
|
+
- **API calls** (REST, programmatic): Use port **57374** -- `http://localhost:57374`
|
|
588
|
+
- **VS Code extension**: Connects to API on port **57374** automatically (configurable via `loki.apiPort` setting)
|
|
589
|
+
- The server is started automatically when you run `loki start` or `./autonomy/run.sh`. No manual configuration is needed.
|
|
591
590
|
|
|
592
591
|
### Port Configuration
|
|
593
592
|
|
|
@@ -595,8 +594,8 @@ Loki Mode uses two network ports for different services:
|
|
|
595
594
|
# Dashboard port (default: 57374)
|
|
596
595
|
LOKI_DASHBOARD_PORT=57374 loki dashboard start
|
|
597
596
|
|
|
598
|
-
# API port (default:
|
|
599
|
-
loki serve --port
|
|
597
|
+
# API port (default: 57374)
|
|
598
|
+
loki serve --port 57374
|
|
600
599
|
```
|
|
601
600
|
|
|
602
601
|
### CORS Configuration
|
package/docs/TOOL-INTEGRATION.md
CHANGED
package/docs/dashboard-guide.md
CHANGED
|
@@ -24,7 +24,7 @@ open http://localhost:57374
|
|
|
24
24
|
|
|
25
25
|
The dashboard automatically syncs with Loki Mode when it's running, polling `dashboard-state.json` every 2 seconds.
|
|
26
26
|
|
|
27
|
-
**Ports:** The dashboard
|
|
27
|
+
**Ports:** The dashboard and API run on unified port **57374** (FastAPI serves both). See [INSTALLATION.md](INSTALLATION.md#ports) for details.
|
|
28
28
|
|
|
29
29
|
---
|
|
30
30
|
|
package/events/emit.sh
CHANGED
|
@@ -25,30 +25,37 @@ mkdir -p "$EVENTS_DIR"
|
|
|
25
25
|
TYPE="${1:-state}"
|
|
26
26
|
SOURCE="${2:-cli}"
|
|
27
27
|
ACTION="${3:-unknown}"
|
|
28
|
-
if [ $# -ge 3 ]; then shift 3; else shift
|
|
28
|
+
if [ "$#" -ge 3 ]; then shift 3; else shift "$#"; fi
|
|
29
29
|
|
|
30
30
|
# Generate event ID and timestamp
|
|
31
31
|
EVENT_ID=$(head -c 4 /dev/urandom | od -An -tx1 | tr -d ' \n')
|
|
32
32
|
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")
|
|
33
33
|
|
|
34
|
+
# JSON escape helper: handles \, ", and control characters
|
|
35
|
+
json_escape() {
|
|
36
|
+
printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\t/\\t/g; s/\r/\\r/g' | tr -d '\n'
|
|
37
|
+
}
|
|
38
|
+
|
|
34
39
|
# Build payload JSON
|
|
35
|
-
|
|
40
|
+
ACTION_ESC=$(json_escape "$ACTION")
|
|
41
|
+
PAYLOAD="{\"action\":\"$ACTION_ESC\""
|
|
36
42
|
for arg in "$@"; do
|
|
37
43
|
key="${arg%%=*}"
|
|
38
44
|
value="${arg#*=}"
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
PAYLOAD="$PAYLOAD,\"$key_escaped\":\"$value\""
|
|
45
|
+
key_escaped=$(json_escape "$key")
|
|
46
|
+
value_escaped=$(json_escape "$value")
|
|
47
|
+
PAYLOAD="$PAYLOAD,\"$key_escaped\":\"$value_escaped\""
|
|
43
48
|
done
|
|
44
49
|
PAYLOAD="$PAYLOAD}"
|
|
45
50
|
|
|
46
|
-
# Build full event JSON
|
|
51
|
+
# Build full event JSON (escape type/source for safe embedding)
|
|
52
|
+
TYPE_ESC=$(json_escape "$TYPE")
|
|
53
|
+
SOURCE_ESC=$(json_escape "$SOURCE")
|
|
47
54
|
EVENT=$(cat <<EOF
|
|
48
55
|
{
|
|
49
56
|
"id": "$EVENT_ID",
|
|
50
|
-
"type": "$
|
|
51
|
-
"source": "$
|
|
57
|
+
"type": "$TYPE_ESC",
|
|
58
|
+
"source": "$SOURCE_ESC",
|
|
52
59
|
"timestamp": "$TIMESTAMP",
|
|
53
60
|
"payload": $PAYLOAD,
|
|
54
61
|
"version": "1.0"
|
package/memory/engine.py
CHANGED
|
@@ -6,7 +6,7 @@ from __future__ import annotations
|
|
|
6
6
|
|
|
7
7
|
import json
|
|
8
8
|
import os
|
|
9
|
-
from datetime import datetime, timedelta
|
|
9
|
+
from datetime import datetime, timedelta, timezone
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
from typing import Any, Callable, Dict, List, Optional, Union
|
|
12
12
|
|
|
@@ -118,7 +118,7 @@ class MemoryEngine:
|
|
|
118
118
|
"index.json",
|
|
119
119
|
{
|
|
120
120
|
"version": "1.0",
|
|
121
|
-
"last_updated": datetime.now().isoformat(),
|
|
121
|
+
"last_updated": datetime.now(timezone.utc).isoformat(),
|
|
122
122
|
"topics": [],
|
|
123
123
|
"total_memories": 0,
|
|
124
124
|
"total_tokens_available": 0,
|
|
@@ -131,7 +131,7 @@ class MemoryEngine:
|
|
|
131
131
|
"timeline.json",
|
|
132
132
|
{
|
|
133
133
|
"version": "1.0",
|
|
134
|
-
"last_updated": datetime.now().isoformat(),
|
|
134
|
+
"last_updated": datetime.now(timezone.utc).isoformat(),
|
|
135
135
|
"recent_actions": [],
|
|
136
136
|
"key_decisions": [],
|
|
137
137
|
"active_context": {
|
|
@@ -195,7 +195,7 @@ class MemoryEngine:
|
|
|
195
195
|
Returns:
|
|
196
196
|
Number of memories removed
|
|
197
197
|
"""
|
|
198
|
-
cutoff = datetime.now() - timedelta(days=days)
|
|
198
|
+
cutoff = datetime.now(timezone.utc) - timedelta(days=days)
|
|
199
199
|
removed_count = 0
|
|
200
200
|
|
|
201
201
|
# Get referenced episode IDs from semantic patterns
|
|
@@ -215,7 +215,7 @@ class MemoryEngine:
|
|
|
215
215
|
|
|
216
216
|
# Parse date from directory name (e.g., 2026-01-06)
|
|
217
217
|
try:
|
|
218
|
-
dir_date = datetime.strptime(date_dir.name, "%Y-%m-%d")
|
|
218
|
+
dir_date = datetime.strptime(date_dir.name, "%Y-%m-%d").replace(tzinfo=timezone.utc)
|
|
219
219
|
except ValueError:
|
|
220
220
|
continue
|
|
221
221
|
|
|
@@ -251,7 +251,7 @@ class MemoryEngine:
|
|
|
251
251
|
"""
|
|
252
252
|
# Determine storage path based on timestamp
|
|
253
253
|
trace_dict = trace.to_dict() if hasattr(trace, "to_dict") else trace.__dict__.copy()
|
|
254
|
-
timestamp = trace_dict.get("timestamp", datetime.now().isoformat())
|
|
254
|
+
timestamp = trace_dict.get("timestamp", datetime.now(timezone.utc).isoformat())
|
|
255
255
|
|
|
256
256
|
if isinstance(timestamp, str):
|
|
257
257
|
date_str = timestamp[:10] # Extract YYYY-MM-DD
|
|
@@ -436,7 +436,7 @@ class MemoryEngine:
|
|
|
436
436
|
for pattern in patterns_data["patterns"]:
|
|
437
437
|
if pattern.get("id") == pattern_id:
|
|
438
438
|
pattern["usage_count"] = pattern.get("usage_count", 0) + 1
|
|
439
|
-
pattern["last_used"] = datetime.now().isoformat()
|
|
439
|
+
pattern["last_used"] = datetime.now(timezone.utc).isoformat()
|
|
440
440
|
break
|
|
441
441
|
|
|
442
442
|
self.storage.write_json("semantic/patterns.json", patterns_data)
|
|
@@ -618,7 +618,7 @@ class MemoryEngine:
|
|
|
618
618
|
Returns:
|
|
619
619
|
List of memories within the time range
|
|
620
620
|
"""
|
|
621
|
-
until = until or datetime.now()
|
|
621
|
+
until = until or datetime.now(timezone.utc)
|
|
622
622
|
results: List[Dict[str, Any]] = []
|
|
623
623
|
|
|
624
624
|
episodic_path = Path(self.base_path) / "episodic"
|
|
@@ -630,7 +630,7 @@ class MemoryEngine:
|
|
|
630
630
|
continue
|
|
631
631
|
|
|
632
632
|
try:
|
|
633
|
-
dir_date = datetime.strptime(date_dir.name, "%Y-%m-%d")
|
|
633
|
+
dir_date = datetime.strptime(date_dir.name, "%Y-%m-%d").replace(tzinfo=timezone.utc)
|
|
634
634
|
except ValueError:
|
|
635
635
|
continue
|
|
636
636
|
|
|
@@ -745,7 +745,7 @@ class MemoryEngine:
|
|
|
745
745
|
"index.json",
|
|
746
746
|
{
|
|
747
747
|
"version": "1.0",
|
|
748
|
-
"last_updated": datetime.now().isoformat(),
|
|
748
|
+
"last_updated": datetime.now(timezone.utc).isoformat(),
|
|
749
749
|
"topics": list(topics.values()),
|
|
750
750
|
"total_memories": total_memories,
|
|
751
751
|
"total_tokens_available": total_tokens,
|
|
@@ -774,7 +774,7 @@ class MemoryEngine:
|
|
|
774
774
|
# Create action summary
|
|
775
775
|
context = episode.get("context", {})
|
|
776
776
|
action_entry = {
|
|
777
|
-
"timestamp": episode.get("timestamp", datetime.now().isoformat()),
|
|
777
|
+
"timestamp": episode.get("timestamp", datetime.now(timezone.utc).isoformat()),
|
|
778
778
|
"action": context.get("goal", "Task completed")[:100],
|
|
779
779
|
"outcome": episode.get("outcome", "unknown"),
|
|
780
780
|
"topic_id": context.get("phase", "general"),
|
|
@@ -783,7 +783,7 @@ class MemoryEngine:
|
|
|
783
783
|
# Add to recent actions (keep last 50)
|
|
784
784
|
timeline["recent_actions"].insert(0, action_entry)
|
|
785
785
|
timeline["recent_actions"] = timeline["recent_actions"][:50]
|
|
786
|
-
timeline["last_updated"] = datetime.now().isoformat()
|
|
786
|
+
timeline["last_updated"] = datetime.now(timezone.utc).isoformat()
|
|
787
787
|
|
|
788
788
|
self.storage.write_json("timeline.json", timeline)
|
|
789
789
|
|
|
@@ -802,7 +802,7 @@ class MemoryEngine:
|
|
|
802
802
|
topic_found = False
|
|
803
803
|
for topic in index["topics"]:
|
|
804
804
|
if topic.get("id") == category:
|
|
805
|
-
topic["last_accessed"] = datetime.now().isoformat()
|
|
805
|
+
topic["last_accessed"] = datetime.now(timezone.utc).isoformat()
|
|
806
806
|
topic["relevance_score"] = max(
|
|
807
807
|
topic.get("relevance_score", 0.5),
|
|
808
808
|
pattern.get("confidence", 0.5),
|
|
@@ -815,11 +815,11 @@ class MemoryEngine:
|
|
|
815
815
|
"id": category,
|
|
816
816
|
"summary": f"Patterns for {category}",
|
|
817
817
|
"relevance_score": pattern.get("confidence", 0.5),
|
|
818
|
-
"last_accessed": datetime.now().isoformat(),
|
|
818
|
+
"last_accessed": datetime.now(timezone.utc).isoformat(),
|
|
819
819
|
"token_count": len(json.dumps(pattern)) // 4,
|
|
820
820
|
})
|
|
821
821
|
|
|
822
|
-
index["last_updated"] = datetime.now().isoformat()
|
|
822
|
+
index["last_updated"] = datetime.now(timezone.utc).isoformat()
|
|
823
823
|
index["total_memories"] = index.get("total_memories", 0) + 1
|
|
824
824
|
|
|
825
825
|
self.storage.write_json("index.json", index)
|
|
@@ -851,10 +851,14 @@ class MemoryEngine:
|
|
|
851
851
|
if timestamp_str.endswith("Z"):
|
|
852
852
|
timestamp_str = timestamp_str[:-1]
|
|
853
853
|
timestamp = datetime.fromisoformat(timestamp_str)
|
|
854
|
+
if timestamp.tzinfo is None:
|
|
855
|
+
timestamp = timestamp.replace(tzinfo=timezone.utc)
|
|
854
856
|
elif isinstance(timestamp_str, datetime):
|
|
855
857
|
timestamp = timestamp_str
|
|
858
|
+
if timestamp.tzinfo is None:
|
|
859
|
+
timestamp = timestamp.replace(tzinfo=timezone.utc)
|
|
856
860
|
else:
|
|
857
|
-
timestamp = datetime.now()
|
|
861
|
+
timestamp = datetime.now(timezone.utc)
|
|
858
862
|
|
|
859
863
|
# Extract phase and goal from context dict
|
|
860
864
|
context = data.get("context", {})
|
|
@@ -904,8 +908,12 @@ class MemoryEngine:
|
|
|
904
908
|
if last_used_raw.endswith("Z"):
|
|
905
909
|
last_used_raw = last_used_raw[:-1]
|
|
906
910
|
last_used = datetime.fromisoformat(last_used_raw)
|
|
911
|
+
if last_used.tzinfo is None:
|
|
912
|
+
last_used = last_used.replace(tzinfo=timezone.utc)
|
|
907
913
|
elif isinstance(last_used_raw, datetime):
|
|
908
914
|
last_used = last_used_raw
|
|
915
|
+
if last_used.tzinfo is None:
|
|
916
|
+
last_used = last_used.replace(tzinfo=timezone.utc)
|
|
909
917
|
|
|
910
918
|
# Convert links dicts to Link objects
|
|
911
919
|
links_raw = data.get("links", [])
|
package/memory/retrieval.py
CHANGED
|
@@ -19,7 +19,7 @@ from __future__ import annotations
|
|
|
19
19
|
|
|
20
20
|
import json
|
|
21
21
|
import re
|
|
22
|
-
from datetime import datetime
|
|
22
|
+
from datetime import datetime, timezone
|
|
23
23
|
from pathlib import Path
|
|
24
24
|
from typing import Any, Callable, Dict, List, Optional, Protocol, Tuple, Union, TYPE_CHECKING
|
|
25
25
|
|
|
@@ -746,7 +746,7 @@ class MemoryRetrieval:
|
|
|
746
746
|
Returns:
|
|
747
747
|
List of memories within the time range
|
|
748
748
|
"""
|
|
749
|
-
until = until or datetime.now()
|
|
749
|
+
until = until or datetime.now(timezone.utc)
|
|
750
750
|
results: List[Dict[str, Any]] = []
|
|
751
751
|
|
|
752
752
|
# Search episodic memories by date directory (via storage layer)
|
|
@@ -756,7 +756,7 @@ class MemoryRetrieval:
|
|
|
756
756
|
continue
|
|
757
757
|
|
|
758
758
|
try:
|
|
759
|
-
dir_date = datetime.strptime(date_dir.name, "%Y-%m-%d")
|
|
759
|
+
dir_date = datetime.strptime(date_dir.name, "%Y-%m-%d").replace(tzinfo=timezone.utc)
|
|
760
760
|
except ValueError:
|
|
761
761
|
continue
|
|
762
762
|
|
|
@@ -938,7 +938,7 @@ class MemoryRetrieval:
|
|
|
938
938
|
Returns:
|
|
939
939
|
Results with recency boost applied
|
|
940
940
|
"""
|
|
941
|
-
now = datetime.now()
|
|
941
|
+
now = datetime.now(timezone.utc)
|
|
942
942
|
|
|
943
943
|
for result in results:
|
|
944
944
|
timestamp = result.get("timestamp") or result.get("last_used")
|
|
@@ -1281,25 +1281,34 @@ class MemoryRetrieval:
|
|
|
1281
1281
|
"""
|
|
1282
1282
|
Save all vector indices to disk.
|
|
1283
1283
|
"""
|
|
1284
|
-
|
|
1285
|
-
|
|
1284
|
+
if hasattr(self.storage, 'ensure_directory'):
|
|
1285
|
+
self.storage.ensure_directory("vectors")
|
|
1286
1286
|
|
|
1287
1287
|
for name, index in self.vector_indices.items():
|
|
1288
|
-
|
|
1289
|
-
|
|
1288
|
+
# Resolve path through storage to respect namespace isolation
|
|
1289
|
+
if hasattr(self.storage, '_resolve_path'):
|
|
1290
|
+
index_path = self.storage._resolve_path(f"vectors/{name}_index")
|
|
1291
|
+
else:
|
|
1292
|
+
index_path = str(self.base_path / "vectors" / f"{name}_index")
|
|
1293
|
+
index.save(index_path)
|
|
1290
1294
|
|
|
1291
1295
|
def load_indices(self) -> None:
|
|
1292
1296
|
"""
|
|
1293
1297
|
Load all vector indices from disk.
|
|
1294
1298
|
"""
|
|
1295
|
-
|
|
1296
|
-
if not
|
|
1299
|
+
vectors_files = self.storage.list_files("vectors", "*")
|
|
1300
|
+
if not vectors_files:
|
|
1297
1301
|
return
|
|
1298
1302
|
|
|
1299
1303
|
for name, index in self.vector_indices.items():
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1304
|
+
if hasattr(self.storage, '_resolve_path'):
|
|
1305
|
+
index_path = self.storage._resolve_path(f"vectors/{name}_index")
|
|
1306
|
+
else:
|
|
1307
|
+
index_path = str(self.base_path / "vectors" / f"{name}_index")
|
|
1308
|
+
# Check if the npz file exists (VectorIndex.load expects base path without extension)
|
|
1309
|
+
import os
|
|
1310
|
+
if os.path.exists(f"{index_path}.npz"):
|
|
1311
|
+
index.load(index_path)
|
|
1303
1312
|
|
|
1304
1313
|
# -------------------------------------------------------------------------
|
|
1305
1314
|
# Private Helper Methods
|
|
@@ -1453,16 +1462,16 @@ class MemoryRetrieval:
|
|
|
1453
1462
|
return
|
|
1454
1463
|
|
|
1455
1464
|
index = self.vector_indices["episodic"]
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
if not episodic_path.exists():
|
|
1459
|
-
return
|
|
1465
|
+
date_dirs = self.storage.list_files("episodic", "*")
|
|
1460
1466
|
|
|
1461
|
-
for date_dir in
|
|
1467
|
+
for date_dir in date_dirs:
|
|
1462
1468
|
if not date_dir.is_dir():
|
|
1463
1469
|
continue
|
|
1464
1470
|
|
|
1465
|
-
|
|
1471
|
+
episode_files = self.storage.list_files(
|
|
1472
|
+
f"episodic/{date_dir.name}", "*.json"
|
|
1473
|
+
)
|
|
1474
|
+
for episode_file in episode_files:
|
|
1466
1475
|
if episode_file.name == "index.json":
|
|
1467
1476
|
continue
|
|
1468
1477
|
|
package/memory/storage.py
CHANGED
|
@@ -118,6 +118,23 @@ class MemoryStorage:
|
|
|
118
118
|
for directory in directories:
|
|
119
119
|
directory.mkdir(parents=True, exist_ok=True)
|
|
120
120
|
|
|
121
|
+
# Clean up stale lock files from previous crashed processes
|
|
122
|
+
self._cleanup_stale_locks()
|
|
123
|
+
|
|
124
|
+
def _cleanup_stale_locks(self) -> None:
|
|
125
|
+
"""Remove stale .lock files older than 5 minutes (safe with concurrent processes)."""
|
|
126
|
+
try:
|
|
127
|
+
stale_cutoff = datetime.now(timezone.utc) - timedelta(minutes=5)
|
|
128
|
+
for lock_file in self.base_path.rglob("*.lock"):
|
|
129
|
+
try:
|
|
130
|
+
mtime = datetime.fromtimestamp(lock_file.stat().st_mtime, tz=timezone.utc)
|
|
131
|
+
if mtime < stale_cutoff:
|
|
132
|
+
lock_file.unlink()
|
|
133
|
+
except OSError:
|
|
134
|
+
pass
|
|
135
|
+
except OSError:
|
|
136
|
+
pass
|
|
137
|
+
|
|
121
138
|
def _ensure_index(self) -> None:
|
|
122
139
|
"""Initialize index.json if it doesn't exist."""
|
|
123
140
|
index_path = self.base_path / "index.json"
|
|
@@ -383,10 +400,19 @@ class MemoryStorage:
|
|
|
383
400
|
if file_path.exists():
|
|
384
401
|
with self._file_lock(file_path, exclusive=True):
|
|
385
402
|
file_path.unlink()
|
|
386
|
-
# Clean up lock file
|
|
403
|
+
# Clean up lock file (safety net in case _file_lock missed it)
|
|
387
404
|
lock_path = file_path.with_suffix(".json.lock")
|
|
388
|
-
|
|
389
|
-
lock_path.
|
|
405
|
+
try:
|
|
406
|
+
if lock_path.exists():
|
|
407
|
+
lock_path.unlink()
|
|
408
|
+
except OSError:
|
|
409
|
+
pass
|
|
410
|
+
# Clean up any remaining lock files before checking if dir is empty
|
|
411
|
+
for stale_lock in date_dir.glob("*.lock"):
|
|
412
|
+
try:
|
|
413
|
+
stale_lock.unlink()
|
|
414
|
+
except OSError:
|
|
415
|
+
pass
|
|
390
416
|
# Clean up empty date directory
|
|
391
417
|
if not any(date_dir.iterdir()):
|
|
392
418
|
date_dir.rmdir()
|
package/memory/vector_index.py
CHANGED
|
@@ -348,15 +348,15 @@ class VectorIndex:
|
|
|
348
348
|
Normalize copies of all vectors for cosine similarity search.
|
|
349
349
|
|
|
350
350
|
This is called automatically before search operations.
|
|
351
|
-
Uses copies to avoid corrupting the original stored embeddings.
|
|
351
|
+
Uses explicit copies to avoid corrupting the original stored embeddings.
|
|
352
352
|
"""
|
|
353
353
|
self._normalized_embeddings = []
|
|
354
354
|
for embedding in self.embeddings:
|
|
355
|
-
|
|
355
|
+
vec_copy = embedding.copy()
|
|
356
|
+
norm = np.linalg.norm(vec_copy)
|
|
356
357
|
if norm > 0:
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
self._normalized_embeddings.append(embedding.copy())
|
|
358
|
+
vec_copy = vec_copy / norm
|
|
359
|
+
self._normalized_embeddings.append(vec_copy)
|
|
360
360
|
self._normalized = True
|
|
361
361
|
|
|
362
362
|
def _cosine_similarity(self, a: np.ndarray, b: np.ndarray) -> float:
|