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,188 @@
1
+ """SQLite state backend for Loki Mode.
2
+
3
+ Secondary storage layer that mirrors file-based state into SQLite
4
+ for queryable access. The file-based state in .loki/ remains primary.
5
+ SQLite is a read-optimized query layer, NOT the source of truth.
6
+
7
+ Usage:
8
+ db = SqliteStateBackend(".loki/state.db")
9
+ db.record_event("agent_started", {"agent_id": "arch_001", "phase": "understand"})
10
+ events = db.query_events(agent_id="arch_001")
11
+ db.record_message("task.completed", {"step_id": "step_001"}, sender="migration_planner")
12
+ messages = db.query_messages(topic="task.*")
13
+ """
14
+
15
+ import json
16
+ import os
17
+ import sqlite3
18
+ import threading
19
+ from datetime import datetime, timezone
20
+ from pathlib import Path
21
+
22
+
23
+ class SqliteStateBackend:
24
+ """SQLite-backed queryable state for debugging and inspection."""
25
+
26
+ def __init__(self, db_path: str = None):
27
+ if db_path is None:
28
+ db_path = os.path.join(
29
+ os.environ.get("LOKI_DATA_DIR", os.path.expanduser("~/.loki")),
30
+ "state.db"
31
+ )
32
+ self.db_path = db_path
33
+ self._lock = threading.Lock()
34
+ self._ensure_schema()
35
+
36
+ def _ensure_schema(self) -> None:
37
+ Path(self.db_path).parent.mkdir(parents=True, exist_ok=True)
38
+ with self._connect() as conn:
39
+ conn.executescript("""
40
+ CREATE TABLE IF NOT EXISTS events (
41
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
42
+ timestamp TEXT NOT NULL,
43
+ event_type TEXT NOT NULL,
44
+ data TEXT NOT NULL,
45
+ session_id TEXT,
46
+ agent_id TEXT,
47
+ migration_id TEXT
48
+ );
49
+ CREATE INDEX IF NOT EXISTS idx_events_type ON events(event_type);
50
+ CREATE INDEX IF NOT EXISTS idx_events_agent ON events(agent_id);
51
+ CREATE INDEX IF NOT EXISTS idx_events_migration ON events(migration_id);
52
+
53
+ CREATE TABLE IF NOT EXISTS messages (
54
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
55
+ timestamp TEXT NOT NULL,
56
+ topic TEXT NOT NULL,
57
+ data TEXT NOT NULL,
58
+ sender TEXT,
59
+ cluster_id TEXT
60
+ );
61
+ CREATE INDEX IF NOT EXISTS idx_messages_topic ON messages(topic);
62
+ CREATE INDEX IF NOT EXISTS idx_messages_cluster ON messages(cluster_id);
63
+
64
+ CREATE TABLE IF NOT EXISTS checkpoints (
65
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
66
+ timestamp TEXT NOT NULL,
67
+ migration_id TEXT NOT NULL,
68
+ step_id TEXT NOT NULL,
69
+ git_sha TEXT,
70
+ metadata TEXT
71
+ );
72
+ CREATE INDEX IF NOT EXISTS idx_checkpoints_migration ON checkpoints(migration_id);
73
+ """)
74
+ # Set secure file permissions (owner read/write only)
75
+ try:
76
+ os.chmod(self.db_path, 0o600)
77
+ except OSError:
78
+ pass
79
+
80
+ def _connect(self) -> sqlite3.Connection:
81
+ conn = sqlite3.connect(self.db_path, timeout=5)
82
+ conn.row_factory = sqlite3.Row
83
+ return conn
84
+
85
+ def record_event(self, event_type: str, data: dict,
86
+ session_id: str = None, agent_id: str = None,
87
+ migration_id: str = None) -> int:
88
+ """Record an event. Returns row ID."""
89
+ with self._lock, self._connect() as conn:
90
+ cursor = conn.execute(
91
+ "INSERT INTO events (timestamp, event_type, data, session_id, agent_id, migration_id) "
92
+ "VALUES (?, ?, ?, ?, ?, ?)",
93
+ (datetime.now(timezone.utc).isoformat() + "Z", event_type,
94
+ json.dumps(data), session_id, agent_id, migration_id)
95
+ )
96
+ return cursor.lastrowid
97
+
98
+ def query_events(self, event_type: str = None, agent_id: str = None,
99
+ migration_id: str = None, limit: int = 100) -> list[dict]:
100
+ """Query events with optional filters."""
101
+ conditions = []
102
+ params = []
103
+ if event_type:
104
+ conditions.append("event_type = ?")
105
+ params.append(event_type)
106
+ if agent_id:
107
+ conditions.append("agent_id = ?")
108
+ params.append(agent_id)
109
+ if migration_id:
110
+ conditions.append("migration_id = ?")
111
+ params.append(migration_id)
112
+
113
+ where = f"WHERE {' AND '.join(conditions)}" if conditions else ""
114
+ with self._connect() as conn:
115
+ rows = conn.execute(
116
+ f"SELECT * FROM events {where} ORDER BY id DESC LIMIT ?",
117
+ params + [limit]
118
+ ).fetchall()
119
+ return [dict(row) for row in rows]
120
+
121
+ def record_message(self, topic: str, data: dict, sender: str = "",
122
+ cluster_id: str = None) -> int:
123
+ """Record a pub/sub message. Returns row ID."""
124
+ with self._lock, self._connect() as conn:
125
+ cursor = conn.execute(
126
+ "INSERT INTO messages (timestamp, topic, data, sender, cluster_id) "
127
+ "VALUES (?, ?, ?, ?, ?)",
128
+ (datetime.now(timezone.utc).isoformat() + "Z", topic,
129
+ json.dumps(data), sender, cluster_id)
130
+ )
131
+ return cursor.lastrowid
132
+
133
+ def query_messages(self, topic: str = None, cluster_id: str = None,
134
+ limit: int = 100) -> list[dict]:
135
+ """Query messages with optional filters. Supports wildcard topics."""
136
+ conditions = []
137
+ params = []
138
+ if topic:
139
+ if "*" in topic:
140
+ conditions.append("topic GLOB ?")
141
+ params.append(topic)
142
+ else:
143
+ conditions.append("topic = ?")
144
+ params.append(topic)
145
+ if cluster_id:
146
+ conditions.append("cluster_id = ?")
147
+ params.append(cluster_id)
148
+
149
+ where = f"WHERE {' AND '.join(conditions)}" if conditions else ""
150
+ with self._connect() as conn:
151
+ rows = conn.execute(
152
+ f"SELECT * FROM messages {where} ORDER BY id DESC LIMIT ?",
153
+ params + [limit]
154
+ ).fetchall()
155
+ return [dict(row) for row in rows]
156
+
157
+ def record_checkpoint(self, migration_id: str, step_id: str,
158
+ git_sha: str = None, metadata: dict = None) -> int:
159
+ """Record a migration checkpoint. Returns row ID."""
160
+ with self._lock, self._connect() as conn:
161
+ cursor = conn.execute(
162
+ "INSERT INTO checkpoints (timestamp, migration_id, step_id, git_sha, metadata) "
163
+ "VALUES (?, ?, ?, ?, ?)",
164
+ (datetime.now(timezone.utc).isoformat() + "Z", migration_id,
165
+ step_id, git_sha, json.dumps(metadata or {}))
166
+ )
167
+ return cursor.lastrowid
168
+
169
+ def query_checkpoints(self, migration_id: str = None,
170
+ limit: int = 100) -> list[dict]:
171
+ """Query checkpoints with optional migration_id filter."""
172
+ conditions = []
173
+ params = []
174
+ if migration_id:
175
+ conditions.append("migration_id = ?")
176
+ params.append(migration_id)
177
+
178
+ where = f"WHERE {' AND '.join(conditions)}" if conditions else ""
179
+ with self._connect() as conn:
180
+ rows = conn.execute(
181
+ f"SELECT * FROM checkpoints {where} ORDER BY id DESC LIMIT ?",
182
+ params + [limit]
183
+ ).fetchall()
184
+ return [dict(row) for row in rows]
185
+
186
+ def get_db_path(self) -> str:
187
+ """Return the path to the SQLite database file."""
188
+ return self.db_path