superlocalmemory 4.0.0 → 4.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.
Files changed (83) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +11 -11
  3. package/package.json +1 -1
  4. package/plugin/.claude-plugin/plugin.json +1 -1
  5. package/plugin/CLAUDE.md +3 -3
  6. package/plugin/agents/slm-governance-advisor.md +1 -1
  7. package/plugin/agents/slm-loop-runner.md +1 -1
  8. package/plugin/agents/slm-memory-advisor.md +1 -1
  9. package/plugin/agents/slm-optimize-advisor.md +1 -1
  10. package/plugin/requirements.txt +1 -1
  11. package/plugin/skills/slm-cache/SKILL.md +1 -1
  12. package/plugin/skills/slm-compress/SKILL.md +1 -1
  13. package/plugin/skills/slm-governance/SKILL.md +1 -1
  14. package/plugin/skills/slm-graph/SKILL.md +1 -1
  15. package/plugin/skills/slm-loop/SKILL.md +1 -1
  16. package/plugin/skills/slm-mesh/SKILL.md +1 -1
  17. package/plugin/skills/slm-profile/SKILL.md +1 -1
  18. package/plugin/skills/slm-recall/SKILL.md +1 -1
  19. package/plugin/skills/slm-remember/SKILL.md +1 -1
  20. package/plugin/skills/slm-scope/SKILL.md +1 -1
  21. package/plugin/skills/slm-session/SKILL.md +1 -1
  22. package/plugin/skills/slm-status/SKILL.md +1 -1
  23. package/plugin-src/rules/AGENTS.md +1 -1
  24. package/plugin-src/skills/slm-cache/SKILL.md +1 -1
  25. package/plugin-src/skills/slm-compress/SKILL.md +1 -1
  26. package/plugin-src/skills/slm-governance/SKILL.md +1 -1
  27. package/plugin-src/skills/slm-graph/SKILL.md +1 -1
  28. package/plugin-src/skills/slm-loop/SKILL.md +1 -1
  29. package/plugin-src/skills/slm-mesh/SKILL.md +1 -1
  30. package/plugin-src/skills/slm-profile/SKILL.md +1 -1
  31. package/plugin-src/skills/slm-recall/SKILL.md +1 -1
  32. package/plugin-src/skills/slm-remember/SKILL.md +1 -1
  33. package/plugin-src/skills/slm-scope/SKILL.md +1 -1
  34. package/plugin-src/skills/slm-session/SKILL.md +1 -1
  35. package/plugin-src/skills/slm-status/SKILL.md +1 -1
  36. package/pyproject.toml +5 -4
  37. package/src/superlocalmemory/__init__.py +1 -1
  38. package/src/superlocalmemory/cli/commands.py +60 -1
  39. package/src/superlocalmemory/cli/main.py +24 -1
  40. package/src/superlocalmemory/compliance/gdpr.py +104 -73
  41. package/src/superlocalmemory/contracts/__init__.py +1 -0
  42. package/src/superlocalmemory/contracts/schemas/agent-experience-v1.schema.json +92 -0
  43. package/src/superlocalmemory/contracts/schemas/agent-integration-contract-v2.schema.json +46 -0
  44. package/src/superlocalmemory/contracts/schemas/cognitive-turn-receipt-v1.schema.json +59 -0
  45. package/src/superlocalmemory/contracts/v402.py +62 -0
  46. package/src/superlocalmemory/core/engine.py +10 -0
  47. package/src/superlocalmemory/core/recall_pipeline.py +6 -0
  48. package/src/superlocalmemory/core/recall_worker.py +12 -0
  49. package/src/superlocalmemory/core/worker_pool.py +12 -0
  50. package/src/superlocalmemory/hooks/hook_handlers.py +16 -0
  51. package/src/superlocalmemory/hooks/post_tool_outcome_hook.py +12 -6
  52. package/src/superlocalmemory/hooks/session_registry.py +136 -3
  53. package/src/superlocalmemory/hooks/user_prompt_hook.py +9 -2
  54. package/src/superlocalmemory/integrations/__init__.py +1 -0
  55. package/src/superlocalmemory/integrations/bounded_loops_v051.py +236 -0
  56. package/src/superlocalmemory/learning/database.py +21 -14
  57. package/src/superlocalmemory/mcp/_daemon_proxy.py +9 -0
  58. package/src/superlocalmemory/mcp/server.py +5 -0
  59. package/src/superlocalmemory/mcp/tools_brain.py +132 -0
  60. package/src/superlocalmemory/mcp/tools_core.py +25 -6
  61. package/src/superlocalmemory/mcp/tools_v3.py +16 -2
  62. package/src/superlocalmemory/retrieval/engine.py +43 -1
  63. package/src/superlocalmemory/retrieval/temporal_utils.py +16 -1
  64. package/src/superlocalmemory/retrieval/temporal_validity_filter.py +151 -0
  65. package/src/superlocalmemory/server/routes/brain.py +206 -1
  66. package/src/superlocalmemory/server/routes/helpers.py +53 -35
  67. package/src/superlocalmemory/server/routes/v3_api.py +118 -11
  68. package/src/superlocalmemory/server/ui.py +7 -2
  69. package/src/superlocalmemory/server/unified_daemon.py +37 -4
  70. package/src/superlocalmemory/storage/_migration_internals.py +4 -0
  71. package/src/superlocalmemory/storage/_schema_version.py +2 -2
  72. package/src/superlocalmemory/storage/agent_experience.py +490 -0
  73. package/src/superlocalmemory/storage/database.py +189 -34
  74. package/src/superlocalmemory/storage/migration_runner.py +8 -0
  75. package/src/superlocalmemory/storage/migrations/M015_add_pinned_column.py +18 -0
  76. package/src/superlocalmemory/storage/migrations/M040_agent_experience_receipts.py +254 -0
  77. package/src/superlocalmemory/storage/migrations/__init__.py +2 -0
  78. package/src/superlocalmemory/storage/schema.py +4 -0
  79. package/src/superlocalmemory/ui/index.html +2 -2
  80. package/src/superlocalmemory/ui/js/auto-settings.js +18 -14
  81. package/src/superlocalmemory/ui/js/brain.js +57 -1
  82. package/src/superlocalmemory/ui/js/od-brain.js +114 -40
  83. package/src/superlocalmemory/ui/js/od-settings.js +8 -1
