loki-mode 6.83.1 → 7.0.2

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.
@@ -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
- hydrate_patterns(local_mtime_floor=floor)
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": "6.83.1",
3
+ "version": "7.0.2",
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"