agmem 0.1.4__tar.gz → 0.1.5__tar.gz

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 (145) hide show
  1. {agmem-0.1.4/agmem.egg-info → agmem-0.1.5}/PKG-INFO +1 -1
  2. {agmem-0.1.4 → agmem-0.1.5/agmem.egg-info}/PKG-INFO +1 -1
  3. {agmem-0.1.4 → agmem-0.1.5}/memvcs/commands/daemon.py +1 -0
  4. {agmem-0.1.4 → agmem-0.1.5}/memvcs/commands/gc.py +3 -1
  5. {agmem-0.1.4 → agmem-0.1.5}/memvcs/commands/prove.py +3 -1
  6. {agmem-0.1.4 → agmem-0.1.5}/memvcs/commands/timeline.py +1 -0
  7. {agmem-0.1.4 → agmem-0.1.5}/memvcs/commands/when.py +1 -0
  8. {agmem-0.1.4 → agmem-0.1.5}/memvcs/core/compression_pipeline.py +12 -4
  9. {agmem-0.1.4 → agmem-0.1.5}/memvcs/core/crypto_verify.py +2 -2
  10. {agmem-0.1.4 → agmem-0.1.5}/memvcs/core/distiller.py +54 -6
  11. {agmem-0.1.4 → agmem-0.1.5}/memvcs/core/federated.py +13 -3
  12. {agmem-0.1.4 → agmem-0.1.5}/memvcs/core/gardener.py +64 -8
  13. {agmem-0.1.4 → agmem-0.1.5}/memvcs/core/ipfs_remote.py +1 -2
  14. {agmem-0.1.4 → agmem-0.1.5}/memvcs/core/knowledge_graph.py +3 -1
  15. {agmem-0.1.4 → agmem-0.1.5}/memvcs/core/objects.py +2 -0
  16. {agmem-0.1.4 → agmem-0.1.5}/memvcs/core/pack.py +18 -4
  17. {agmem-0.1.4 → agmem-0.1.5}/memvcs/core/remote.py +12 -3
  18. {agmem-0.1.4 → agmem-0.1.5}/memvcs/core/zk_proofs.py +2 -0
  19. {agmem-0.1.4 → agmem-0.1.5}/pyproject.toml +1 -1
  20. {agmem-0.1.4 → agmem-0.1.5}/tests/test_federated.py +6 -2
  21. {agmem-0.1.4 → agmem-0.1.5}/tests/test_privacy_budget.py +21 -5
  22. {agmem-0.1.4 → agmem-0.1.5}/LICENSE +0 -0
  23. {agmem-0.1.4 → agmem-0.1.5}/MANIFEST.in +0 -0
  24. {agmem-0.1.4 → agmem-0.1.5}/README.md +0 -0
  25. {agmem-0.1.4 → agmem-0.1.5}/agmem.egg-info/SOURCES.txt +0 -0
  26. {agmem-0.1.4 → agmem-0.1.5}/agmem.egg-info/dependency_links.txt +0 -0
  27. {agmem-0.1.4 → agmem-0.1.5}/agmem.egg-info/entry_points.txt +0 -0
  28. {agmem-0.1.4 → agmem-0.1.5}/agmem.egg-info/requires.txt +0 -0
  29. {agmem-0.1.4 → agmem-0.1.5}/agmem.egg-info/top_level.txt +0 -0
  30. {agmem-0.1.4 → agmem-0.1.5}/docs/AGMEM_PUBLISHING_SETUP.md +0 -0
  31. {agmem-0.1.4 → agmem-0.1.5}/docs/CONFIG.md +0 -0
  32. {agmem-0.1.4 → agmem-0.1.5}/docs/FEDERATED.md +0 -0
  33. {agmem-0.1.4 → agmem-0.1.5}/docs/GTM.md +0 -0
  34. {agmem-0.1.4 → agmem-0.1.5}/docs/KNOWLEDGE_GRAPH.md +0 -0
  35. {agmem-0.1.4 → agmem-0.1.5}/docs/SEQUENTIAL_VALIDATION.md +0 -0
  36. {agmem-0.1.4 → agmem-0.1.5}/docs/TEST_REPORT.md +0 -0
  37. {agmem-0.1.4 → agmem-0.1.5}/docs/aux/INSTALL.md +0 -0
  38. {agmem-0.1.4 → agmem-0.1.5}/examples/basic_workflow.sh +0 -0
  39. {agmem-0.1.4 → agmem-0.1.5}/memvcs/__init__.py +0 -0
  40. {agmem-0.1.4 → agmem-0.1.5}/memvcs/cli.py +0 -0
  41. {agmem-0.1.4 → agmem-0.1.5}/memvcs/commands/__init__.py +0 -0
  42. {agmem-0.1.4 → agmem-0.1.5}/memvcs/commands/add.py +0 -0
  43. {agmem-0.1.4 → agmem-0.1.5}/memvcs/commands/audit.py +0 -0
  44. {agmem-0.1.4 → agmem-0.1.5}/memvcs/commands/base.py +0 -0
  45. {agmem-0.1.4 → agmem-0.1.5}/memvcs/commands/blame.py +0 -0
  46. {agmem-0.1.4 → agmem-0.1.5}/memvcs/commands/branch.py +0 -0
  47. {agmem-0.1.4 → agmem-0.1.5}/memvcs/commands/checkout.py +0 -0
  48. {agmem-0.1.4 → agmem-0.1.5}/memvcs/commands/clean.py +0 -0
  49. {agmem-0.1.4 → agmem-0.1.5}/memvcs/commands/clone.py +0 -0
  50. {agmem-0.1.4 → agmem-0.1.5}/memvcs/commands/commit.py +0 -0
  51. {agmem-0.1.4 → agmem-0.1.5}/memvcs/commands/decay.py +0 -0
  52. {agmem-0.1.4 → agmem-0.1.5}/memvcs/commands/diff.py +0 -0
  53. {agmem-0.1.4 → agmem-0.1.5}/memvcs/commands/distill.py +0 -0
  54. {agmem-0.1.4 → agmem-0.1.5}/memvcs/commands/federated.py +0 -0
  55. {agmem-0.1.4 → agmem-0.1.5}/memvcs/commands/fsck.py +0 -0
  56. {agmem-0.1.4 → agmem-0.1.5}/memvcs/commands/garden.py +0 -0
  57. {agmem-0.1.4 → agmem-0.1.5}/memvcs/commands/graph.py +0 -0
  58. {agmem-0.1.4 → agmem-0.1.5}/memvcs/commands/init.py +0 -0
  59. {agmem-0.1.4 → agmem-0.1.5}/memvcs/commands/log.py +0 -0
  60. {agmem-0.1.4 → agmem-0.1.5}/memvcs/commands/mcp.py +0 -0
  61. {agmem-0.1.4 → agmem-0.1.5}/memvcs/commands/merge.py +0 -0
  62. {agmem-0.1.4 → agmem-0.1.5}/memvcs/commands/pack.py +0 -0
  63. {agmem-0.1.4 → agmem-0.1.5}/memvcs/commands/pull.py +0 -0
  64. {agmem-0.1.4 → agmem-0.1.5}/memvcs/commands/push.py +0 -0
  65. {agmem-0.1.4 → agmem-0.1.5}/memvcs/commands/recall.py +0 -0
  66. {agmem-0.1.4 → agmem-0.1.5}/memvcs/commands/reflog.py +0 -0
  67. {agmem-0.1.4 → agmem-0.1.5}/memvcs/commands/remote.py +0 -0
  68. {agmem-0.1.4 → agmem-0.1.5}/memvcs/commands/repair.py +0 -0
  69. {agmem-0.1.4 → agmem-0.1.5}/memvcs/commands/reset.py +0 -0
  70. {agmem-0.1.4 → agmem-0.1.5}/memvcs/commands/resolve.py +0 -0
  71. {agmem-0.1.4 → agmem-0.1.5}/memvcs/commands/resurrect.py +0 -0
  72. {agmem-0.1.4 → agmem-0.1.5}/memvcs/commands/search.py +0 -0
  73. {agmem-0.1.4 → agmem-0.1.5}/memvcs/commands/serve.py +0 -0
  74. {agmem-0.1.4 → agmem-0.1.5}/memvcs/commands/show.py +0 -0
  75. {agmem-0.1.4 → agmem-0.1.5}/memvcs/commands/stash.py +0 -0
  76. {agmem-0.1.4 → agmem-0.1.5}/memvcs/commands/status.py +0 -0
  77. {agmem-0.1.4 → agmem-0.1.5}/memvcs/commands/tag.py +0 -0
  78. {agmem-0.1.4 → agmem-0.1.5}/memvcs/commands/test.py +0 -0
  79. {agmem-0.1.4 → agmem-0.1.5}/memvcs/commands/tree.py +0 -0
  80. {agmem-0.1.4 → agmem-0.1.5}/memvcs/commands/verify.py +0 -0
  81. {agmem-0.1.4 → agmem-0.1.5}/memvcs/core/__init__.py +0 -0
  82. {agmem-0.1.4 → agmem-0.1.5}/memvcs/core/access_index.py +0 -0
  83. {agmem-0.1.4 → agmem-0.1.5}/memvcs/core/audit.py +0 -0
  84. {agmem-0.1.4 → agmem-0.1.5}/memvcs/core/config_loader.py +0 -0
  85. {agmem-0.1.4 → agmem-0.1.5}/memvcs/core/consistency.py +0 -0
  86. {agmem-0.1.4 → agmem-0.1.5}/memvcs/core/constants.py +0 -0
  87. {agmem-0.1.4 → agmem-0.1.5}/memvcs/core/decay.py +0 -0
  88. {agmem-0.1.4 → agmem-0.1.5}/memvcs/core/diff.py +0 -0
  89. {agmem-0.1.4 → agmem-0.1.5}/memvcs/core/encryption.py +0 -0
  90. {agmem-0.1.4 → agmem-0.1.5}/memvcs/core/hooks.py +0 -0
  91. {agmem-0.1.4 → agmem-0.1.5}/memvcs/core/llm/__init__.py +0 -0
  92. {agmem-0.1.4 → agmem-0.1.5}/memvcs/core/llm/anthropic_provider.py +0 -0
  93. {agmem-0.1.4 → agmem-0.1.5}/memvcs/core/llm/base.py +0 -0
  94. {agmem-0.1.4 → agmem-0.1.5}/memvcs/core/llm/factory.py +0 -0
  95. {agmem-0.1.4 → agmem-0.1.5}/memvcs/core/llm/openai_provider.py +0 -0
  96. {agmem-0.1.4 → agmem-0.1.5}/memvcs/core/merge.py +0 -0
  97. {agmem-0.1.4 → agmem-0.1.5}/memvcs/core/pii_scanner.py +0 -0
  98. {agmem-0.1.4 → agmem-0.1.5}/memvcs/core/privacy_budget.py +0 -0
  99. {agmem-0.1.4 → agmem-0.1.5}/memvcs/core/refs.py +0 -0
  100. {agmem-0.1.4 → agmem-0.1.5}/memvcs/core/repository.py +0 -0
  101. {agmem-0.1.4 → agmem-0.1.5}/memvcs/core/schema.py +0 -0
  102. {agmem-0.1.4 → agmem-0.1.5}/memvcs/core/staging.py +0 -0
  103. {agmem-0.1.4 → agmem-0.1.5}/memvcs/core/storage/__init__.py +0 -0
  104. {agmem-0.1.4 → agmem-0.1.5}/memvcs/core/storage/base.py +0 -0
  105. {agmem-0.1.4 → agmem-0.1.5}/memvcs/core/storage/gcs.py +0 -0
  106. {agmem-0.1.4 → agmem-0.1.5}/memvcs/core/storage/local.py +0 -0
  107. {agmem-0.1.4 → agmem-0.1.5}/memvcs/core/storage/s3.py +0 -0
  108. {agmem-0.1.4 → agmem-0.1.5}/memvcs/core/temporal_index.py +0 -0
  109. {agmem-0.1.4 → agmem-0.1.5}/memvcs/core/test_runner.py +0 -0
  110. {agmem-0.1.4 → agmem-0.1.5}/memvcs/core/trust.py +0 -0
  111. {agmem-0.1.4 → agmem-0.1.5}/memvcs/core/vector_store.py +0 -0
  112. {agmem-0.1.4 → agmem-0.1.5}/memvcs/integrations/__init__.py +0 -0
  113. {agmem-0.1.4 → agmem-0.1.5}/memvcs/integrations/mcp_server.py +0 -0
  114. {agmem-0.1.4 → agmem-0.1.5}/memvcs/integrations/web_ui/__init__.py +0 -0
  115. {agmem-0.1.4 → agmem-0.1.5}/memvcs/integrations/web_ui/server.py +0 -0
  116. {agmem-0.1.4 → agmem-0.1.5}/memvcs/retrieval/__init__.py +0 -0
  117. {agmem-0.1.4 → agmem-0.1.5}/memvcs/retrieval/base.py +0 -0
  118. {agmem-0.1.4 → agmem-0.1.5}/memvcs/retrieval/pack.py +0 -0
  119. {agmem-0.1.4 → agmem-0.1.5}/memvcs/retrieval/recaller.py +0 -0
  120. {agmem-0.1.4 → agmem-0.1.5}/memvcs/retrieval/strategies.py +0 -0
  121. {agmem-0.1.4 → agmem-0.1.5}/memvcs/utils/__init__.py +0 -0
  122. {agmem-0.1.4 → agmem-0.1.5}/memvcs/utils/helpers.py +0 -0
  123. {agmem-0.1.4 → agmem-0.1.5}/setup.cfg +0 -0
  124. {agmem-0.1.4 → agmem-0.1.5}/setup.py +0 -0
  125. {agmem-0.1.4 → agmem-0.1.5}/tests/test_access_index.py +0 -0
  126. {agmem-0.1.4 → agmem-0.1.5}/tests/test_advanced_commands.py +0 -0
  127. {agmem-0.1.4 → agmem-0.1.5}/tests/test_audit.py +0 -0
  128. {agmem-0.1.4 → agmem-0.1.5}/tests/test_commit_importance.py +0 -0
  129. {agmem-0.1.4 → agmem-0.1.5}/tests/test_consistency.py +0 -0
  130. {agmem-0.1.4 → agmem-0.1.5}/tests/test_crypto_verify.py +0 -0
  131. {agmem-0.1.4 → agmem-0.1.5}/tests/test_decay.py +0 -0
  132. {agmem-0.1.4 → agmem-0.1.5}/tests/test_edge_cases.py +0 -0
  133. {agmem-0.1.4 → agmem-0.1.5}/tests/test_encryption.py +0 -0
  134. {agmem-0.1.4 → agmem-0.1.5}/tests/test_ipfs_remote.py +0 -0
  135. {agmem-0.1.4 → agmem-0.1.5}/tests/test_llm_provider.py +0 -0
  136. {agmem-0.1.4 → agmem-0.1.5}/tests/test_objects.py +0 -0
  137. {agmem-0.1.4 → agmem-0.1.5}/tests/test_pack_gc.py +0 -0
  138. {agmem-0.1.4 → agmem-0.1.5}/tests/test_pii.py +0 -0
  139. {agmem-0.1.4 → agmem-0.1.5}/tests/test_plan_features.py +0 -0
  140. {agmem-0.1.4 → agmem-0.1.5}/tests/test_repository.py +0 -0
  141. {agmem-0.1.4 → agmem-0.1.5}/tests/test_resolve_helpers.py +0 -0
  142. {agmem-0.1.4 → agmem-0.1.5}/tests/test_retrieval.py +0 -0
  143. {agmem-0.1.4 → agmem-0.1.5}/tests/test_temporal_index.py +0 -0
  144. {agmem-0.1.4 → agmem-0.1.5}/tests/test_trust.py +0 -0
  145. {agmem-0.1.4 → agmem-0.1.5}/tests/test_zk_proofs.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agmem