@@ -53,11 +53,12 @@ class GDPRCompliance:
53
53
 
54
54
  def _memory_has_siblings(self, memory_id: str, profile_id: str) -> bool:
55
55
  try:
56
- return bool(self._db.execute(
57
- "SELECT 1 FROM atomic_facts "
58
- "WHERE memory_id = ? AND profile_id = ? LIMIT 1",
59
- (memory_id, profile_id),
60
- ))
56
+ return bool(
57
+ self._db.execute(
58
+ "SELECT 1 FROM atomic_facts WHERE memory_id = ? AND profile_id = ? LIMIT 1",
59
+ (memory_id, profile_id),
60
+ )
61
+ )
61
62
  except Exception:
62
63
  return True
63
64
 
@@ -69,8 +70,12 @@ class GDPRCompliance:
69
70
  from superlocalmemory.core.transactions.erasure import write_tombstones
70
71
 
71
72
  write_tombstones(
72
- self._db, profile_id, (fact_id,), uuid.uuid4().hex,
73
- time.time(), memory_id,
73
+ self._db,
74
+ profile_id,
75
+ (fact_id,),
76
+ uuid.uuid4().hex,
77
+ time.time(),
78
+ memory_id,
74
79
  )
75
80
  except Exception:
76
81
  pass
@@ -192,9 +197,7 @@ class GDPRCompliance:
192
197
  try:
193
198
  names = [
194
199
  dict(r)["name"]
195
- for r in self._db.execute(
196
- "SELECT name FROM sqlite_master WHERE type='table'"
197
- )
200
+ for r in self._db.execute("SELECT name FROM sqlite_master WHERE type='table'")
198
201
  ]
199
202
  except Exception:
200
203
  return []
@@ -230,18 +233,14 @@ class GDPRCompliance:
230
233
 
231
234
  # Profile record itself (the tenant metadata).
232
235
  try:
233
- rows = self._db.execute(
234
- "SELECT * FROM profiles WHERE profile_id = ?", (profile_id,)
235
- )
236
+ rows = self._db.execute("SELECT * FROM profiles WHERE profile_id = ?", (profile_id,))
236
237
  data["profile_record"] = [dict(r) for r in rows]
237
238
  except Exception:
238
239
  data["profile_record"] = []
239
240
 
240
241
  # total_items counts the canonical (table-name) keys only, before
241
242
  # friendly aliases are added, so it is not double-counted.
