loki-mode 5.32.2 → 5.33.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 +20 -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 +41 -32
- package/autonomy/run.sh +49 -18
- package/dashboard/__init__.py +1 -1
- package/dashboard/control.py +7 -3
- package/dashboard/server.py +6 -4
- package/dashboard/static/index.html +5 -5
- package/docs/INSTALLATION.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/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:
|