loki-mode 6.82.0 → 6.83.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.
@@ -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())
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loki-mode",
3
- "version": "6.82.0",
3
+ "version": "6.83.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",
@@ -101,7 +101,7 @@
101
101
  "postinstall": "node bin/postinstall.js",
102
102
  "prepack": "find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null; find . -name '*.pyc' -delete 2>/dev/null; true",
103
103
  "prepublishOnly": "cd dashboard-ui && npm ci && npm run build:all",
104
- "test": "bash -n autonomy/run.sh && bash -n autonomy/loki && bash -n autonomy/completion-council.sh && bash -n autonomy/app-runner.sh && bash -n autonomy/prd-checklist.sh && bash -n autonomy/playwright-verify.sh && node --test tests/protocols/*.test.js && node --test tests/protocols/a2a/*.test.js && node --test tests/observability/*.test.js && node --test tests/policies/*.test.js && node --test tests/audit/*.test.js && node --test tests/integrations/*.test.js && node --test tests/integrations/jira/*.test.js && node --test tests/integrations/github/*.test.js && node --test tests/integrations/slack/*.test.js && echo 'All checks passed'",
104
+ "test": "bash -n autonomy/run.sh && bash -n autonomy/loki && bash -n autonomy/completion-council.sh && bash -n autonomy/app-runner.sh && bash -n autonomy/prd-checklist.sh && bash -n autonomy/playwright-verify.sh && node --test tests/protocols/*.test.js && node --test tests/protocols/a2a/*.test.js && node --test tests/observability/*.test.js && node --test tests/policies/*.test.js && node --test tests/audit/*.test.js && node --test tests/integrations/*.test.js && node --test tests/integrations/jira/*.test.js && node --test tests/integrations/github/*.test.js && node --test tests/integrations/slack/*.test.js && bash tests/managed_memory/test_flag_matrix.sh && bash tests/managed_memory/test_sdk_isolation.sh && bash tests/managed_memory/test_kill_switch.sh && python3 -m unittest tests.managed_memory.test_shadow_write_mock tests.managed_memory.test_retrieve_mock && echo 'All checks passed'",
105
105
  "test:visual": "node --experimental-vm-modules node_modules/jest/bin/jest.js dashboard-ui/tests/visual-regression.test.js",
106
106
  "test:parity": "node --experimental-vm-modules dashboard-ui/scripts/check-parity.js",
107
107
  "test:parity:json": "node --experimental-vm-modules dashboard-ui/scripts/check-parity.js --json",