242
- data["total_items"] = sum(
243
- len(v) for v in data.values() if isinstance(v, list)
244
- )
243
+ data["total_items"] = sum(len(v) for v in data.values() if isinstance(v, list))
245
244
 
246
245
  # Backward-compatible friendly aliases for the well-known keys (stable
247
246
  # export contract) — they reference the same lists, not copies.
@@ -264,8 +263,9 @@ class GDPRCompliance:
264
263
  the chain in a separate DB is the durable evidence.
265
264
  """
266
265
  if profile_id == "default":
267
- raise ValueError("Cannot delete the default profile via GDPR erasure. "
268
- "Use profile deletion instead.")
266
+ raise ValueError(
267
+ "Cannot delete the default profile via GDPR erasure. Use profile deletion instead."
268
+ )
269
269
 
270
270
  counts: dict[str, int] = {}
271
271
 
@@ -276,14 +276,18 @@ class GDPRCompliance:
276
276
  try:
277
277
  from superlocalmemory.compliance.audit import AuditChain
278
278
  from superlocalmemory.infra.data_root import state_path
279
+
279
280
  AuditChain(str(state_path("audit_chain.db"))).log(
280
- "gdpr_erase", agent_id="gdpr", profile_id=profile_id,
281
+ "gdpr_erase",
282
+ agent_id="gdpr",
283
+ profile_id=profile_id,
281
284
  metadata={"basis": "GDPR Art.17 right-to-erasure"},
282
285
  )
283
286
  except Exception as exc:
284
287
  logger.error(
285
288
  "GDPR erase ABORTED for %r: pre-deletion audit-chain log failed: %s",
286
- profile_id, exc,
289
+ profile_id,
290
+ exc,
287
291
  )
288
292
  counts["audit_request_failed"] = 1
289
293
  counts["erasure_aborted"] = 1
@@ -324,6 +328,7 @@ class GDPRCompliance:
324
328
  data_root = self._data_root
325
329
  try:
326
330
  from superlocalmemory.core.context_cache import purge_profile_from_cache_db
331
+
327
332
  if data_root is None:
328
333
  db_path = getattr(self._db, "db_path", None)
329
334
  if db_path is not None:
@@ -389,10 +394,9 @@ class GDPRCompliance:
389
394
  "SELECT fact_id FROM atomic_facts WHERE profile_id = ?",
390
395
  (profile_id,),
391
396
  )
392
- _profile_fact_ids = tuple(sorted(
393
- dict(r)["fact_id"] for r in _fact_rows
394
- if dict(r).get("fact_id") is not None
395
- ))
397
+ _profile_fact_ids = tuple(
398
+ sorted(dict(r)["fact_id"] for r in _fact_rows if dict(r).get("fact_id") is not None)
399
+ )
396
400
  except Exception as exc:
397
401
  logger.warning("GDPR profile erase: fact_id scan failed: %s", exc)
398
402
 
@@ -417,7 +421,8 @@ class GDPRCompliance:
417
421
  )
418
422
  _remove_result = _erasure_svc.remove(self._db, _ctx)
419
423
  _receipt = _erasure_svc.finalize(
420
- self._db, _ctx,
424
+ self._db,
425
+ _ctx,
421
426
  subject_type="profile",
422
427
  subject_id=profile_id,
423
428
  requested_by="gdpr",
@@ -432,6 +437,33 @@ class GDPRCompliance:
432
437
  counts["receipt_error"] = str(exc)
433
438
  raise
434
439
 
440
+ # Purge the learning sidecar *before* removing memory/profile rows. A
441
+ # learning failure is retryable and must leave the profile intact; the
442
+ # former best-effort-after-delete ordering could orphan receipts.
443
+ if data_root is None:
444
+ # Compatibility for third-party legacy wrappers that expose no
445
+ # durable path. We cannot safely guess another installation's
446
+ # sidecar. Native v4.0.2 runtime objects always provide the root.
447
+ logger.warning(
448
+ "GDPR erase: learning receipt purge skipped for profile %r — "
449
+ "data root could not be resolved",
450
+ profile_id,
451
+ )
452
+ counts["learning_db_skipped"] = 1
453
+ else:
454
+ try:
455
+ from superlocalmemory.learning.database import LearningDatabase
456
+
457
+ learning_db = LearningDatabase(data_root / "learning.db")
458
+ learning_db.reset(profile_id)
459
+ counts["learning_db"] = 1
460
+ except Exception as exc:
461
+ logger.warning("GDPR erase: learning-db reset failed: %s", exc)
462
+ counts["learning_db_failed"] = 1
463
+ raise RuntimeError(
464
+ "learning receipt purge failed; profile deletion was not started"
465
+ ) from exc
466
+
435
467
  # Pass 2 — full-tenant wipe with FK enforcement OFF so table order is
436
468
  # irrelevant (every profile row in every table goes). FTS shadow rows
437
469
  # are still removed by the base-table delete triggers.
@@ -443,9 +475,7 @@ class GDPRCompliance:
443
475
  try:
444
476
  for table in tables:
445
477
  try:
446
- self._db.execute(
447
- f"DELETE FROM {table} WHERE profile_id = ?", (profile_id,)
448
- )
478
+ self._db.execute(f"DELETE FROM {table} WHERE profile_id = ?", (profile_id,))
449
479
  except Exception as exc: # pragma: no cover — defensive per-table
450
480
  logger.warning("GDPR erase: delete %s failed: %s", table, exc)
451
481
  table_delete_failures.append(table)
@@ -460,21 +490,6 @@ class GDPRCompliance:
460
490
  if table_delete_failures:
461
491
  counts["table_delete_failures"] = len(table_delete_failures)
462
492
 
463
- # Erase the learning sidecar next to the active memory database. A
464
- # custom SLM data root must never fall back to another installation's
465
- # DEFAULT_BASE_DIR: doing so can both miss the subject data and erase
466
- # unrelated learning state.
467
- try:
468
- from superlocalmemory.learning.database import LearningDatabase
469
- if data_root is None:
470
- raise RuntimeError("active data root could not be resolved")
471
- learning_db = LearningDatabase(data_root / "learning.db")
472
- learning_db.reset(profile_id)
473
- counts["learning_db"] = 1
474
- except Exception as exc:
475
- logger.warning("GDPR erase: learning-db reset failed: %s", exc)
476
- counts["learning_db_failed"] = 1
477
-
478
493
  # VACUUM to remove deleted data from physical file
479
494
  try:
480
495
  self._db.execute("VACUUM")
@@ -497,28 +512,34 @@ class GDPRCompliance:
497
512
  # Fail-closed: a residue re-count that cannot be performed is a
498
513
  # verification failure, not zero residue. We cannot certify the
499
514
  # table is clean, so erasure must not report complete.
500
- logger.warning(
501
- "GDPR erase: residue re-count for %s failed: %s", table, exc
502
- )
515
+ logger.warning("GDPR erase: residue re-count for %s failed: %s", table, exc)
503
516
  residue_recount_failed = True
504
517
  counts["residue_rows"] = residue_rows
505
518
  if residue_recount_failed:
506
519
  counts["residue_recount_failed"] = 1
507
- counts["erasure_complete"] = 1 if (
508
- residue_rows == 0
509
- and not residue_recount_failed
510
- and not table_delete_failures
511
- and not counts.get("learning_db_failed")
512
- and not counts.get("vector_store_failures")
513
- and not counts.get("context_cache_failed")
514
- and not counts.get("owner_erasure_incomplete")
515
- ) else 0
520
+ counts["erasure_complete"] = (
521
+ 1
522
+ if (
523
+ residue_rows == 0
524
+ and not residue_recount_failed
525
+ and not table_delete_failures
526
+ and not counts.get("learning_db_failed")
527
+ and not counts.get("learning_db_skipped")
528
+ and not counts.get("vector_store_failures")
529
+ and not counts.get("context_cache_failed")
530
+ and not counts.get("owner_erasure_incomplete")
531
+ )
532
+ else 0
533
+ )
516
534
 
517
535
  try:
518
536
  from superlocalmemory.compliance.audit import AuditChain
519
537
  from superlocalmemory.infra.data_root import state_path
538
+
520
539
  AuditChain(str(state_path("audit_chain.db"))).log(
521
- "gdpr_erase_complete", agent_id="gdpr", profile_id=profile_id,
540
+ "gdpr_erase_complete",
541
+ agent_id="gdpr",
542
+ profile_id=profile_id,
522
543
  metadata={
523
544
  "basis": "GDPR Art.17 right-to-erasure",
524
545
  "tables_erased": len(tables),
@@ -539,13 +560,17 @@ class GDPRCompliance:
539
560
  and the entity itself. For targeted erasure requests.
540
561
  """
