loki-mode 6.83.1 → 7.0.1
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 +62 -11
- package/VERSION +1 -1
- package/agents/managed_registry.py +246 -0
- package/agents/types.json +330 -0
- package/autonomy/completion-council.sh +226 -0
- package/autonomy/loki +346 -15
- package/autonomy/run.sh +357 -1
- package/dashboard/__init__.py +1 -1
- package/dashboard/server.py +235 -0
- package/docs/INSTALLATION.md +1 -1
- package/mcp/__init__.py +1 -1
- package/mcp/managed_tools.py +234 -0
- package/mcp/server.py +22 -0
- package/memory/managed_memory/__init__.py +9 -0
- package/memory/managed_memory/retrieve.py +237 -1
- package/package.json +4 -2
- package/providers/managed.py +789 -0
- package/skills/00-index.md +1 -0
- package/skills/memory.md +185 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"""MCP tools for Managed Agents Memory (PII redaction, read proxies).
|
|
2
|
+
|
|
3
|
+
This module hosts the actual implementation of the loki_memory_redact tool.
|
|
4
|
+
The logic lives here -- rather than inline in mcp/server.py -- so unit tests
|
|
5
|
+
can import and exercise ``redact_memory_versions`` directly without having
|
|
6
|
+
to load the full MCP FastMCP runtime.
|
|
7
|
+
|
|
8
|
+
Registration pattern:
|
|
9
|
+
from mcp.managed_tools import register_managed_tools
|
|
10
|
+
register_managed_tools(mcp_server) # Called from mcp/server.py
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import re
|
|
17
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
_VALID_SCOPES = ("user", "org", "all")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _store_scope(store: Any) -> str:
|
|
24
|
+
if isinstance(store, dict):
|
|
25
|
+
return (store.get("scope") or "").lower()
|
|
26
|
+
return (getattr(store, "scope", "") or "").lower()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _store_id(store: Any) -> Optional[str]:
|
|
30
|
+
if isinstance(store, dict):
|
|
31
|
+
return store.get("id") or store.get("store_id")
|
|
32
|
+
return getattr(store, "id", None) or getattr(store, "store_id", None)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _version_to_dict(version: Any) -> Dict[str, Any]:
|
|
36
|
+
if isinstance(version, dict):
|
|
37
|
+
return version
|
|
38
|
+
to_dict = getattr(version, "model_dump", None) or getattr(version, "dict", None)
|
|
39
|
+
if callable(to_dict):
|
|
40
|
+
try:
|
|
41
|
+
return to_dict()
|
|
42
|
+
except Exception:
|
|
43
|
+
return {"raw": str(version)}
|
|
44
|
+
return {"raw": str(version)}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _resolve_sdk(client: Any) -> Tuple[Any, Any, Any]:
|
|
48
|
+
"""
|
|
49
|
+
Return (stores_list_fn, versions_list_fn, redact_fn) or raise RuntimeError.
|
|
50
|
+
"""
|
|
51
|
+
beta = getattr(client._client, "beta", None) # type: ignore[attr-defined]
|
|
52
|
+
memory_stores = getattr(beta, "memory_stores", None) if beta is not None else None
|
|
53
|
+
stores_list_fn = (
|
|
54
|
+
getattr(memory_stores, "list", None) if memory_stores is not None else None
|
|
55
|
+
)
|
|
56
|
+
memory_versions = (
|
|
57
|
+
getattr(memory_stores, "memory_versions", None)
|
|
58
|
+
if memory_stores is not None else None
|
|
59
|
+
)
|
|
60
|
+
versions_list_fn = (
|
|
61
|
+
getattr(memory_versions, "list", None) if memory_versions is not None else None
|
|
62
|
+
)
|
|
63
|
+
redact_fn = (
|
|
64
|
+
getattr(memory_versions, "redact", None) if memory_versions is not None else None
|
|
65
|
+
)
|
|
66
|
+
if versions_list_fn is None or redact_fn is None:
|
|
67
|
+
raise RuntimeError(
|
|
68
|
+
"memory_versions.list / memory_versions.redact not available in SDK"
|
|
69
|
+
)
|
|
70
|
+
return stores_list_fn, versions_list_fn, redact_fn
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def redact_memory_versions(
|
|
74
|
+
pattern: str,
|
|
75
|
+
scope: str = "all",
|
|
76
|
+
) -> Dict[str, Any]:
|
|
77
|
+
"""
|
|
78
|
+
Redact memory versions whose content matches ``pattern`` (regex).
|
|
79
|
+
|
|
80
|
+
Hard requirements:
|
|
81
|
+
- LOKI_MANAGED_AGENTS=true AND LOKI_MANAGED_MEMORY=true
|
|
82
|
+
(otherwise raises ManagedDisabled).
|
|
83
|
+
|
|
84
|
+
Soft failures (returned as structured dicts, never raise):
|
|
85
|
+
- invalid regex -> {"error": "...", "redacted_count": 0}
|
|
86
|
+
- invalid scope -> {"error": "...", "redacted_count": 0}
|
|
87
|
+
- per-store / per-redact SDK errors -> collected in "errors"
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
{"redacted_count": int, "scanned": int, "errors": [...]}.
|
|
91
|
+
"""
|
|
92
|
+
if scope not in _VALID_SCOPES:
|
|
93
|
+
return {
|
|
94
|
+
"error": f"invalid scope '{scope}'; expected one of "
|
|
95
|
+
+ "|".join(_VALID_SCOPES),
|
|
96
|
+
"redacted_count": 0,
|
|
97
|
+
"errors": [],
|
|
98
|
+
"scanned": 0,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
compiled = re.compile(pattern)
|
|
103
|
+
except re.error as e:
|
|
104
|
+
return {
|
|
105
|
+
"error": f"invalid regex: {e}",
|
|
106
|
+
"redacted_count": 0,
|
|
107
|
+
"errors": [],
|
|
108
|
+
"scanned": 0,
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
# Hard flag check: raise so MCP callers see the ManagedDisabled exception
|
|
112
|
+
# path rather than a silent no-op.
|
|
113
|
+
from memory.managed_memory import ManagedDisabled, is_enabled
|
|
114
|
+
from memory.managed_memory.client import get_client
|
|
115
|
+
from memory.managed_memory.events import emit_managed_event
|
|
116
|
+
|
|
117
|
+
if not is_enabled():
|
|
118
|
+
raise ManagedDisabled(
|
|
119
|
+
"loki_memory_redact requires LOKI_MANAGED_AGENTS=true and "
|
|
120
|
+
"LOKI_MANAGED_MEMORY=true"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
client = get_client()
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
stores_list_fn, versions_list_fn, redact_fn = _resolve_sdk(client)
|
|
127
|
+
except RuntimeError as exc:
|
|
128
|
+
return {
|
|
129
|
+
"error": str(exc),
|
|
130
|
+
"redacted_count": 0,
|
|
131
|
+
"errors": [],
|
|
132
|
+
"scanned": 0,
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
errors: List[Dict[str, Any]] = []
|
|
136
|
+
redacted_count = 0
|
|
137
|
+
scanned = 0
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
stores_result = stores_list_fn() if stores_list_fn is not None else []
|
|
141
|
+
stores_data = getattr(stores_result, "data", stores_result) or []
|
|
142
|
+
except Exception as e:
|
|
143
|
+
errors.append({"op": "stores_list", "error": str(e)})
|
|
144
|
+
stores_data = []
|
|
145
|
+
|
|
146
|
+
for store in stores_data:
|
|
147
|
+
if scope != "all" and _store_scope(store) != scope:
|
|
148
|
+
continue
|
|
149
|
+
sid = _store_id(store)
|
|
150
|
+
if not sid:
|
|
151
|
+
continue
|
|
152
|
+
try:
|
|
153
|
+
versions_result = versions_list_fn(store_id=sid)
|
|
154
|
+
versions_data = getattr(versions_result, "data", versions_result) or []
|
|
155
|
+
except Exception as e:
|
|
156
|
+
errors.append({"op": "versions_list", "store_id": sid, "error": str(e)})
|
|
157
|
+
continue
|
|
158
|
+
|
|
159
|
+
for version in versions_data:
|
|
160
|
+
scanned += 1
|
|
161
|
+
vdict = _version_to_dict(version)
|
|
162
|
+
content = vdict.get("content") or vdict.get("text") or ""
|
|
163
|
+
if not isinstance(content, str):
|
|
164
|
+
try:
|
|
165
|
+
content = json.dumps(content, default=str)
|
|
166
|
+
except Exception:
|
|
167
|
+
content = str(content)
|
|
168
|
+
if not compiled.search(content):
|
|
169
|
+
continue
|
|
170
|
+
vid = (
|
|
171
|
+
vdict.get("id")
|
|
172
|
+
or vdict.get("memory_version_id")
|
|
173
|
+
or vdict.get("version_id")
|
|
174
|
+
)
|
|
175
|
+
if not vid:
|
|
176
|
+
errors.append(
|
|
177
|
+
{"op": "redact", "store_id": sid, "error": "no version id"}
|
|
178
|
+
)
|
|
179
|
+
continue
|
|
180
|
+
try:
|
|
181
|
+
redact_fn(store_id=sid, memory_version_id=vid)
|
|
182
|
+
redacted_count += 1
|
|
183
|
+
try:
|
|
184
|
+
emit_managed_event(
|
|
185
|
+
"managed_memory_redact",
|
|
186
|
+
{
|
|
187
|
+
"store_id": sid,
|
|
188
|
+
"memory_version_id": vid,
|
|
189
|
+
"scope": scope,
|
|
190
|
+
"pattern": pattern,
|
|
191
|
+
},
|
|
192
|
+
)
|
|
193
|
+
except Exception:
|
|
194
|
+
pass
|
|
195
|
+
except Exception as e:
|
|
196
|
+
errors.append(
|
|
197
|
+
{
|
|
198
|
+
"op": "redact",
|
|
199
|
+
"store_id": sid,
|
|
200
|
+
"memory_version_id": vid,
|
|
201
|
+
"error": str(e),
|
|
202
|
+
}
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
"redacted_count": redacted_count,
|
|
207
|
+
"scanned": scanned,
|
|
208
|
+
"errors": errors,
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def register_managed_tools(mcp) -> None:
|
|
213
|
+
"""Attach managed-memory MCP tools to a FastMCP instance."""
|
|
214
|
+
|
|
215
|
+
@mcp.tool()
|
|
216
|
+
async def loki_memory_redact(pattern: str, scope: str = "all") -> str:
|
|
217
|
+
"""
|
|
218
|
+
Redact memory versions in the managed-agents store whose content matches a regex.
|
|
219
|
+
|
|
220
|
+
Iterates memory versions within the requested scope and calls
|
|
221
|
+
``client.beta.memory_stores.memory_versions.redact(...)`` for each
|
|
222
|
+
match. Requires ``LOKI_MANAGED_AGENTS=true`` and
|
|
223
|
+
``LOKI_MANAGED_MEMORY=true`` -- otherwise raises ``ManagedDisabled``.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
pattern: Python regex compiled with ``re.search`` against each
|
|
227
|
+
version's content.
|
|
228
|
+
scope: One of ``user``, ``org``, or ``all`` (default).
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
JSON ``{"redacted_count": int, "errors": [...], "scanned": int}``.
|
|
232
|
+
"""
|
|
233
|
+
result = redact_memory_versions(pattern=pattern, scope=scope)
|
|
234
|
+
return json.dumps(result)
|
package/mcp/server.py
CHANGED
|
@@ -2059,6 +2059,28 @@ async def loki_phase_report() -> str:
|
|
|
2059
2059
|
Use loki_state_get and loki_task_queue_list to gather data."""
|
|
2060
2060
|
|
|
2061
2061
|
|
|
2062
|
+
# ============================================================
|
|
2063
|
+
# MANAGED MEMORY TOOLS (PII redaction, read proxy)
|
|
2064
|
+
#
|
|
2065
|
+
# The actual implementation lives in mcp/managed_tools.py so unit tests can
|
|
2066
|
+
# import the core redact function without booting the FastMCP runtime.
|
|
2067
|
+
# loki_memory_redact appears below for grep-ability and is a thin wrapper.
|
|
2068
|
+
# ============================================================
|
|
2069
|
+
|
|
2070
|
+
try:
|
|
2071
|
+
from mcp.managed_tools import register_managed_tools
|
|
2072
|
+
register_managed_tools(mcp)
|
|
2073
|
+
# Emit tool-call events by wrapping the registered tool's underlying
|
|
2074
|
+
# callable. We reference loki_memory_redact by name here for discoverability.
|
|
2075
|
+
_MANAGED_MEMORY_TOOLS = ("loki_memory_redact",)
|
|
2076
|
+
except Exception as _managed_err:
|
|
2077
|
+
import sys as _sys
|
|
2078
|
+
print(
|
|
2079
|
+
f"[warn] managed_tools registration skipped: {_managed_err}",
|
|
2080
|
+
file=_sys.stderr,
|
|
2081
|
+
)
|
|
2082
|
+
|
|
2083
|
+
|
|
2062
2084
|
# ============================================================
|
|
2063
2085
|
# MAGIC MODULES TOOLS (spec-driven component generation)
|
|
2064
2086
|
# ============================================================
|
|
@@ -100,10 +100,19 @@ def hydrate_patterns(local_mtime_floor: float):
|
|
|
100
100
|
return _r.hydrate_patterns(local_mtime_floor)
|
|
101
101
|
|
|
102
102
|
|
|
103
|
+
def hydrate(namespace: Optional[str] = None, mtime_floor: Optional[float] = None):
|
|
104
|
+
"""Session-boot hydrate (patterns + skills). No-op when disabled."""
|
|
105
|
+
if not is_enabled():
|
|
106
|
+
return {"patterns": 0, "skills": 0, "skipped": True}
|
|
107
|
+
from . import retrieve as _r
|
|
108
|
+
return _r.hydrate(namespace=namespace, mtime_floor=mtime_floor)
|
|
109
|
+
|
|
110
|
+
|
|
103
111
|
__all__ = [
|
|
104
112
|
"BETA_HEADER",
|
|
105
113
|
"ManagedDisabled",
|
|
106
114
|
"emit_managed_event",
|
|
115
|
+
"hydrate",
|
|
107
116
|
"hydrate_patterns",
|
|
108
117
|
"is_enabled",
|
|
109
118
|
"probe_beta_header",
|
|
@@ -284,6 +284,234 @@ def hydrate_patterns(
|
|
|
284
284
|
return merged
|
|
285
285
|
|
|
286
286
|
|
|
287
|
+
# ---------------------------------------------------------------------------
|
|
288
|
+
# Hydrate procedural skills
|
|
289
|
+
# ---------------------------------------------------------------------------
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def hydrate_skills(
|
|
293
|
+
local_mtime_floor: float,
|
|
294
|
+
target_dir: Optional[str] = None,
|
|
295
|
+
) -> int:
|
|
296
|
+
"""
|
|
297
|
+
Pull procedural skills from the managed store and merge them into
|
|
298
|
+
.loki/memory/skills/{name}.json (one file per skill). Returns the number
|
|
299
|
+
of skill files written. Returns 0 on disabled / error.
|
|
300
|
+
|
|
301
|
+
Only skills whose remote timestamp is newer than `local_mtime_floor` are
|
|
302
|
+
merged. Local wins on conflict: a skill whose filename already exists is
|
|
303
|
+
NOT overwritten.
|
|
304
|
+
"""
|
|
305
|
+
if not is_enabled():
|
|
306
|
+
return 0
|
|
307
|
+
|
|
308
|
+
try:
|
|
309
|
+
client = _get_client()
|
|
310
|
+
except ManagedDisabled as e:
|
|
311
|
+
emit_managed_event(
|
|
312
|
+
"managed_agents_fallback",
|
|
313
|
+
{"reason": "client_unavailable", "detail": str(e), "op": "hydrate_skills"},
|
|
314
|
+
)
|
|
315
|
+
return 0
|
|
316
|
+
except Exception as e: # pragma: no cover
|
|
317
|
+
emit_managed_event(
|
|
318
|
+
"managed_agents_fallback",
|
|
319
|
+
{"reason": "client_error", "detail": str(e), "op": "hydrate_skills"},
|
|
320
|
+
)
|
|
321
|
+
return 0
|
|
322
|
+
|
|
323
|
+
try:
|
|
324
|
+
store = client.stores_get_or_create(
|
|
325
|
+
name=_store_name(),
|
|
326
|
+
description="Loki Mode RARV-C shadow-write store (v6.83.0)",
|
|
327
|
+
scope="project",
|
|
328
|
+
)
|
|
329
|
+
store_id = store.get("id") or store.get("store_id")
|
|
330
|
+
if not store_id:
|
|
331
|
+
return 0
|
|
332
|
+
entries = client.memories_list(store_id=store_id, path_prefix="skills/")
|
|
333
|
+
except Exception as e:
|
|
334
|
+
emit_managed_event(
|
|
335
|
+
"managed_agents_fallback",
|
|
336
|
+
{"reason": "list_error", "detail": str(e), "op": "hydrate_skills"},
|
|
337
|
+
)
|
|
338
|
+
return 0
|
|
339
|
+
|
|
340
|
+
target_dir = target_dir or os.environ.get("LOKI_TARGET_DIR") or os.getcwd()
|
|
341
|
+
skills_dir = Path(target_dir) / ".loki" / "memory" / "skills"
|
|
342
|
+
skills_dir.mkdir(parents=True, exist_ok=True)
|
|
343
|
+
|
|
344
|
+
merged = 0
|
|
345
|
+
for e in entries:
|
|
346
|
+
content = e.get("content")
|
|
347
|
+
if not content:
|
|
348
|
+
continue
|
|
349
|
+
try:
|
|
350
|
+
skill = json.loads(content)
|
|
351
|
+
except (TypeError, json.JSONDecodeError):
|
|
352
|
+
continue
|
|
353
|
+
sid = skill.get("id") or skill.get("skill_id")
|
|
354
|
+
name = skill.get("name") or sid
|
|
355
|
+
if not name:
|
|
356
|
+
continue
|
|
357
|
+
|
|
358
|
+
# Sanitize filename (mirror MemoryStorage.save_skill).
|
|
359
|
+
safe_name = "".join(
|
|
360
|
+
c if c.isalnum() or c in "-_" else "_" for c in str(name)
|
|
361
|
+
)
|
|
362
|
+
skill_path = skills_dir / f"{safe_name}.json"
|
|
363
|
+
if skill_path.exists():
|
|
364
|
+
# Local wins on conflict.
|
|
365
|
+
continue
|
|
366
|
+
|
|
367
|
+
# Optional mtime gate.
|
|
368
|
+
ts = skill.get("updated_at") or skill.get("created_at")
|
|
369
|
+
if ts and local_mtime_floor:
|
|
370
|
+
try:
|
|
371
|
+
if isinstance(ts, (int, float)) and float(ts) < local_mtime_floor:
|
|
372
|
+
continue
|
|
373
|
+
except (TypeError, ValueError):
|
|
374
|
+
pass
|
|
375
|
+
|
|
376
|
+
try:
|
|
377
|
+
from memory.storage import MemoryStorage # type: ignore
|
|
378
|
+
|
|
379
|
+
storage = MemoryStorage(str(skills_dir.parent))
|
|
380
|
+
storage._atomic_write(skill_path, skill)
|
|
381
|
+
except Exception:
|
|
382
|
+
import tempfile
|
|
383
|
+
|
|
384
|
+
fd, tmp = tempfile.mkstemp(
|
|
385
|
+
dir=str(skills_dir), prefix=".tmp_", suffix=".json"
|
|
386
|
+
)
|
|
387
|
+
try:
|
|
388
|
+
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
|
389
|
+
json.dump(skill, f, indent=2, default=str)
|
|
390
|
+
os.replace(tmp, skill_path)
|
|
391
|
+
except Exception as ex:
|
|
392
|
+
if os.path.exists(tmp):
|
|
393
|
+
os.unlink(tmp)
|
|
394
|
+
emit_managed_event(
|
|
395
|
+
"managed_agents_fallback",
|
|
396
|
+
{"reason": "atomic_write_failed", "detail": str(ex), "op": "hydrate_skills"},
|
|
397
|
+
)
|
|
398
|
+
continue
|
|
399
|
+
merged += 1
|
|
400
|
+
|
|
401
|
+
emit_managed_event(
|
|
402
|
+
"managed_memory_hydrate_skills",
|
|
403
|
+
{"merged": merged, "candidates": len(entries)},
|
|
404
|
+
)
|
|
405
|
+
return merged
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
# ---------------------------------------------------------------------------
|
|
409
|
+
# Session hydrate (patterns + skills) with idempotency guard
|
|
410
|
+
# ---------------------------------------------------------------------------
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
_HYDRATE_SENTINEL = ".loki/managed/hydrate.lock"
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def _already_hydrated_this_session(target_dir: str) -> bool:
|
|
417
|
+
"""Idempotent: once we write the sentinel file, a second hydrate is no-op."""
|
|
418
|
+
sentinel = Path(target_dir) / _HYDRATE_SENTINEL
|
|
419
|
+
return sentinel.exists()
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def _mark_hydrated(target_dir: str) -> None:
|
|
423
|
+
sentinel = Path(target_dir) / _HYDRATE_SENTINEL
|
|
424
|
+
sentinel.parent.mkdir(parents=True, exist_ok=True)
|
|
425
|
+
try:
|
|
426
|
+
sentinel.write_text(str(int(time.time())), encoding="utf-8")
|
|
427
|
+
except OSError:
|
|
428
|
+
pass
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def hydrate(
|
|
432
|
+
namespace: Optional[str] = None,
|
|
433
|
+
mtime_floor: Optional[float] = None,
|
|
434
|
+
target_dir: Optional[str] = None,
|
|
435
|
+
) -> Dict[str, int]:
|
|
436
|
+
"""
|
|
437
|
+
Session-boot hydrate: pull semantic patterns AND procedural skills from
|
|
438
|
+
the managed store and merge them into local .loki/memory/. Emits a single
|
|
439
|
+
`managed_memory_hydrate` event with counts.
|
|
440
|
+
|
|
441
|
+
Args:
|
|
442
|
+
namespace: Optional logical namespace label; reserved for multi-tenant
|
|
443
|
+
stores (not yet used by the backend). Included in the event for
|
|
444
|
+
observability.
|
|
445
|
+
mtime_floor: Only merge remote entries updated after this epoch
|
|
446
|
+
timestamp. Defaults to 0.0 (pull everything not already local).
|
|
447
|
+
target_dir: Override .loki root; defaults to LOKI_TARGET_DIR or cwd.
|
|
448
|
+
|
|
449
|
+
Returns:
|
|
450
|
+
{"patterns": N, "skills": M, "skipped": bool}. Disabled flags / errors
|
|
451
|
+
return {"patterns": 0, "skills": 0, "skipped": True/False}.
|
|
452
|
+
|
|
453
|
+
Idempotent: a second call within the same session (while the lock file
|
|
454
|
+
exists) short-circuits and returns zero counts with skipped=True.
|
|
455
|
+
"""
|
|
456
|
+
target_dir = target_dir or os.environ.get("LOKI_TARGET_DIR") or os.getcwd()
|
|
457
|
+
|
|
458
|
+
if not is_enabled():
|
|
459
|
+
return {"patterns": 0, "skills": 0, "skipped": True}
|
|
460
|
+
|
|
461
|
+
if _already_hydrated_this_session(target_dir):
|
|
462
|
+
emit_managed_event(
|
|
463
|
+
"managed_memory_hydrate",
|
|
464
|
+
{
|
|
465
|
+
"patterns": 0,
|
|
466
|
+
"skills": 0,
|
|
467
|
+
"skipped": True,
|
|
468
|
+
"reason": "already_hydrated_this_session",
|
|
469
|
+
"namespace": namespace or "",
|
|
470
|
+
},
|
|
471
|
+
)
|
|
472
|
+
return {"patterns": 0, "skills": 0, "skipped": True}
|
|
473
|
+
|
|
474
|
+
floor = float(mtime_floor) if mtime_floor is not None else 0.0
|
|
475
|
+
|
|
476
|
+
patterns_merged = 0
|
|
477
|
+
skills_merged = 0
|
|
478
|
+
try:
|
|
479
|
+
patterns_merged = hydrate_patterns(
|
|
480
|
+
local_mtime_floor=floor, target_dir=target_dir
|
|
481
|
+
)
|
|
482
|
+
except Exception as e: # pragma: no cover - defensive
|
|
483
|
+
emit_managed_event(
|
|
484
|
+
"managed_agents_fallback",
|
|
485
|
+
{"reason": "hydrate_patterns_error", "detail": str(e), "op": "hydrate"},
|
|
486
|
+
)
|
|
487
|
+
try:
|
|
488
|
+
skills_merged = hydrate_skills(
|
|
489
|
+
local_mtime_floor=floor, target_dir=target_dir
|
|
490
|
+
)
|
|
491
|
+
except Exception as e: # pragma: no cover - defensive
|
|
492
|
+
emit_managed_event(
|
|
493
|
+
"managed_agents_fallback",
|
|
494
|
+
{"reason": "hydrate_skills_error", "detail": str(e), "op": "hydrate"},
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
_mark_hydrated(target_dir)
|
|
498
|
+
|
|
499
|
+
emit_managed_event(
|
|
500
|
+
"managed_memory_hydrate",
|
|
501
|
+
{
|
|
502
|
+
"patterns": patterns_merged,
|
|
503
|
+
"skills": skills_merged,
|
|
504
|
+
"skipped": False,
|
|
505
|
+
"namespace": namespace or "",
|
|
506
|
+
},
|
|
507
|
+
)
|
|
508
|
+
return {
|
|
509
|
+
"patterns": patterns_merged,
|
|
510
|
+
"skills": skills_merged,
|
|
511
|
+
"skipped": False,
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
|
|
287
515
|
# ---------------------------------------------------------------------------
|
|
288
516
|
# Module CLI
|
|
289
517
|
# ---------------------------------------------------------------------------
|
|
@@ -319,7 +547,15 @@ def _main(argv: Optional[list] = None) -> int:
|
|
|
319
547
|
floor = 0.0
|
|
320
548
|
if args.since_seconds and args.since_seconds > 0:
|
|
321
549
|
floor = time.time() - args.since_seconds
|
|
322
|
-
|
|
550
|
+
# Phase 2: session-boot hydrate covers patterns + skills and is
|
|
551
|
+
# idempotent (sentinel-guarded). Prints a one-line summary to
|
|
552
|
+
# stdout so callers can log counts without parsing JSON.
|
|
553
|
+
result = hydrate(mtime_floor=floor)
|
|
554
|
+
print(
|
|
555
|
+
f"[managed] hydrate patterns={result.get('patterns', 0)} "
|
|
556
|
+
f"skills={result.get('skills', 0)} "
|
|
557
|
+
f"skipped={result.get('skipped', False)}"
|
|
558
|
+
)
|
|
323
559
|
return 0
|
|
324
560
|
|
|
325
561
|
query = args.query or ""
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "loki-mode",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "7.0.1",
|
|
4
4
|
"description": "Loki Mode by Autonomi - Multi-agent autonomous startup system for Claude Code, Codex CLI, and Gemini CLI",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent",
|
|
@@ -67,6 +67,7 @@
|
|
|
67
67
|
"VERSION",
|
|
68
68
|
"autonomy/",
|
|
69
69
|
"providers/",
|
|
70
|
+
"agents/",
|
|
70
71
|
"skills/",
|
|
71
72
|
"references/",
|
|
72
73
|
"docs/**/*.md",
|
|
@@ -105,7 +106,8 @@
|
|
|
105
106
|
"test:visual": "node --experimental-vm-modules node_modules/jest/bin/jest.js dashboard-ui/tests/visual-regression.test.js",
|
|
106
107
|
"test:parity": "node --experimental-vm-modules dashboard-ui/scripts/check-parity.js",
|
|
107
108
|
"test:parity:json": "node --experimental-vm-modules dashboard-ui/scripts/check-parity.js --json",
|
|
108
|
-
"test:dashboard": "npm run test:visual && npm run test:parity"
|
|
109
|
+
"test:dashboard": "npm run test:visual && npm run test:parity",
|
|
110
|
+
"test:integration": "bash tests/integration/run_integration_suite.sh"
|
|
109
111
|
},
|
|
110
112
|
"engines": {
|
|
111
113
|
"node": ">=18.0.0"
|