loki-mode 6.81.1 → 6.83.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/completion-council.sh +58 -0
- package/autonomy/run.sh +334 -44
- package/dashboard/__init__.py +1 -1
- package/docs/INSTALLATION.md +1 -1
- package/mcp/__init__.py +1 -1
- package/mcp/requirements.txt +1 -0
- package/mcp/server.py +105 -0
- package/memory/managed_memory/__init__.py +113 -0
- package/memory/managed_memory/_beta.py +11 -0
- package/memory/managed_memory/client.py +210 -0
- package/memory/managed_memory/events.py +79 -0
- package/memory/managed_memory/fakes.py +120 -0
- package/memory/managed_memory/retrieve.py +347 -0
- package/memory/managed_memory/shadow_write.py +350 -0
- package/package.json +2 -2
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Loki Managed Agents Memory - Retrieve path (v6.83.0 Phase 1).
|
|
3
|
+
|
|
4
|
+
Used by the REASON phase (retrieve_memory_context in autonomy/run.sh) and by
|
|
5
|
+
the completion council (augment prompts with prior verdicts). Also provides
|
|
6
|
+
hydrate_patterns() which pulls semantic patterns updated after a local mtime
|
|
7
|
+
floor and merges them into .loki/memory/semantic/patterns.json via the
|
|
8
|
+
existing MemoryStorage._atomic_write.
|
|
9
|
+
|
|
10
|
+
CLI:
|
|
11
|
+
python3 -m memory.managed_memory.retrieve --query <str> [--top-k N] \\
|
|
12
|
+
[--store-id <id>]
|
|
13
|
+
|
|
14
|
+
python3 -m memory.managed_memory.retrieve --hydrate [--since-seconds N]
|
|
15
|
+
|
|
16
|
+
Never raises: on any error the CLI prints nothing and exits 0. Bash callers
|
|
17
|
+
apply a 5s hard timeout; this module keeps its own 10s SDK timeout.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import argparse
|
|
23
|
+
import json
|
|
24
|
+
import logging
|
|
25
|
+
import os
|
|
26
|
+
import sys
|
|
27
|
+
import time
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
from typing import Any, Dict, List, Optional
|
|
30
|
+
|
|
31
|
+
from . import ManagedDisabled, is_enabled
|
|
32
|
+
from .events import emit_managed_event
|
|
33
|
+
|
|
34
|
+
_LOG = logging.getLogger("loki.managed_memory.retrieve")
|
|
35
|
+
|
|
36
|
+
_DEFAULT_STORE_NAME = "loki-rarv-c-learnings"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _store_name() -> str:
|
|
40
|
+
return os.environ.get("LOKI_MANAGED_STORE_NAME", _DEFAULT_STORE_NAME)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _warn_once(msg: str) -> None:
|
|
44
|
+
sys.stderr.write(f"WARN [managed_memory] {msg}\n")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _get_client():
|
|
48
|
+
from .client import get_client
|
|
49
|
+
|
|
50
|
+
return get_client()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _summarize(content: str, limit: int = 240) -> str:
|
|
54
|
+
"""Return a short single-line summary for prompt injection."""
|
|
55
|
+
s = (content or "").strip().replace("\n", " ")
|
|
56
|
+
if len(s) > limit:
|
|
57
|
+
s = s[: limit - 3] + "..."
|
|
58
|
+
return s
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# ---------------------------------------------------------------------------
|
|
62
|
+
# Retrieve related verdicts (used by REASON and completion council)
|
|
63
|
+
# ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def retrieve_related_verdicts(
|
|
67
|
+
query: str,
|
|
68
|
+
top_k: int = 3,
|
|
69
|
+
store_id: Optional[str] = None,
|
|
70
|
+
) -> List[Dict[str, Any]]:
|
|
71
|
+
"""
|
|
72
|
+
Return up to `top_k` recent verdicts most-related to `query`.
|
|
73
|
+
|
|
74
|
+
Phase 1 implementation: fetch memories under `verdicts/` and
|
|
75
|
+
prefix-match. This is naive but honest -- true similarity retrieval
|
|
76
|
+
ships in Phase 2 when the beta exposes a semantic search endpoint.
|
|
77
|
+
Returns [] on any error.
|
|
78
|
+
"""
|
|
79
|
+
if not is_enabled():
|
|
80
|
+
return []
|
|
81
|
+
|
|
82
|
+
start = time.monotonic()
|
|
83
|
+
try:
|
|
84
|
+
client = _get_client()
|
|
85
|
+
except ManagedDisabled as e:
|
|
86
|
+
emit_managed_event(
|
|
87
|
+
"managed_agents_fallback",
|
|
88
|
+
{"reason": "client_unavailable", "detail": str(e), "op": "retrieve"},
|
|
89
|
+
)
|
|
90
|
+
return []
|
|
91
|
+
except Exception as e: # pragma: no cover - defensive
|
|
92
|
+
emit_managed_event(
|
|
93
|
+
"managed_agents_fallback",
|
|
94
|
+
{"reason": "client_error", "detail": str(e), "op": "retrieve"},
|
|
95
|
+
)
|
|
96
|
+
return []
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
if not store_id:
|
|
100
|
+
store = client.stores_get_or_create(
|
|
101
|
+
name=_store_name(),
|
|
102
|
+
description="Loki Mode RARV-C shadow-write store (v6.83.0)",
|
|
103
|
+
scope="project",
|
|
104
|
+
)
|
|
105
|
+
store_id = store.get("id") or store.get("store_id")
|
|
106
|
+
if not store_id:
|
|
107
|
+
return []
|
|
108
|
+
entries = client.memories_list(store_id=store_id, path_prefix="verdicts/")
|
|
109
|
+
except Exception as e:
|
|
110
|
+
_warn_once(f"memories_list failed ({e}); returning empty")
|
|
111
|
+
emit_managed_event(
|
|
112
|
+
"managed_agents_fallback",
|
|
113
|
+
{"reason": "list_error", "detail": str(e), "op": "retrieve"},
|
|
114
|
+
)
|
|
115
|
+
return []
|
|
116
|
+
|
|
117
|
+
# Naive ranking: keep entries whose content contains any query token.
|
|
118
|
+
tokens = [t.lower() for t in query.split() if len(t) > 2]
|
|
119
|
+
scored: List[tuple] = []
|
|
120
|
+
for e in entries:
|
|
121
|
+
content = e.get("content") or ""
|
|
122
|
+
content_lc = content.lower()
|
|
123
|
+
score = sum(1 for t in tokens if t in content_lc)
|
|
124
|
+
scored.append((score, e))
|
|
125
|
+
scored.sort(key=lambda pair: pair[0], reverse=True)
|
|
126
|
+
|
|
127
|
+
out: List[Dict[str, Any]] = []
|
|
128
|
+
for score, e in scored[:top_k]:
|
|
129
|
+
out.append(
|
|
130
|
+
{
|
|
131
|
+
"path": e.get("path"),
|
|
132
|
+
"content_summary": _summarize(e.get("content") or ""),
|
|
133
|
+
"version_id": e.get("version") or e.get("version_id"),
|
|
134
|
+
"score": score,
|
|
135
|
+
}
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
emit_managed_event(
|
|
139
|
+
"managed_memory_retrieve",
|
|
140
|
+
{
|
|
141
|
+
"query_tokens": len(tokens),
|
|
142
|
+
"hits": len(out),
|
|
143
|
+
"total_candidates": len(entries),
|
|
144
|
+
"elapsed_ms": int((time.monotonic() - start) * 1000),
|
|
145
|
+
},
|
|
146
|
+
)
|
|
147
|
+
return out
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
# ---------------------------------------------------------------------------
|
|
151
|
+
# Hydrate semantic patterns
|
|
152
|
+
# ---------------------------------------------------------------------------
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def hydrate_patterns(
|
|
156
|
+
local_mtime_floor: float,
|
|
157
|
+
target_dir: Optional[str] = None,
|
|
158
|
+
) -> int:
|
|
159
|
+
"""
|
|
160
|
+
Pull semantic patterns from the managed store and merge them into
|
|
161
|
+
.loki/memory/semantic/patterns.json. Returns the number of patterns
|
|
162
|
+
merged in. Returns 0 on disabled / error.
|
|
163
|
+
|
|
164
|
+
Only patterns whose remote version timestamp is newer than
|
|
165
|
+
`local_mtime_floor` are merged. The merge is additive: existing local
|
|
166
|
+
patterns with the same pattern_id are kept unchanged.
|
|
167
|
+
"""
|
|
168
|
+
if not is_enabled():
|
|
169
|
+
return 0
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
client = _get_client()
|
|
173
|
+
except ManagedDisabled as e:
|
|
174
|
+
emit_managed_event(
|
|
175
|
+
"managed_agents_fallback",
|
|
176
|
+
{"reason": "client_unavailable", "detail": str(e), "op": "hydrate"},
|
|
177
|
+
)
|
|
178
|
+
return 0
|
|
179
|
+
except Exception as e: # pragma: no cover
|
|
180
|
+
emit_managed_event(
|
|
181
|
+
"managed_agents_fallback",
|
|
182
|
+
{"reason": "client_error", "detail": str(e), "op": "hydrate"},
|
|
183
|
+
)
|
|
184
|
+
return 0
|
|
185
|
+
|
|
186
|
+
try:
|
|
187
|
+
store = client.stores_get_or_create(
|
|
188
|
+
name=_store_name(),
|
|
189
|
+
description="Loki Mode RARV-C shadow-write store (v6.83.0)",
|
|
190
|
+
scope="project",
|
|
191
|
+
)
|
|
192
|
+
store_id = store.get("id") or store.get("store_id")
|
|
193
|
+
if not store_id:
|
|
194
|
+
return 0
|
|
195
|
+
entries = client.memories_list(store_id=store_id, path_prefix="patterns/")
|
|
196
|
+
except Exception as e:
|
|
197
|
+
emit_managed_event(
|
|
198
|
+
"managed_agents_fallback",
|
|
199
|
+
{"reason": "list_error", "detail": str(e), "op": "hydrate"},
|
|
200
|
+
)
|
|
201
|
+
return 0
|
|
202
|
+
|
|
203
|
+
target_dir = target_dir or os.environ.get("LOKI_TARGET_DIR") or os.getcwd()
|
|
204
|
+
patterns_path = Path(target_dir) / ".loki" / "memory" / "semantic" / "patterns.json"
|
|
205
|
+
|
|
206
|
+
# Read existing local patterns file (may not exist).
|
|
207
|
+
existing: Dict[str, Any] = {"patterns": {}}
|
|
208
|
+
if patterns_path.exists():
|
|
209
|
+
try:
|
|
210
|
+
with open(patterns_path, "r", encoding="utf-8") as f:
|
|
211
|
+
existing = json.load(f)
|
|
212
|
+
except (OSError, json.JSONDecodeError):
|
|
213
|
+
existing = {"patterns": {}}
|
|
214
|
+
if "patterns" not in existing or not isinstance(existing["patterns"], dict):
|
|
215
|
+
existing["patterns"] = {}
|
|
216
|
+
|
|
217
|
+
merged = 0
|
|
218
|
+
for e in entries:
|
|
219
|
+
content = e.get("content")
|
|
220
|
+
if not content:
|
|
221
|
+
continue
|
|
222
|
+
try:
|
|
223
|
+
pat = json.loads(content)
|
|
224
|
+
except (TypeError, json.JSONDecodeError):
|
|
225
|
+
continue
|
|
226
|
+
pid = pat.get("pattern_id") or pat.get("id")
|
|
227
|
+
if not pid:
|
|
228
|
+
continue
|
|
229
|
+
if pid in existing["patterns"]:
|
|
230
|
+
# Local wins on duplicate ids (Phase 1 policy).
|
|
231
|
+
continue
|
|
232
|
+
# Optional mtime gate: skip if the remote entry has a timestamp and
|
|
233
|
+
# it predates the floor.
|
|
234
|
+
ts = pat.get("updated_at") or pat.get("created_at")
|
|
235
|
+
if ts and local_mtime_floor:
|
|
236
|
+
try:
|
|
237
|
+
# Very loose timestamp parse: accept both epoch and ISO.
|
|
238
|
+
if isinstance(ts, (int, float)) and float(ts) < local_mtime_floor:
|
|
239
|
+
continue
|
|
240
|
+
except (TypeError, ValueError):
|
|
241
|
+
pass
|
|
242
|
+
existing["patterns"][pid] = pat
|
|
243
|
+
merged += 1
|
|
244
|
+
|
|
245
|
+
if merged == 0:
|
|
246
|
+
emit_managed_event(
|
|
247
|
+
"managed_memory_hydrate", {"merged": 0, "candidates": len(entries)}
|
|
248
|
+
)
|
|
249
|
+
return 0
|
|
250
|
+
|
|
251
|
+
# Use existing MemoryStorage._atomic_write where possible; fall back to
|
|
252
|
+
# tempfile+rename if MemoryStorage is not importable in this process.
|
|
253
|
+
try:
|
|
254
|
+
# Late import to avoid module-load cycles.
|
|
255
|
+
from memory.storage import MemoryStorage # type: ignore
|
|
256
|
+
|
|
257
|
+
storage = MemoryStorage(str(patterns_path.parent.parent))
|
|
258
|
+
storage._atomic_write(patterns_path, existing)
|
|
259
|
+
except Exception:
|
|
260
|
+
# Fallback: inline atomic write.
|
|
261
|
+
import tempfile
|
|
262
|
+
|
|
263
|
+
patterns_path.parent.mkdir(parents=True, exist_ok=True)
|
|
264
|
+
fd, tmp = tempfile.mkstemp(
|
|
265
|
+
dir=str(patterns_path.parent), prefix=".tmp_", suffix=".json"
|
|
266
|
+
)
|
|
267
|
+
try:
|
|
268
|
+
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
|
269
|
+
json.dump(existing, f, indent=2, default=str)
|
|
270
|
+
os.replace(tmp, patterns_path)
|
|
271
|
+
except Exception as e:
|
|
272
|
+
if os.path.exists(tmp):
|
|
273
|
+
os.unlink(tmp)
|
|
274
|
+
emit_managed_event(
|
|
275
|
+
"managed_agents_fallback",
|
|
276
|
+
{"reason": "atomic_write_failed", "detail": str(e), "op": "hydrate"},
|
|
277
|
+
)
|
|
278
|
+
return 0
|
|
279
|
+
|
|
280
|
+
emit_managed_event(
|
|
281
|
+
"managed_memory_hydrate",
|
|
282
|
+
{"merged": merged, "candidates": len(entries)},
|
|
283
|
+
)
|
|
284
|
+
return merged
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
# ---------------------------------------------------------------------------
|
|
288
|
+
# Module CLI
|
|
289
|
+
# ---------------------------------------------------------------------------
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _main(argv: Optional[list] = None) -> int:
|
|
293
|
+
# Silent no-op if flags are off; bash callers rely on exit 0 + empty stdout.
|
|
294
|
+
if not is_enabled():
|
|
295
|
+
return 0
|
|
296
|
+
|
|
297
|
+
parser = argparse.ArgumentParser(
|
|
298
|
+
prog="python3 -m memory.managed_memory.retrieve",
|
|
299
|
+
description="Retrieve related verdicts / hydrate patterns from the managed store.",
|
|
300
|
+
)
|
|
301
|
+
parser.add_argument("--query", help="Retrieval query string")
|
|
302
|
+
parser.add_argument("--top-k", type=int, default=3)
|
|
303
|
+
parser.add_argument("--store-id", default=None)
|
|
304
|
+
parser.add_argument(
|
|
305
|
+
"--hydrate",
|
|
306
|
+
action="store_true",
|
|
307
|
+
help="Hydrate semantic patterns into .loki/memory/semantic/",
|
|
308
|
+
)
|
|
309
|
+
parser.add_argument(
|
|
310
|
+
"--since-seconds",
|
|
311
|
+
type=int,
|
|
312
|
+
default=0,
|
|
313
|
+
help="Hydrate: only merge patterns newer than NOW - since_seconds",
|
|
314
|
+
)
|
|
315
|
+
args = parser.parse_args(argv)
|
|
316
|
+
|
|
317
|
+
try:
|
|
318
|
+
if args.hydrate:
|
|
319
|
+
floor = 0.0
|
|
320
|
+
if args.since_seconds and args.since_seconds > 0:
|
|
321
|
+
floor = time.time() - args.since_seconds
|
|
322
|
+
hydrate_patterns(local_mtime_floor=floor)
|
|
323
|
+
return 0
|
|
324
|
+
|
|
325
|
+
query = args.query or ""
|
|
326
|
+
if not query:
|
|
327
|
+
return 0
|
|
328
|
+
results = retrieve_related_verdicts(
|
|
329
|
+
query=query, top_k=args.top_k, store_id=args.store_id
|
|
330
|
+
)
|
|
331
|
+
# Print one line per hit, suitable for pasting into a prompt section.
|
|
332
|
+
for r in results:
|
|
333
|
+
path = r.get("path", "")
|
|
334
|
+
summary = r.get("content_summary", "")
|
|
335
|
+
print(f"- [managed] {path}: {summary}")
|
|
336
|
+
return 0
|
|
337
|
+
except Exception as e: # pragma: no cover - defensive
|
|
338
|
+
_warn_once(f"retrieve CLI unexpected error: {e}")
|
|
339
|
+
emit_managed_event(
|
|
340
|
+
"managed_agents_fallback",
|
|
341
|
+
{"reason": "cli_unexpected", "detail": str(e), "op": "retrieve"},
|
|
342
|
+
)
|
|
343
|
+
return 0
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
if __name__ == "__main__":
|
|
347
|
+
sys.exit(_main())
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Loki Managed Agents Memory - Shadow-write path (v6.83.0 Phase 1).
|
|
3
|
+
|
|
4
|
+
Writes a whitelisted subset of RARV-C artifacts to the managed memory store:
|
|
5
|
+
|
|
6
|
+
- Completion-council verdicts (one JSON file per iteration)
|
|
7
|
+
- High-importance semantic patterns (importance >= 0.6)
|
|
8
|
+
|
|
9
|
+
Design rules enforced by this module:
|
|
10
|
+
|
|
11
|
+
1. Every call is gated on LOKI_MANAGED_AGENTS=true AND LOKI_MANAGED_MEMORY=true.
|
|
12
|
+
2. On API error, we emit ONE `managed_agents_fallback` event and return.
|
|
13
|
+
No retry storm.
|
|
14
|
+
3. On a 409 (sha256 precondition mismatch), we re-read the remote entry,
|
|
15
|
+
merge with the local content, and retry ONCE. After that, fall back.
|
|
16
|
+
4. Non-blocking from the caller: this module never raises to its caller.
|
|
17
|
+
Callers in bash can background with `&` for extra isolation.
|
|
18
|
+
5. This file must NOT import anthropic at module load time. Client
|
|
19
|
+
construction is deferred to first call.
|
|
20
|
+
|
|
21
|
+
CLI:
|
|
22
|
+
python3 -m memory.managed_memory.shadow_write --verdict <path>
|
|
23
|
+
python3 -m memory.managed_memory.shadow_write --path <episode.json>
|
|
24
|
+
python3 -m memory.managed_memory.shadow_write --pattern-json <json_file>
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import argparse
|
|
30
|
+
import json
|
|
31
|
+
import logging
|
|
32
|
+
import os
|
|
33
|
+
import sys
|
|
34
|
+
import time
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
from typing import Any, Dict, Optional
|
|
37
|
+
|
|
38
|
+
from . import ManagedDisabled, is_enabled
|
|
39
|
+
from .events import emit_managed_event
|
|
40
|
+
|
|
41
|
+
_LOG = logging.getLogger("loki.managed_memory.shadow_write")
|
|
42
|
+
|
|
43
|
+
# Store name is stable across runs so different projects share lineage.
|
|
44
|
+
# Callers can override via LOKI_MANAGED_STORE_NAME.
|
|
45
|
+
_DEFAULT_STORE_NAME = "loki-rarv-c-learnings"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _store_name() -> str:
|
|
49
|
+
return os.environ.get("LOKI_MANAGED_STORE_NAME", _DEFAULT_STORE_NAME)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _warn_once(msg: str) -> None:
|
|
53
|
+
# Single-line WARN, no retry-storm. Kept trivially simple on purpose.
|
|
54
|
+
sys.stderr.write(f"WARN [managed_memory] {msg}\n")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _get_client():
|
|
58
|
+
"""Import client lazily so importing this module stays SDK-free."""
|
|
59
|
+
from .client import get_client, compute_sha256 # noqa: F401
|
|
60
|
+
|
|
61
|
+
return get_client(), compute_sha256
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _read_json(path: str) -> Optional[Dict[str, Any]]:
|
|
65
|
+
try:
|
|
66
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
67
|
+
return json.load(f)
|
|
68
|
+
except (OSError, json.JSONDecodeError):
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# ---------------------------------------------------------------------------
|
|
73
|
+
# Core write helpers
|
|
74
|
+
# ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _shadow_write_blob(
|
|
78
|
+
logical_path: str,
|
|
79
|
+
payload: Dict[str, Any],
|
|
80
|
+
kind: str,
|
|
81
|
+
) -> bool:
|
|
82
|
+
"""
|
|
83
|
+
Write `payload` (a JSON-serializable dict) at `logical_path` in the
|
|
84
|
+
managed store. Returns True on success, False on fallback.
|
|
85
|
+
|
|
86
|
+
This function is responsible for the single 409 retry cycle and for
|
|
87
|
+
emitting fallback/success events.
|
|
88
|
+
"""
|
|
89
|
+
if not is_enabled():
|
|
90
|
+
return False
|
|
91
|
+
|
|
92
|
+
content = json.dumps(payload, sort_keys=True, default=str)
|
|
93
|
+
try:
|
|
94
|
+
client, compute_sha = _get_client()
|
|
95
|
+
except ManagedDisabled as e:
|
|
96
|
+
_warn_once(f"client unavailable ({e}); local path only")
|
|
97
|
+
emit_managed_event(
|
|
98
|
+
"managed_agents_fallback",
|
|
99
|
+
{"reason": "client_unavailable", "detail": str(e), "kind": kind},
|
|
100
|
+
)
|
|
101
|
+
return False
|
|
102
|
+
except Exception as e: # pragma: no cover - defensive
|
|
103
|
+
_warn_once(f"client construction failed ({e}); local path only")
|
|
104
|
+
emit_managed_event(
|
|
105
|
+
"managed_agents_fallback",
|
|
106
|
+
{"reason": "client_error", "detail": str(e), "kind": kind},
|
|
107
|
+
)
|
|
108
|
+
return False
|
|
109
|
+
|
|
110
|
+
# Ensure the store exists. Failure here => fallback.
|
|
111
|
+
try:
|
|
112
|
+
store = client.stores_get_or_create(
|
|
113
|
+
name=_store_name(),
|
|
114
|
+
description="Loki Mode RARV-C shadow-write store (v6.83.0)",
|
|
115
|
+
scope="project",
|
|
116
|
+
)
|
|
117
|
+
store_id = store.get("id") or store.get("store_id")
|
|
118
|
+
except Exception as e:
|
|
119
|
+
_warn_once(f"stores_get_or_create failed ({e}); local path only")
|
|
120
|
+
emit_managed_event(
|
|
121
|
+
"managed_agents_fallback",
|
|
122
|
+
{"reason": "stores_error", "detail": str(e), "kind": kind},
|
|
123
|
+
)
|
|
124
|
+
return False
|
|
125
|
+
|
|
126
|
+
if not store_id:
|
|
127
|
+
emit_managed_event(
|
|
128
|
+
"managed_agents_fallback",
|
|
129
|
+
{"reason": "missing_store_id", "kind": kind},
|
|
130
|
+
)
|
|
131
|
+
return False
|
|
132
|
+
|
|
133
|
+
sha = compute_sha(content)
|
|
134
|
+
start = time.monotonic()
|
|
135
|
+
try:
|
|
136
|
+
client.memory_create(
|
|
137
|
+
store_id=store_id,
|
|
138
|
+
path=logical_path,
|
|
139
|
+
content=content,
|
|
140
|
+
sha256_precondition=sha,
|
|
141
|
+
)
|
|
142
|
+
emit_managed_event(
|
|
143
|
+
"managed_memory_shadow_write",
|
|
144
|
+
{
|
|
145
|
+
"kind": kind,
|
|
146
|
+
"path": logical_path,
|
|
147
|
+
"sha256": sha,
|
|
148
|
+
"elapsed_ms": int((time.monotonic() - start) * 1000),
|
|
149
|
+
},
|
|
150
|
+
)
|
|
151
|
+
return True
|
|
152
|
+
except Exception as first_err:
|
|
153
|
+
# Detect 409 / precondition failure.
|
|
154
|
+
status = getattr(first_err, "status_code", None) or getattr(
|
|
155
|
+
first_err, "status", None
|
|
156
|
+
)
|
|
157
|
+
if status == 409:
|
|
158
|
+
# Re-read, merge, retry once.
|
|
159
|
+
try:
|
|
160
|
+
existing_list = client.memories_list(
|
|
161
|
+
store_id=store_id, path_prefix=logical_path
|
|
162
|
+
)
|
|
163
|
+
existing_entry = next(
|
|
164
|
+
(m for m in existing_list if m.get("path") == logical_path),
|
|
165
|
+
None,
|
|
166
|
+
)
|
|
167
|
+
existing_sha = (
|
|
168
|
+
existing_entry.get("sha") or existing_entry.get("content_sha256")
|
|
169
|
+
if existing_entry
|
|
170
|
+
else None
|
|
171
|
+
)
|
|
172
|
+
if existing_entry and existing_sha:
|
|
173
|
+
# Simple merge: if existing content already contains a
|
|
174
|
+
# "versions" list, append; else seed it. We do NOT attempt
|
|
175
|
+
# semantic merging in Phase 1.
|
|
176
|
+
try:
|
|
177
|
+
existing_content_str = existing_entry.get("content", "{}")
|
|
178
|
+
existing_doc = json.loads(existing_content_str)
|
|
179
|
+
except (TypeError, json.JSONDecodeError):
|
|
180
|
+
existing_doc = {}
|
|
181
|
+
merged = {
|
|
182
|
+
**existing_doc,
|
|
183
|
+
**payload,
|
|
184
|
+
"_merged_versions": (
|
|
185
|
+
existing_doc.get("_merged_versions", [])
|
|
186
|
+
+ [existing_sha]
|
|
187
|
+
),
|
|
188
|
+
}
|
|
189
|
+
merged_content = json.dumps(merged, sort_keys=True, default=str)
|
|
190
|
+
new_sha = compute_sha(merged_content)
|
|
191
|
+
# Precondition is the sha of the CURRENT remote state that
|
|
192
|
+
# we just read. The new sha is what the server will store.
|
|
193
|
+
client.memory_create(
|
|
194
|
+
store_id=store_id,
|
|
195
|
+
path=logical_path,
|
|
196
|
+
content=merged_content,
|
|
197
|
+
sha256_precondition=existing_sha,
|
|
198
|
+
)
|
|
199
|
+
emit_managed_event(
|
|
200
|
+
"managed_memory_shadow_write",
|
|
201
|
+
{
|
|
202
|
+
"kind": kind,
|
|
203
|
+
"path": logical_path,
|
|
204
|
+
"sha256": new_sha,
|
|
205
|
+
"merged": True,
|
|
206
|
+
"elapsed_ms": int(
|
|
207
|
+
(time.monotonic() - start) * 1000
|
|
208
|
+
),
|
|
209
|
+
},
|
|
210
|
+
)
|
|
211
|
+
return True
|
|
212
|
+
except Exception as retry_err:
|
|
213
|
+
_warn_once(
|
|
214
|
+
f"409 retry for {logical_path} failed ({retry_err}); fallback"
|
|
215
|
+
)
|
|
216
|
+
emit_managed_event(
|
|
217
|
+
"managed_agents_fallback",
|
|
218
|
+
{
|
|
219
|
+
"reason": "retry_failed",
|
|
220
|
+
"detail": str(retry_err),
|
|
221
|
+
"kind": kind,
|
|
222
|
+
"path": logical_path,
|
|
223
|
+
},
|
|
224
|
+
)
|
|
225
|
+
return False
|
|
226
|
+
|
|
227
|
+
# Non-409 error => immediate fallback, single WARN line.
|
|
228
|
+
_warn_once(f"memory_create failed ({first_err}); local path only")
|
|
229
|
+
emit_managed_event(
|
|
230
|
+
"managed_agents_fallback",
|
|
231
|
+
{
|
|
232
|
+
"reason": "memory_create_error",
|
|
233
|
+
"detail": str(first_err),
|
|
234
|
+
"kind": kind,
|
|
235
|
+
"path": logical_path,
|
|
236
|
+
},
|
|
237
|
+
)
|
|
238
|
+
return False
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
# ---------------------------------------------------------------------------
|
|
242
|
+
# Public entry points
|
|
243
|
+
# ---------------------------------------------------------------------------
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def shadow_write_verdict(verdict_json_path: str) -> bool:
|
|
247
|
+
"""
|
|
248
|
+
Read a council verdict JSON from disk and shadow-write it.
|
|
249
|
+
|
|
250
|
+
The logical remote path is `verdicts/<iteration>.json`. If the file is
|
|
251
|
+
missing or unreadable, the call is a no-op. Never raises.
|
|
252
|
+
"""
|
|
253
|
+
if not is_enabled():
|
|
254
|
+
return False
|
|
255
|
+
payload = _read_json(verdict_json_path)
|
|
256
|
+
if not payload:
|
|
257
|
+
emit_managed_event(
|
|
258
|
+
"managed_agents_fallback",
|
|
259
|
+
{"reason": "verdict_unreadable", "path": verdict_json_path},
|
|
260
|
+
)
|
|
261
|
+
return False
|
|
262
|
+
iteration = payload.get("iteration") or payload.get("round") or "unknown"
|
|
263
|
+
logical = f"verdicts/iteration-{iteration}.json"
|
|
264
|
+
return _shadow_write_blob(logical, payload, kind="verdict")
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def shadow_write_pattern(pattern: Dict[str, Any]) -> bool:
|
|
268
|
+
"""
|
|
269
|
+
Shadow-write a SemanticPattern-shaped dict. Logical path is
|
|
270
|
+
`patterns/<pattern_id>.json`. Never raises.
|
|
271
|
+
"""
|
|
272
|
+
if not is_enabled():
|
|
273
|
+
return False
|
|
274
|
+
if not isinstance(pattern, dict):
|
|
275
|
+
emit_managed_event(
|
|
276
|
+
"managed_agents_fallback",
|
|
277
|
+
{"reason": "pattern_not_dict", "type": str(type(pattern))},
|
|
278
|
+
)
|
|
279
|
+
return False
|
|
280
|
+
pid = pattern.get("pattern_id") or pattern.get("id") or "unknown"
|
|
281
|
+
logical = f"patterns/{pid}.json"
|
|
282
|
+
return _shadow_write_blob(logical, pattern, kind="pattern")
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def shadow_write_episode(episode_path: str) -> bool:
|
|
286
|
+
"""
|
|
287
|
+
Shadow-write a high-importance episode trace JSON. Logical path is
|
|
288
|
+
`episodes/<episode_id>.json`. Called from autonomy/run.sh's
|
|
289
|
+
auto_capture_episode ONLY when importance >= 0.6. Never raises.
|
|
290
|
+
"""
|
|
291
|
+
if not is_enabled():
|
|
292
|
+
return False
|
|
293
|
+
payload = _read_json(episode_path)
|
|
294
|
+
if not payload:
|
|
295
|
+
emit_managed_event(
|
|
296
|
+
"managed_agents_fallback",
|
|
297
|
+
{"reason": "episode_unreadable", "path": episode_path},
|
|
298
|
+
)
|
|
299
|
+
return False
|
|
300
|
+
eid = payload.get("id") or payload.get("task_id") or Path(episode_path).stem
|
|
301
|
+
logical = f"episodes/{eid}.json"
|
|
302
|
+
return _shadow_write_blob(logical, payload, kind="episode")
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
# ---------------------------------------------------------------------------
|
|
306
|
+
# Module CLI
|
|
307
|
+
# ---------------------------------------------------------------------------
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _main(argv: Optional[list] = None) -> int:
|
|
311
|
+
# Silent no-op if flags are off -- bash callers rely on exit 0.
|
|
312
|
+
if not is_enabled():
|
|
313
|
+
return 0
|
|
314
|
+
|
|
315
|
+
parser = argparse.ArgumentParser(
|
|
316
|
+
prog="python3 -m memory.managed_memory.shadow_write",
|
|
317
|
+
description="Shadow-write a RARV-C artifact to the managed memory store.",
|
|
318
|
+
)
|
|
319
|
+
parser.add_argument("--verdict", help="Path to a council verdict JSON")
|
|
320
|
+
parser.add_argument("--path", help="Path to an episode trace JSON")
|
|
321
|
+
parser.add_argument(
|
|
322
|
+
"--pattern-json", help="Path to a file containing a pattern JSON blob"
|
|
323
|
+
)
|
|
324
|
+
args = parser.parse_args(argv)
|
|
325
|
+
|
|
326
|
+
try:
|
|
327
|
+
if args.verdict:
|
|
328
|
+
shadow_write_verdict(args.verdict)
|
|
329
|
+
return 0
|
|
330
|
+
if args.path:
|
|
331
|
+
shadow_write_episode(args.path)
|
|
332
|
+
return 0
|
|
333
|
+
if args.pattern_json:
|
|
334
|
+
pat = _read_json(args.pattern_json)
|
|
335
|
+
if pat:
|
|
336
|
+
shadow_write_pattern(pat)
|
|
337
|
+
return 0
|
|
338
|
+
parser.print_help(sys.stderr)
|
|
339
|
+
return 0
|
|
340
|
+
except Exception as e: # pragma: no cover - defensive
|
|
341
|
+
_warn_once(f"shadow_write CLI unexpected error: {e}")
|
|
342
|
+
emit_managed_event(
|
|
343
|
+
"managed_agents_fallback",
|
|
344
|
+
{"reason": "cli_unexpected", "detail": str(e)},
|
|
345
|
+
)
|
|
346
|
+
return 0 # Bash callers must see exit 0.
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
if __name__ == "__main__":
|
|
350
|
+
sys.exit(_main())
|