541
562
  import time
563
+
542
564
  requested_at = time.time()
543
565
  audit_request_ok = True
544
566
  try:
545
567
  from superlocalmemory.compliance.audit import AuditChain
546
568
  from superlocalmemory.infra.data_root import state_path
569
+
547
570
  AuditChain(str(state_path("audit_chain.db"))).log(
548
- "gdpr_erase_entity", agent_id="gdpr", profile_id=profile_id,
571
+ "gdpr_erase_entity",
572
+ agent_id="gdpr",
573
+ profile_id=profile_id,
549
574
  metadata={
550
575
  "basis": "GDPR Art.17 right-to-erasure",
551
576
  "entity": entity_name,
@@ -554,9 +579,13 @@ class GDPRCompliance:
554
579
  except Exception as exc:
555
580
  logger.warning("GDPR entity erase: audit-chain log failed: %s", exc)
556
581
  audit_request_ok = False
557
- self._audit("delete", "entity", entity_name,
558
- f"GDPR entity erasure in profile {profile_id}",
559
- profile_id=profile_id)
582
+ self._audit(
583
+ "delete",
584
+ "entity",
585
+ entity_name,
586
+ f"GDPR entity erasure in profile {profile_id}",
587
+ profile_id=profile_id,
588
+ )
560
589
 
561
590
  entity = self._db.get_entity_by_name(entity_name, profile_id)
562
591
  if entity is None:
@@ -597,7 +626,8 @@ class GDPRCompliance:
597
626
  )
598
627
  erasure_svc.remove(self._db, ctx)
599
628
  receipt = erasure_svc.finalize(
600
- self._db, ctx,
629
+ self._db,
630
+ ctx,
601
631
  subject_type="entity",
602
632
  subject_id=entity_name,
603
633
  requested_by="gdpr",
@@ -606,9 +636,7 @@ class GDPRCompliance:
606
636
  if not receipt.persisted:
607
637
  counts["receipt_persist_failed"] = 1
608
638
  if not receipt.all_erased:
609
- counts["vector_store_failures"] = sum(
610
- 1 for p in receipt.proofs if not p.erased
611
- )
639
+ counts["vector_store_failures"] = sum(1 for p in receipt.proofs if not p.erased)
612
640
 
613
641
  for fid, mid in targets:
614
642
  self._db.delete_fact(fid)
@@ -636,11 +664,12 @@ class GDPRCompliance:
636
664
  # Delete aliases + entity (profile-scoped — entity_id is UUID-global but
637
665
  # keep the tenant predicate for consistent Art.17 isolation).
638
666
  self._db.execute(
639
- "DELETE FROM entity_aliases WHERE entity_id = ? AND profile_id = ?",
640
- (eid, profile_id))
667
+ "DELETE FROM entity_aliases WHERE entity_id = ? AND profile_id = ?", (eid, profile_id)
668
+ )
641
669
  self._db.execute(
642
670
  "DELETE FROM canonical_entities WHERE entity_id = ? AND profile_id = ?",
643
- (eid, profile_id))
671
+ (eid, profile_id),
672
+ )
644
673
  counts["entity"] = 1