3
- Version: 0.1.4
3
+ Version: 0.1.5
4
4
  Summary: Agentic Memory Version Control System - Git for AI agent memories
5
5
  Home-page: https://github.com/vivek-tiwari-vt/agmem
6
6
  Author: agmem Team
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agmem
3
- Version: 0.1.4
3
+ Version: 0.1.5
4
4
  Summary: Agentic Memory Version Control System - Git for AI agent memories
5
5
  Home-page: https://github.com/vivek-tiwari-vt/agmem
6
6
  Author: agmem Team
@@ -208,6 +208,7 @@ class DaemonCommand:
208
208
  health_check_interval = 3600 # default 1 hour
209
209
  try:
210
210
  from ..core.config_loader import load_agmem_config
211
+
211
212
  config = load_agmem_config(repo.root)
212
213
  daemon_cfg = config.get("daemon") or {}
213
214
  health_check_interval = int(daemon_cfg.get("health_check_interval_seconds", 3600))
@@ -62,5 +62,7 @@ class GcCommand:
62
62
  dry_run=False,
63
63
  )
64
64
  if packed > 0:
65
- print(f"Packed {packed} object(s) into pack file ({repack_freed} bytes from loose).")
65
+ print(
66
+ f"Packed {packed} object(s) into pack file ({repack_freed} bytes from loose)."
67
+ )
66
68
  return 0
