loki-mode 7.76.0 → 7.78.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 +2 -2
- package/VERSION +1 -1
- package/autonomy/NOTIFY_INTEGRATION.md +9 -0
- package/autonomy/lib/voter-agents.sh +16 -1
- package/autonomy/run.sh +7 -973
- package/dashboard/__init__.py +1 -1
- package/dashboard/server.py +119 -13
- package/docs/INSTALLATION.md +2 -2
- package/loki-ts/dist/loki.js +190 -190
- package/mcp/__init__.py +1 -1
- package/memory/consolidation.py +9 -4
- package/memory/engine.py +8 -0
- package/memory/retrieval.py +18 -0
- package/memory/storage.py +21 -5
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
package/mcp/__init__.py
CHANGED
package/memory/consolidation.py
CHANGED
|
@@ -157,10 +157,15 @@ class ConsolidationPipeline:
|
|
|
157
157
|
if lock_file is not None:
|
|
158
158
|
fcntl.flock(lock_file.fileno(), fcntl.LOCK_UN)
|
|
159
159
|
lock_file.close()
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
160
|
+
# Do NOT unlink the lock inode here. Unlinking on release is a
|
|
161
|
+
# flock+unlink inode-replacement race: waiter B blocked on
|
|
162
|
+
# inode-1 acquires it after holder A unlinks inode-1, then a
|
|
163
|
+
# third consolidation C opens the path, finds it gone, creates
|
|
164
|
+
# inode-2 and flocks inode-2 -- entering _consolidate_locked
|
|
165
|
+
# while B is still inside, breaking the BUG-MEM-003
|
|
166
|
+
# single-consolidation guarantee. Same class fixed in
|
|
167
|
+
# storage._file_lock. Persistent lock files are the standard
|
|
168
|
+
# flock pattern; the file is reused across runs.
|
|
164
169
|
|
|
165
170
|
def _consolidate_locked(self, since_hours: int) -> ConsolidationResult:
|
|
166
171
|
"""Run the consolidation pipeline under an exclusive lock."""
|
package/memory/engine.py
CHANGED
|
@@ -7,6 +7,7 @@ from __future__ import annotations
|
|
|
7
7
|
import json
|
|
8
8
|
import logging
|
|
9
9
|
import os
|
|
10
|
+
import re
|
|
10
11
|
from datetime import datetime, timedelta, timezone
|
|
11
12
|
from pathlib import Path
|
|
12
13
|
from typing import Any, Callable, Dict, List, Optional, Union
|
|
@@ -304,6 +305,13 @@ class MemoryEngine:
|
|
|
304
305
|
else:
|
|
305
306
|
date_str = timestamp.strftime("%Y-%m-%d")
|
|
306
307
|
|
|
308
|
+
# Reject a junk date directory derived from a poisoned/round-tripped
|
|
309
|
+
# timestamp (mirrors storage.save_episode). Traversal is already
|
|
310
|
+
# contained by _resolve_path; this just stops non-date dirs being
|
|
311
|
+
# created. Only an exact YYYY-MM-DD string is allowed.
|
|
312
|
+
if not re.fullmatch(r"\d{4}-\d{2}-\d{2}", date_str):
|
|
313
|
+
date_str = datetime.now(timezone.utc).strftime("%Y-%m-%d")
|
|
314
|
+
|
|
307
315
|
episode_id = trace_dict.get("id", f"ep-{date_str}-{self._generate_id()}")
|
|
308
316
|
trace_dict["id"] = episode_id
|
|
309
317
|
|
package/memory/retrieval.py
CHANGED
|
@@ -297,6 +297,14 @@ class MemoryRetrieval:
|
|
|
297
297
|
|
|
298
298
|
# Track which legacy entries we've already warned about to avoid log spam.
|
|
299
299
|
_LEGACY_WARN_LIMIT = 5
|
|
300
|
+
|
|
301
|
+
# Cap on episode files read per keyword scan. The episodic store grows
|
|
302
|
+
# unbounded over a long-lived project, so an uncapped scan reads+parses
|
|
303
|
+
# every episode on every keyword query. Date dirs are walked newest-first,
|
|
304
|
+
# so the cap retains the most recent episodes (the natural relevance order)
|
|
305
|
+
# and only stops unbounded IO; it does not change scoring of retained
|
|
306
|
+
# candidates.
|
|
307
|
+
_KEYWORD_SCAN_MAX_EPISODES = 2000
|
|
300
308
|
_legacy_warned_count: int = 0
|
|
301
309
|
|
|
302
310
|
def _belongs_to_namespace(self, result: Dict[str, Any]) -> bool:
|
|
@@ -1509,7 +1517,14 @@ class MemoryRetrieval:
|
|
|
1509
1517
|
if not date_dirs:
|
|
1510
1518
|
return results
|
|
1511
1519
|
|
|
1520
|
+
# Bound the scan so an unbounded episodic store does not force a
|
|
1521
|
+
# read+parse of every episode on each query (see
|
|
1522
|
+
# _KEYWORD_SCAN_MAX_EPISODES). Newest date dirs first keeps the most
|
|
1523
|
+
# recent episodes.
|
|
1524
|
+
scanned = 0
|
|
1512
1525
|
for date_dir in sorted(date_dirs, reverse=True):
|
|
1526
|
+
if scanned >= self._KEYWORD_SCAN_MAX_EPISODES:
|
|
1527
|
+
break
|
|
1513
1528
|
if not date_dir.is_dir():
|
|
1514
1529
|
continue
|
|
1515
1530
|
|
|
@@ -1519,7 +1534,10 @@ class MemoryRetrieval:
|
|
|
1519
1534
|
for episode_file in episode_files:
|
|
1520
1535
|
if episode_file.name == "index.json":
|
|
1521
1536
|
continue
|
|
1537
|
+
if scanned >= self._KEYWORD_SCAN_MAX_EPISODES:
|
|
1538
|
+
break
|
|
1522
1539
|
|
|
1540
|
+
scanned += 1
|
|
1523
1541
|
data = self.storage.read_json(
|
|
1524
1542
|
f"episodic/{date_dir.name}/{episode_file.name}"
|
|
1525
1543
|
)
|
package/memory/storage.py
CHANGED
|
@@ -429,6 +429,21 @@ class MemoryStorage:
|
|
|
429
429
|
# Episode Storage
|
|
430
430
|
# -------------------------------------------------------------------------
|
|
431
431
|
|
|
432
|
+
@staticmethod
|
|
433
|
+
def _sanitize_episode_id(episode_id) -> str:
|
|
434
|
+
"""
|
|
435
|
+
Sanitize an episode id for use in a filename.
|
|
436
|
+
|
|
437
|
+
Separators and "." segments cannot leak into the path (mirrors
|
|
438
|
+
save_skill). save_episode and load_episode must use the SAME transform
|
|
439
|
+
or a round-tripped id with ":", "/", or "." chars would write to one
|
|
440
|
+
file and read from another.
|
|
441
|
+
"""
|
|
442
|
+
return "".join(
|
|
443
|
+
c if c.isalnum() or c in "-_" else "_"
|
|
444
|
+
for c in str(episode_id)
|
|
445
|
+
)
|
|
446
|
+
|
|
432
447
|
def save_episode(self, episode: EpisodeTrace) -> str:
|
|
433
448
|
"""
|
|
434
449
|
Save an episode trace to storage.
|
|
@@ -475,10 +490,7 @@ class MemoryStorage:
|
|
|
475
490
|
if not re.fullmatch(r"\d{4}-\d{2}-\d{2}", date_str):
|
|
476
491
|
date_str = datetime.now(timezone.utc).strftime("%Y-%m-%d")
|
|
477
492
|
|
|
478
|
-
safe_episode_id =
|
|
479
|
-
c if c.isalnum() or c in "-_" else "_"
|
|
480
|
-
for c in str(episode_id)
|
|
481
|
-
)
|
|
493
|
+
safe_episode_id = self._sanitize_episode_id(episode_id)
|
|
482
494
|
|
|
483
495
|
date_dir = self.base_path / "episodic" / date_str
|
|
484
496
|
date_dir.mkdir(parents=True, exist_ok=True)
|
|
@@ -507,10 +519,14 @@ class MemoryStorage:
|
|
|
507
519
|
if not episodic_dir.exists():
|
|
508
520
|
return None
|
|
509
521
|
|
|
522
|
+
# Sanitize the same way save_episode does so an id carrying ":", "/",
|
|
523
|
+
# or "." chars resolves to the file it was actually written to.
|
|
524
|
+
safe_episode_id = self._sanitize_episode_id(episode_id)
|
|
525
|
+
|
|
510
526
|
# Search all date directories
|
|
511
527
|
for date_dir in episodic_dir.iterdir():
|
|
512
528
|
if date_dir.is_dir():
|
|
513
|
-
file_path = date_dir / f"task-{
|
|
529
|
+
file_path = date_dir / f"task-{safe_episode_id}.json"
|
|
514
530
|
if file_path.exists():
|
|
515
531
|
data = self._load_json(file_path)
|
|
516
532
|
if data:
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "loki-mode",
|
|
3
3
|
"mcpName": "io.github.asklokesh/loki-mode",
|
|
4
|
-
"version": "7.
|
|
4
|
+
"version": "7.78.0",
|
|
5
5
|
"description": "Loki Mode by Autonomi. Autonomous spec-to-product system: takes a PRD, GitHub issue, OpenAPI/JSON/YAML, or one-line brief to a deployed app via the RARV-C closure loop with 8 quality gates. Provider-agnostic (Claude Code, OpenAI Codex, Cline, Aider).",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"agent",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
3
3
|
"name": "loki-mode",
|
|
4
4
|
"displayName": "Loki Mode",
|
|
5
|
-
"version": "7.
|
|
5
|
+
"version": "7.78.0",
|
|
6
6
|
"description": "Autonomous spec-to-product build system with a built-in trust layer (RARV-C closure loop, 8 quality gates, completion council). Ships Loki's spec-hardening, drift-detection, and deterministic PR verification commands plus the Loki MCP server.",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "Autonomi",
|