645
674
  if not audit_request_ok:
646
675
  counts["audit_request_failed"] = 1
@@ -650,23 +679,25 @@ class GDPRCompliance:
650
679
 
651
680
  # -- Audit Trail -------------------------------------------------------
652
681
 
653
- def get_audit_trail(
654
- self, profile_id: str, limit: int = 100
655
- ) -> list[dict]:
682
+ def get_audit_trail(self, profile_id: str, limit: int = 100) -> list[dict]:
656
683
  """Get compliance audit trail for a profile."""
657
684
  rows = self._db.execute(
658
- "SELECT * FROM compliance_audit WHERE profile_id = ? "
659
- "ORDER BY timestamp DESC LIMIT ?",
685
+ "SELECT * FROM compliance_audit WHERE profile_id = ? ORDER BY timestamp DESC LIMIT ?",
660
686
  (profile_id, limit),
661
687
  )
662
688
  return [dict(r) for r in rows]
663
689
 
664
690
  def _audit(
665
- self, action: str, target_type: str, target_id: str, details: str,
691
+ self,
692
+ action: str,
693
+ target_type: str,
694
+ target_id: str,
695
+ details: str,
666
696
  profile_id: str | None = None,
667
697
  ) -> None:
668
698
  """Log a compliance action."""
669
699
  from superlocalmemory.storage.models import _new_id