@@ -60,7 +60,9 @@ class ProveCommand:
60
60
  ok = prove_memory_freshness(path, args.value, out_path, mem_dir=repo.mem_dir)
61
61
 
62
62
  if not ok:
63
- print("Proof generation failed (keyword not in file, or signing key not set for freshness).")
63
+ print(
64
+ "Proof generation failed (keyword not in file, or signing key not set for freshness)."
65
+ )
64
66
  return 1
65
67
  print(f"Proof written to {out_path}")
66
68
  return 0
@@ -54,6 +54,7 @@ class TimelineCommand:
54
54
  if from_ts and to_ts:
55
55
  try:
56
56
  from ..core.temporal_index import TemporalIndex
57
+
57
58
  ti = TemporalIndex(repo.mem_dir, repo.object_store)
58
59
  range_entries = ti.range_query(from_ts, to_ts)
59
60
  commits_in_range = {ch for _, ch in range_entries}
@@ -66,6 +66,7 @@ class WhenCommand:
66
66
  if from_ts and to_ts:
67
67
  try:
68
68
  from ..core.temporal_index import TemporalIndex
69
+
69
70
  ti = TemporalIndex(repo.mem_dir, repo.object_store)
70
71
  range_entries = ti.range_query(from_ts, to_ts)
71
72
  commits_in_range = {ch for _, ch in range_entries}