700
+
670
701
  pid = profile_id if profile_id is not None else target_id
671
702
  self._db.execute(
672
703
  "INSERT INTO compliance_audit "
@@ -0,0 +1 @@
1
+ """Versioned public contracts for cross-SLM integration surfaces."""
@@ -0,0 +1,92 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://superlocalmemory.com/schemas/agent-experience-v1.schema.json",
4
+ "title": "SLM Agent Experience v1",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": [
8
+ "experience_id",
9
+ "profile_id",
10
+ "occurred_at",
11
+ "task_class",
12
+ "project_scope",
13
+ "route",
14
+ "verification",
15
+ "producer_claim",
16
+ "terminal_status"
17
+ ],
18
+ "properties": {
19
+ "experience_id": {"type": "string", "minLength": 1},
20
+ "profile_id": {"type": "string", "minLength": 1},
21
+ "occurred_at": {"type": "string", "format": "date-time"},
22
+ "task_class": {"type": "string", "minLength": 1},
23
+ "project_scope": {"type": "string", "minLength": 1},
24
+ "complexity_band": {"type": "string"},
25
+ "risk": {"type": "string"},
26
+ "route": {"$ref": "#/$defs/route_identity"},
27
+ "bounds": {"$ref": "#/$defs/bounds"},
28
+ "usage": {"$ref": "#/$defs/usage"},
29
+ "verification": {"$ref": "#/$defs/verification"},
30
+ "producer_claim": {"enum": ["success", "failure", "partial", "unknown"]},
31
+ "terminal_status": {"enum": ["succeeded", "failed", "cancelled", "timed_out"]},
32
+ "failure_class": {"type": "string"},
33
+ "human_intervention": {"type": "boolean"},
34
+ "lessons": {"type": "string", "maxLength": 2000},
35
+ "receipt_digest": {"$ref": "#/$defs/sha256"},
36
+ "artifact_digests": {
37
+ "type": "array",
38
+ "items": {"$ref": "#/$defs/sha256"},
39
+ "uniqueItems": true
40
+ }
41
+ },
42
+ "$defs": {
43
+ "sha256": {"type": "string", "pattern": "^[a-f0-9]{64}$"},
44
+ "route_identity": {
45
+ "type": "object",
46
+ "additionalProperties": false,
47
+ "required": ["harness", "provider", "model", "effort", "machine"],
48
+ "properties": {
49
+ "harness": {"type": "string", "minLength": 1},
50
+ "provider": {"type": "string", "minLength": 1},
51
+ "model": {"type": "string", "minLength": 1},
52
+ "effort": {"type": "string", "minLength": 1},
53
+ "machine": {"type": "string", "minLength": 1}
54
+ }
55
+ },
56
+ "bounds": {
57
+ "type": "object",
58
+ "additionalProperties": false,
59
+ "properties": {
60
+ "max_steps": {"type": "integer", "minimum": 1},
61
+ "max_wallclock_ms": {"type": "integer", "minimum": 1},
62
+ "max_cost_usd_micros": {"type": "integer", "minimum": 0}
63
+ }
64
+ },
65
+ "usage": {
66
+ "type": "object",
67
+ "additionalProperties": false,
68
+ "properties": {
69
+ "input_tokens": {"type": "integer", "minimum": 0},
70
+ "output_tokens": {"type": "integer", "minimum": 0},
71
+ "wallclock_ms": {"type": "integer", "minimum": 0}
72
+ }
73
+ },
74
+ "verification": {
75
+ "type": "object",
76
+ "additionalProperties": false,
77
+ "required": ["authority", "evidence_digest"],
78
+ "properties": {
79
+ "authority": {
80
+ "enum": [
81
+ "deterministic_gate",
82
+ "bounded_loop_receipt",
83
+ "human_approval",
84
+ "independent_model_audit"
85
+ ]
86
+ },
87
+ "evidence_digest": {"$ref": "#/$defs/sha256"},
88
+ "reference": {"type": "string", "maxLength": 1024}
89
+ }
90
+ }
91
+ }
92
+ }
@@ -0,0 +1,46 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://superlocalmemory.com/schemas/agent-integration-contract-v2.schema.json",
4
+ "title": "SLM Agent Integration Contract v2",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": ["schema_version", "host", "stage", "lifecycle", "artifact_digest", "evidence"],
8
+ "properties": {
9
+ "schema_version": {"const": "v2"},
10
+ "host": {"type": "string", "minLength": 1},
11
+ "stage": {"$ref": "#/$defs/certification_stage"},
12
+ "lifecycle": {
13
+ "type": "array",
14
+ "items": {
15
+ "enum": [
16
+ "INSTALL", "CONFIG_PARSE", "PROCESS_START", "MCP_DISCOVERY", "SESSION_OPEN",
17
+ "TARGETED_RECALL", "CONTEXT_INJECTION", "MEMORY_WRITE", "OUTCOME_CAPTURE",
18
+ "RECONNECT", "SESSION_CLOSE", "UNINSTALL", "CONFIG_PRESERVATION", "SECRET_BOUNDARY"
19
+ ]
20
+ },
21
+ "uniqueItems": true,
22
+ "minItems": 14,
23
+ "maxItems": 14
24
+ },
25
+ "artifact_digest": {"type": "string", "pattern": "^[a-f0-9]{64}$"},
26
+ "evidence": {
27
+ "type": "array",
28
+ "minItems": 1,
29
+ "items": {
30
+ "type": "object",
31
+ "additionalProperties": false,
32
+ "required": ["kind", "digest", "reference"],
33
+ "properties": {
34
+ "kind": {"enum": ["test_run", "artifact", "receipt", "manual_check"]},
35
+ "digest": {"type": "string", "pattern": "^[a-f0-9]{64}$"},
36
+ "reference": {"type": "string", "minLength": 1, "maxLength": 1024}
37
+ }
38
+ }
39
+ }
40
+ },
41
+ "$defs": {
42
+ "certification_stage": {
43
+ "enum": ["STATIC", "CONFIG_PROVEN", "LOCAL_RUNTIME", "END_TO_END", "RELEASE_GATED"]
44
+ }
45
+ }
46
+ }
@@ -0,0 +1,59 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://superlocalmemory.com/schemas/cognitive-turn-receipt-v1.schema.json",
4
+ "title": "SLM Cognitive Turn Receipt v1",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": [
8
+ "receipt_id",
9
+ "task_id",
10
+ "profile_id",
11
+ "project_scope",
12
+ "query_digest",
13
+ "fact_decisions",
14
+ "state"
15
+ ],
16
+ "properties": {
17
+ "receipt_id": {"type": "string", "minLength": 1},
18
+ "task_id": {"type": "string", "minLength": 1},
19
+ "profile_id": {"type": "string", "minLength": 1},
20
+ "project_scope": {"type": "string", "minLength": 1},
21
+ "query_digest": {"type": "string", "pattern": "^[a-f0-9]{64}$"},
22
+ "fact_decisions": {
23
+ "description": "One decision per opaque fact identifier. An object, rather than an array, makes conflicting duplicate decisions structurally impossible for every schema consumer.",
24
+ "type": "object",
25
+ "minProperties": 1,
26
+ "additionalProperties": false,
27
+ "patternProperties": {
28
+ ".+": {"enum": ["considered", "used", "rejected", "corrected"]}
29
+ }
30
+ },
31
+ "outcome": {"$ref": "#/$defs/outcome"},
32
+ "state": {"enum": ["open", "finalized", "abandoned", "reconciled"]}
33
+ },
34
+ "allOf": [
35
+ {
36
+ "if": {"properties": {"state": {"const": "finalized"}}, "required": ["state"]},
37
+ "then": {"required": ["outcome"]}
38
+ }
39
+ ],
40
+ "$defs": {
41
+ "outcome": {
42
+ "type": "object",
43
+ "additionalProperties": false,
44
+ "required": ["authority", "receipt_digest", "reference"],
45
+ "properties": {
46
+ "authority": {
47
+ "enum": [
48
+ "deterministic_gate",
49
+ "bounded_loop_receipt",
50
+ "human_approval",
51
+ "independent_model_audit"
52
+ ]
53
+ },
54
+ "receipt_digest": {"type": "string", "pattern": "^[a-f0-9]{64}$"},
55
+ "reference": {"type": "string", "minLength": 1, "maxLength": 1024}
56
+ }
57
+ }
58
+ }
59
+ }
@@ -0,0 +1,62 @@
1
+ """Validated SLM 4.0.2 contract boundaries.
2
+
3
+ These validators are the required pre-persistence boundary for forthcoming
4
+ Agent Experience and host-certificate writers. They prevent host/self-reported
5
+ telemetry from masquerading as independently verified learning evidence.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ from datetime import datetime
12
+ from importlib import resources
13
+ from typing import Any
14
+
15
+ from jsonschema import Draft202012Validator, FormatChecker # type: ignore[import-untyped]
16
+
17
+
18
+ class ContractValidationError(ValueError):
19
+ """Raised when a cross-lane public contract is invalid."""
20
+
21
+
22
+ _FORMAT_CHECKER = FormatChecker()
23
+
24
+
25
+ @_FORMAT_CHECKER.checks("date-time")
26
+ def _is_rfc3339_datetime(value: object) -> bool:
27
+ """Accept only timezone-aware ISO/RFC3339 timestamps."""
28
+ if not isinstance(value, str):
29
+ return False
30
+ try:
31
+ parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
32
+ except ValueError:
33
+ return False
34
+ return parsed.tzinfo is not None
35
+
36
+
37
+ def _validate(schema_name: str, payload: dict[str, Any]) -> None:
38
+ schema_path = resources.files(__package__).joinpath("schemas", schema_name)
39
+ schema = json.loads(schema_path.read_text(encoding="utf-8"))
40
+ errors = sorted(
41
+ Draft202012Validator(schema, format_checker=_FORMAT_CHECKER).iter_errors(payload),
42
+ key=lambda error: list(error.absolute_path),
43
+ )
44
+ if errors:
45
+ error = errors[0]
46
+ path = ".".join(str(part) for part in error.absolute_path) or "payload"
47
+ raise ContractValidationError(f"{path}: {error.message}")
48
+
49
+
50
+ def validate_agent_experience(payload: dict[str, Any]) -> None:
51
+ """Validate independent outcome evidence before an experience is stored."""
52
+ _validate("agent-experience-v1.schema.json", payload)
53
+
54
+
55
+ def validate_cognitive_turn(payload: dict[str, Any]) -> None:
56
+ """Validate the language-neutral fact-keyed receipt structure."""
57
+ _validate("cognitive-turn-receipt-v1.schema.json", payload)
58
+
59
+
60
+ def validate_integration_certificate(payload: dict[str, Any]) -> None:
61
+ """Validate a hash-bound host lifecycle certificate."""
62
+ _validate("agent-integration-contract-v2.schema.json", payload)
@@ -707,6 +707,9 @@ class MemoryEngine:
707
707
  include_shared: bool | None = None,
708
708
  window: str | tuple[str, str] | None = None,
709
709
  as_of: str | None = None,
710
+ known_as_of: str | None = None,
711
+ valid_at: str | None = None,
712
+ include_unknown: bool = False,
710
713
  ) -> RecallResponse:
711
714
  """Recall relevant facts for a query.
712
715
 
@@ -744,6 +747,10 @@ class MemoryEngine:
744
747
  if include_shared is None:
745
748
  include_shared = bool(getattr(_scope_cfg, "recall_include_shared", False))
746
749
 
750
+ from superlocalmemory.retrieval.temporal_utils import normalize_strict_boundary
751
+ known_as_of = normalize_strict_boundary(known_as_of, "known_as_of")
752
+ valid_at = normalize_strict_boundary(valid_at, "valid_at")
753
+
747
754
  pid = profile_id or self._profile_id
748
755
 
749
756
  from superlocalmemory.core.recall_pipeline import run_recall
@@ -763,6 +770,9 @@ class MemoryEngine:
763
770
  include_shared=include_shared,
764
771
  window=window,
765
772
  as_of=as_of,
773
+ known_as_of=known_as_of,
774
+ valid_at=valid_at,
775
+ include_unknown=include_unknown,
766
776
  )
767
777
  except Exception:
768
778
  # Diagnostics are intentionally not recorded here. A recall is a
@@ -787,6 +787,9 @@ def run_recall(
787
787
  include_shared: bool = False,
788
788
  window: str | tuple[str, str] | None = None,
789
789
  as_of: str | None = None,
790
+ known_as_of: str | None = None,
791
+ valid_at: str | None = None,
792
+ include_unknown: bool = False,
790
793
  ) -> RecallResponse:
791
794
  """Recall relevant facts for a query.
792
795
 
@@ -836,6 +839,9 @@ def run_recall(
836
839
  include_shared=include_shared,
837
840
  window=window,
838
841
  as_of=as_of,
842
+ known_as_of=known_as_of,
843
+ valid_at=valid_at,
844
+ include_unknown=include_unknown,
839
845
  )
840
846
  _mark("retrieval(chan+rerank)")
841
847