@@ -18,7 +18,9 @@ DEDUP_HASH_ALGO = "sha256"
18
18
  TIER_HOT_DAYS = 7
19
19
 
20
20
 
21
- def chunk_by_size(text: str, size: int = CHUNK_SIZE_DEFAULT, overlap: int = CHUNK_OVERLAP) -> List[str]:
21
+ def chunk_by_size(
22
+ text: str, size: int = CHUNK_SIZE_DEFAULT, overlap: int = CHUNK_OVERLAP
23
+ ) -> List[str]:
22
24
  """Split text into chunks by character size with optional overlap."""
23
25
  if not text or size <= 0:
24
26
  return []
@@ -37,7 +39,7 @@ def chunk_by_sentences(text: str, max_chunk_chars: int = 512) -> List[str]:
37
39
  """Split text into chunks by sentence boundaries, up to max_chunk_chars per chunk."""
38
40
  if not text:
39
41
  return []
40
- sentences = re.split(r'(?<=[.!?])\s+', text)
42
+ sentences = re.split(r"(?<=[.!?])\s+", text)
41
43
  chunks = []
42
44
  current = []
43
45
  current_len = 0
@@ -94,7 +96,10 @@ def dedup_by_similarity_threshold(
94
96
  embeddings = vector_store.embed(items)
95
97
  kept = [items[0]]
96
98
  for i in range(1, len(items)):
97
- sims = [vector_store.similarity(embeddings[i], vector_store.embed([kept[j]])[0]) for j in range(len(kept))]
99
+ sims = [
100
+ vector_store.similarity(embeddings[i], vector_store.embed([kept[j]])[0])
101
+ for j in range(len(kept))
102
+ ]
98
103
  if not any(s >= threshold for s in sims):
99
104
  kept.append(items[i])
100
105
  return kept
@@ -144,12 +149,15 @@ class CompressionPipeline:
144
149
  if self.dedup_hash:
145
150
  chunk_tuples = dedup_by_hash(chunks)
146
151
  else:
147
- chunk_tuples = [(c, hashlib.new(DEDUP_HASH_ALGO, c.encode()).hexdigest()) for c in chunks]
152
+ chunk_tuples = [
153
+ (c, hashlib.new(DEDUP_HASH_ALGO, c.encode()).hexdigest()) for c in chunks
154
+ ]
148
155
  tier = None
149
156
  if self.tier_by_recency and path and path.exists():
150
157
  try:
151
158
  mtime = path.stat().st_mtime
152
159
  from datetime import datetime, timezone
160
+
153
161
  age_days = (datetime.now(timezone.utc).timestamp() - mtime) / 86400
154
162
  tier = "hot" if age_days <= TIER_HOT_DAYS else "cold"
155
163
  except Exception:
@@ -239,7 +239,7 @@ def verify_commit(
239
239
  stored_sig = (commit.metadata or {}).get("signature")
240
240
  if not stored_root:
241
241
  return (False, "commit has no merkle_root (unverified)")
242
-
242
+
243
243
  # Verify that blob objects can be loaded successfully (detects tampering in compressed/encrypted content)
244
244
  blob_hashes = _collect_blob_hashes_from_tree(store, commit.tree)
245
245
  for blob_hash in blob_hashes:
@@ -249,7 +249,7 @@ def verify_commit(
249
249
  return (False, f"blob {blob_hash[:8]} corrupted or missing")
250
250
  except Exception as e:
251
251
  return (False, f"merkle_root mismatch (commit tampered)")
252
-
252
+
253
253
  computed_root = build_merkle_root_for_commit(store, commit_hash)
254
254
  if not computed_root:
255
255
  return (False, "could not build Merkle tree (missing tree/blobs)")
@@ -164,9 +164,16 @@ class Distiller:
164
164
  out_path = self.target_dir / f"consolidated-{ts}.md"
165
165
 
166
166
  confidence_score = self.config.extraction_confidence_threshold
167
- if self.config.use_dp and self.config.dp_epsilon is not None and self.config.dp_delta is not None:
167
+ if (
168
+ self.config.use_dp
169
+ and self.config.dp_epsilon is not None
170
+ and self.config.dp_delta is not None
171
+ ):
168
172
  from .privacy_budget import add_noise
169
- confidence_score = add_noise(confidence_score, 0.1, self.config.dp_epsilon, self.config.dp_delta)
173
+
174
+ confidence_score = add_noise(
175
+ confidence_score, 0.1, self.config.dp_epsilon, self.config.dp_delta
176
+ )
170
177
  confidence_score = max(0.0, min(1.0, confidence_score))
171
178
  frontmatter = {
172
179
  "schema_version": "1.0",
@@ -277,12 +284,53 @@ class Distiller:
277
284
  clusters_processed = len(clusters)
278
285
  facts_extracted = facts_count
279
286
  episodes_archived = archived
280
- if self.config.use_dp and self.config.dp_epsilon is not None and self.config.dp_delta is not None:
287
+ if (
288
+ self.config.use_dp
289
+ and self.config.dp_epsilon is not None
290
+ and self.config.dp_delta is not None
291
+ ):
281
292
  from .privacy_budget import add_noise
293
+
282
294
  sensitivity = 1.0
283
- clusters_processed = max(0, int(round(add_noise(float(clusters_processed), sensitivity, self.config.dp_epsilon, self.config.dp_delta))))
284
- facts_extracted = max(0, int(round(add_noise(float(facts_extracted), sensitivity, self.config.dp_epsilon, self.config.dp_delta))))
285
- episodes_archived = max(0, int(round(add_noise(float(episodes_archived), sensitivity, self.config.dp_epsilon, self.config.dp_delta))))
295
+ clusters_processed = max(
296
+ 0,
297
+ int(
298
+ round(
299
+ add_noise(
300
+ float(clusters_processed),
301
+ sensitivity,
302
+ self.config.dp_epsilon,
303
+ self.config.dp_delta,
304
+ )
305
+ )
306
+ ),
307
+ )
308
+ facts_extracted = max(
309
+ 0,
310
+ int(
311
+ round(
312
+ add_noise(
313
+ float(facts_extracted),
314
+ sensitivity,
315
+ self.config.dp_epsilon,
316
+ self.config.dp_delta,
317
+ )
318
+ )
319
+ ),
320
+ )
321
+ episodes_archived = max(
322
+ 0,
323
+ int(
324
+ round(
325
+ add_noise(
326
+ float(episodes_archived),
327
+ sensitivity,
328
+ self.config.dp_epsilon,
329
+ self.config.dp_delta,
330
+ )
331
+ )
332
+ ),
333
+ )
286
334
 
287
335
  return DistillerResult(
288
336
  success=True,
@@ -48,6 +48,7 @@ def _extract_topic_from_md(path: Path, content: str) -> str:
48
48
  if end > 0:
49
49
  try:
50
50
  import yaml
51
+
51
52
  fm = yaml.safe_load(content[3:end])
52
53
  if isinstance(fm, dict):
53
54
  tags = fm.get("tags", [])
@@ -62,7 +63,11 @@ def _extract_topic_from_md(path: Path, content: str) -> str:
62
63
 
63
64
 
64
65
  def produce_local_summary(
65
- repo_root: Path, memory_types: List[str], use_dp: bool = False, dp_epsilon: float = 0.1, dp_delta: float = 1e-5
66
+ repo_root: Path,
67
+ memory_types: List[str],
68
+ use_dp: bool = False,
69
+ dp_epsilon: float = 0.1,
70
+ dp_delta: float = 1e-5,
66
71
  ) -> Dict[str, Any]:
67
72
  """
68
73
  Produce a local summary from episodic/semantic data (no raw content).
@@ -101,10 +106,15 @@ def produce_local_summary(
101
106
 
102
107
  if use_dp and dp_epsilon and dp_delta:
103
108
  from .privacy_budget import add_noise
109
+
104
110
  for mtype in summary["topics"]:
105
111
  raw = summary["topics"][mtype]
106
- summary["topics"][mtype] = max(0, int(round(add_noise(float(raw), 1.0, dp_epsilon, dp_delta))))
107
- summary["fact_count"] = max(0, int(round(add_noise(float(summary["fact_count"]), 1.0, dp_epsilon, dp_delta))))
112
+ summary["topics"][mtype] = max(
113
+ 0, int(round(add_noise(float(raw), 1.0, dp_epsilon, dp_delta)))
114
+ )
115
+ summary["fact_count"] = max(
116
+ 0, int(round(add_noise(float(summary["fact_count"]), 1.0, dp_epsilon, dp_delta)))
117
+ )
108
118
 
109
119
  return summary
110
120
 
@@ -356,11 +356,26 @@ class Gardener:
356
356
 
357
357
  # Generate frontmatter (optionally noised for differential privacy)
358
358
  source_episodes = len(cluster.episodes)
359
- if self.config.use_dp and self.config.dp_epsilon is not None and self.config.dp_delta is not None:
359
+ if (
360
+ self.config.use_dp
361
+ and self.config.dp_epsilon is not None
362
+ and self.config.dp_delta is not None
363
+ ):
360
364
  from .privacy_budget import add_noise
361
- source_episodes = max(0, int(round(add_noise(
362
- float(source_episodes), 1.0, self.config.dp_epsilon, self.config.dp_delta
363
- ))))
365
+
366
+ source_episodes = max(
367
+ 0,
368
+ int(
369
+ round(
370
+ add_noise(
371
+ float(source_episodes),
372
+ 1.0,
373
+ self.config.dp_epsilon,
374
+ self.config.dp_delta,
375
+ )
376
+ )
377
+ ),
378
+ )
364
379
  frontmatter = {
365
380
  "schema_version": "1.0",
366
381
  "last_updated": datetime.utcnow().isoformat() + "Z",
@@ -499,12 +514,53 @@ class Gardener:
499
514
  clusters_found = len(clusters)
500
515
  insights_generated = insights_written
501
516
  episodes_archived = archived_count
502
- if self.config.use_dp and self.config.dp_epsilon is not None and self.config.dp_delta is not None:
517
+ if (
518
+ self.config.use_dp
519
+ and self.config.dp_epsilon is not None
520
+ and self.config.dp_delta is not None
521
+ ):
503
522
  from .privacy_budget import add_noise
523
+
504
524
  sensitivity = 1.0
505
- clusters_found = max(0, int(round(add_noise(float(clusters_found), sensitivity, self.config.dp_epsilon, self.config.dp_delta))))
506
- insights_generated = max(0, int(round(add_noise(float(insights_generated), sensitivity, self.config.dp_epsilon, self.config.dp_delta))))
507
- episodes_archived = max(0, int(round(add_noise(float(episodes_archived), sensitivity, self.config.dp_epsilon, self.config.dp_delta))))
525
+ clusters_found = max(
526
+ 0,
527
+ int(
528
+ round(
529
+ add_noise(
530
+ float(clusters_found),
531
+ sensitivity,
532
+ self.config.dp_epsilon,
533
+ self.config.dp_delta,
534
+ )
535
+ )
536
+ ),
537
+ )
538
+ insights_generated = max(
539
+ 0,
540
+ int(
541
+ round(
542
+ add_noise(
543
+ float(insights_generated),
544
+ sensitivity,
545
+ self.config.dp_epsilon,
546
+ self.config.dp_delta,
547
+ )
548
+ )
549
+ ),
550
+ )
551
+ episodes_archived = max(
552
+ 0,
553
+ int(
554
+ round(
555
+ add_noise(
556
+ float(episodes_archived),
557
+ sensitivity,
558
+ self.config.dp_epsilon,
559
+ self.config.dp_delta,
560
+ )
561
+ )
562
+ ),
563
+ )
508
564
 
509
565
  return GardenerResult(
510
566
  success=True,
@@ -103,8 +103,7 @@ def _add_to_ipfs_gateway(bundle: bytes, gateway_url: str) -> Optional[str]:
103
103
  body = (
104
104
  b"--" + boundary.encode() + b"\r\n"
105
105
  b'Content-Disposition: form-data; name="file"; filename="agmem-bundle.bin"\r\n'
106
- b"Content-Type: application/octet-stream\r\n\r\n"
107
- + bundle + b"\r\n"
106
+ b"Content-Type: application/octet-stream\r\n\r\n" + bundle + b"\r\n"
108
107
  b"--" + boundary.encode() + b"--\r\n"
109
108
  )
110
109
  try:
@@ -326,7 +326,9 @@ class KnowledgeGraphBuilder:
326
326
  common = file_entities.get(path1, set()) & file_entities.get(path2, set())
327
327
  if common:
328
328
  w = min(1.0, 0.3 + 0.1 * len(common))
329
- edge = GraphEdge(source=path1, target=path2, edge_type="co_occurrence", weight=w)
329
+ edge = GraphEdge(
330
+ source=path1, target=path2, edge_type="co_occurrence", weight=w
331
+ )
330
332
  edges.append(edge)
331
333
  if self._graph is not None:
332
334
  self._graph.add_edge(path1, path2, type="co_occurrence", weight=w)
@@ -110,6 +110,7 @@ class ObjectStore:
110
110
  # Try pack file when loose object missing
111
111
  try:
112
112
  from .pack import retrieve_from_pack
113
+
113
114
  result = retrieve_from_pack(self.objects_dir, hash_id, expected_type=obj_type)
114
115
  if result is not None:
115
116
  return result[1]
@@ -126,6 +127,7 @@ class ObjectStore:
126
127
  return True
127
128
  try:
128
129
  from .pack import retrieve_from_pack
130
+
129
131
  return retrieve_from_pack(self.objects_dir, hash_id, expected_type=obj_type) is not None
130
132
  except Exception:
131
133
  return False
@@ -21,7 +21,12 @@ OBJ_TYPE_BLOB = 1
21
21
  OBJ_TYPE_TREE = 2
22
22
  OBJ_TYPE_COMMIT = 3
23
23
  OBJ_TYPE_TAG = 4
24
- TYPE_TO_BYTE = {"blob": OBJ_TYPE_BLOB, "tree": OBJ_TYPE_TREE, "commit": OBJ_TYPE_COMMIT, "tag": OBJ_TYPE_TAG}
24
+ TYPE_TO_BYTE = {
25
+ "blob": OBJ_TYPE_BLOB,
26
+ "tree": OBJ_TYPE_TREE,
27
+ "commit": OBJ_TYPE_COMMIT,
28
+ "tag": OBJ_TYPE_TAG,
29
+ }
25
30
  BYTE_TO_TYPE = {v: k for k, v in TYPE_TO_BYTE.items()}
26
31
 
27
32
 
@@ -152,7 +157,12 @@ def write_pack(
152
157
  if not index_entries:
153
158
  raise ValueError("No objects to pack")
154
159
 
155
- pack_content = PACK_MAGIC + struct.pack(">I", PACK_VERSION) + struct.pack(">I", len(index_entries)) + bytes(pack_body)
160
+ pack_content = (
161
+ PACK_MAGIC
162
+ + struct.pack(">I", PACK_VERSION)
163
+ + struct.pack(">I", len(index_entries))
164
+ + bytes(pack_body)
165
+ )
156
166
  pack_hash = hashlib.sha256(pack_content).digest()
157
167
  pack_content += pack_hash
158
168
 
@@ -160,7 +170,9 @@ def write_pack(
160
170
  pack_path = pack_d / pack_name
161
171
  pack_path.write_bytes(pack_content)
162
172
 
163
- index_content = bytearray(IDX_MAGIC + struct.pack(">I", IDX_VERSION) + struct.pack(">I", len(index_entries)))
173
+ index_content = bytearray(
174
+ IDX_MAGIC + struct.pack(">I", IDX_VERSION) + struct.pack(">I", len(index_entries))
175
+ )
164
176
  for hash_id, obj_type, off in index_entries:
165
177
  index_content.extend(bytes.fromhex(hash_id))
166
178
  index_content.append(TYPE_TO_BYTE[obj_type])
@@ -184,7 +196,9 @@ def _find_pack_index(objects_dir: Path) -> Optional[Path]:
184
196
  return None
185
197
 
186
198
 
187
- def retrieve_from_pack(objects_dir: Path, hash_id: str, expected_type: Optional[str] = None) -> Optional[Tuple[str, bytes]]:
199
+ def retrieve_from_pack(
200
+ objects_dir: Path, hash_id: str, expected_type: Optional[str] = None
201
+ ) -> Optional[Tuple[str, bytes]]:
188
202
  """
189
203
  Retrieve object from pack by hash. Returns (obj_type, content) or None.
190
204
  If expected_type is set, only return if pack type matches.
@@ -70,6 +70,7 @@ def _collect_objects_from_commit(store: ObjectStore, commit_hash: str) -> Set[st
70
70
  def _read_object_from_adapter(adapter: Any, hash_id: str) -> Optional[tuple]:
71
71
  """Read object from storage adapter. Returns (obj_type, content_bytes) or None."""
72
72
  import zlib
73
+
73
74
  for obj_type in ["commit", "tree", "blob", "tag"]:
74
75
  rel = f".mem/objects/{obj_type}/{hash_id[:2]}/{hash_id[2:]}"
75
76
  if not adapter.exists(rel):
@@ -78,7 +79,7 @@ def _read_object_from_adapter(adapter: Any, hash_id: str) -> Optional[tuple]:
78
79
  raw = adapter.read_file(rel)
79
80
  full = zlib.decompress(raw)
80
81
  null_idx = full.index(b"\0")
81
- content = full[null_idx + 1:]
82
+ content = full[null_idx + 1 :]
82
83
  return (obj_type, content)
83
84
  except Exception:
84
85
  continue
@@ -240,7 +241,10 @@ class Remote:
240
241
  adapter.write_file(f".mem/refs/tags/{t}", (ch + "\n").encode())
241
242
  try:
242
243
  from .audit import append_audit
243
- append_audit(self.mem_dir, "push", {"remote": self.name, "branch": branch, "copied": copied})
244
+
245
+ append_audit(
246
+ self.mem_dir, "push", {"remote": self.name, "branch": branch, "copied": copied}
247
+ )
244
248
  except Exception:
245
249
  pass
246
250
  return f"Pushed {copied} object(s) to {self.name}"
@@ -290,7 +294,10 @@ class Remote:
290
294
  break
291
295
  try:
292
296
  from .audit import append_audit
293
- append_audit(self.mem_dir, "fetch", {"remote": self.name, "branch": branch, "copied": copied})
297
+
298
+ append_audit(
299
+ self.mem_dir, "fetch", {"remote": self.name, "branch": branch, "copied": copied}
300
+ )
294
301
  except Exception:
295
302
  pass
296
303
  return f"Fetched {copied} object(s) from {self.name}"
@@ -308,6 +315,7 @@ class Remote:
308
315
  try:
309
316
  from .storage import get_adapter
310
317
  from .storage.base import LockError
318
+
311
319
  adapter = get_adapter(url, self._config)
312
320
  lock_name = "agmem-push"
313
321
  adapter.acquire_lock(lock_name, 30)
@@ -423,6 +431,7 @@ class Remote:
423
431
  try:
424
432
  from .storage import get_adapter
425
433
  from .storage.base import LockError
434
+
426
435
  adapter = get_adapter(url, self._config)
427
436
  lock_name = "agmem-fetch"
428
437
  adapter.acquire_lock(lock_name, 30)
@@ -79,6 +79,7 @@ def prove_memory_freshness(
79
79
  stat = memory_path.stat()
80
80
  ts = stat.st_mtime
81
81
  from datetime import datetime, timezone
82
+
82
83
  iso_ts = datetime.fromtimestamp(ts, tz=timezone.utc).isoformat()
83
84
  except Exception:
84
85
  return False
@@ -150,6 +151,7 @@ def verify_proof(proof_path: Path, statement_type: str, **kwargs: Any) -> bool:
150
151
  return False
151
152
  try:
152
153
  from datetime import datetime
154
+
153
155
  after_dt = datetime.fromisoformat(after_ts.replace("Z", "+00:00"))
154
156
  ts_dt = datetime.fromisoformat(ts_str.replace("Z", "+00:00"))
155
157
  return ts_dt >= after_dt
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "agmem"
7
- version = "0.1.4"
7
+ version = "0.1.5"
8
8
  description = "Agentic Memory Version Control System - Git for AI agent memories"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -20,7 +20,9 @@ class TestProduceLocalSummary:
20
20
  with tempfile.TemporaryDirectory() as tmpdir:
21
21
  root = Path(tmpdir)
22
22
  (root / "current" / "semantic").mkdir(parents=True)
23
- (root / "current" / "semantic" / "prefs.md").write_text("# User prefs\n- prefers dark mode")
23
+ (root / "current" / "semantic" / "prefs.md").write_text(
24
+ "# User prefs\n- prefers dark mode"
25
+ )
24
26
  summary = produce_local_summary(root, ["semantic"])
25
27
  assert "topics" in summary
26
28
  assert summary["topics"].get("semantic", 0) >= 1
@@ -34,7 +36,9 @@ class TestProduceLocalSummary:
34
36
  for i in range(3):
35
37
  (root / "current" / "episodic" / f"e{i}.md").write_text(f"episode {i}")
36
38
  raw = produce_local_summary(root, ["episodic"], use_dp=False)
37
- noised = produce_local_summary(root, ["episodic"], use_dp=True, dp_epsilon=0.5, dp_delta=1e-5)
39
+ noised = produce_local_summary(
40
+ root, ["episodic"], use_dp=True, dp_epsilon=0.5, dp_delta=1e-5
41
+ )
38
42
  assert "topics" in noised
39
43
  assert "fact_count" in noised
40
44
 
@@ -97,10 +97,14 @@ class TestGardenerDistillerDPIntegration:
97
97
  self.refs = None
98
98
 
99
99
  fake_repo = FakeRepo(root)
100
- config = GardenerConfig(threshold=2, auto_commit=False, use_dp=True, dp_epsilon=0.1, dp_delta=1e-5)
100
+ config = GardenerConfig(
101
+ threshold=2, auto_commit=False, use_dp=True, dp_epsilon=0.1, dp_delta=1e-5
102
+ )
101
103
  gardener = Gardener(fake_repo, config)
102
104
 
103
- with patch("memvcs.core.privacy_budget.add_noise", side_effect=lambda v, s, e, d: v + 1):
105
+ with patch(
106
+ "memvcs.core.privacy_budget.add_noise", side_effect=lambda v, s, e, d: v + 1
107
+ ):
104
108
  result = gardener.run(force=True)
105
109
  assert result.success
106
110
  assert result.clusters_found >= 0
@@ -125,13 +129,25 @@ class TestGardenerDistillerDPIntegration:
125
129
  self.current_dir = r / "current"
126
130
  self.mem_dir = r / ".mem"
127
131
  self.object_store = None
128
- self.refs = type("R", (), {"branch_exists": lambda n: True, "create_branch": lambda n: None, "get_current_branch": lambda: "main"})()
132
+ self.refs = type(
133
+ "R",
134
+ (),
135
+ {
136
+ "branch_exists": lambda n: True,
137
+ "create_branch": lambda n: None,
138
+ "get_current_branch": lambda: "main",
139
+ },
140
+ )()
129
141
 
130
142
  fake_repo = FakeRepo(root)
131
- config = DistillerConfig(create_safety_branch=False, use_dp=True, dp_epsilon=0.1, dp_delta=1e-5)
143
+ config = DistillerConfig(
144
+ create_safety_branch=False, use_dp=True, dp_epsilon=0.1, dp_delta=1e-5
145
+ )
132
146
  distiller = Distiller(fake_repo, config)
133
147
 
134
- with patch("memvcs.core.privacy_budget.add_noise", side_effect=lambda v, s, e, d: v + 0.5):
148
+ with patch(
149
+ "memvcs.core.privacy_budget.add_noise", side_effect=lambda v, s, e, d: v + 0.5
150
+ ):
135
151
  result = distiller.run()
136
152
  assert result.success
137
153
  assert result.facts_extracted >= 0
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes