agmem 0.1.1__py3-none-any.whl → 0.1.3__py3-none-any.whl

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 (100) hide show
  1. {agmem-0.1.1.dist-info → agmem-0.1.3.dist-info}/METADATA +157 -16
  2. agmem-0.1.3.dist-info/RECORD +105 -0
  3. memvcs/__init__.py +1 -1
  4. memvcs/cli.py +45 -31
  5. memvcs/commands/__init__.py +9 -9
  6. memvcs/commands/add.py +83 -76
  7. memvcs/commands/audit.py +59 -0
  8. memvcs/commands/blame.py +46 -53
  9. memvcs/commands/branch.py +13 -33
  10. memvcs/commands/checkout.py +27 -32
  11. memvcs/commands/clean.py +18 -23
  12. memvcs/commands/clone.py +11 -1
  13. memvcs/commands/commit.py +40 -39
  14. memvcs/commands/daemon.py +109 -76
  15. memvcs/commands/decay.py +77 -0
  16. memvcs/commands/diff.py +56 -57
  17. memvcs/commands/distill.py +90 -0
  18. memvcs/commands/federated.py +53 -0
  19. memvcs/commands/fsck.py +86 -61
  20. memvcs/commands/garden.py +40 -35
  21. memvcs/commands/gc.py +51 -0
  22. memvcs/commands/graph.py +41 -48
  23. memvcs/commands/init.py +16 -24
  24. memvcs/commands/log.py +25 -40
  25. memvcs/commands/merge.py +69 -27
  26. memvcs/commands/pack.py +129 -0
  27. memvcs/commands/prove.py +66 -0
  28. memvcs/commands/pull.py +31 -1
  29. memvcs/commands/push.py +4 -2
  30. memvcs/commands/recall.py +145 -0
  31. memvcs/commands/reflog.py +13 -22
  32. memvcs/commands/remote.py +1 -0
  33. memvcs/commands/repair.py +66 -0
  34. memvcs/commands/reset.py +23 -33
  35. memvcs/commands/resolve.py +130 -0
  36. memvcs/commands/resurrect.py +82 -0
  37. memvcs/commands/search.py +3 -4
  38. memvcs/commands/serve.py +2 -1
  39. memvcs/commands/show.py +66 -36
  40. memvcs/commands/stash.py +34 -34
  41. memvcs/commands/status.py +27 -35
  42. memvcs/commands/tag.py +23 -47
  43. memvcs/commands/test.py +30 -44
  44. memvcs/commands/timeline.py +111 -0
  45. memvcs/commands/tree.py +26 -27
  46. memvcs/commands/verify.py +110 -0
  47. memvcs/commands/when.py +115 -0
  48. memvcs/core/access_index.py +167 -0
  49. memvcs/core/audit.py +124 -0
  50. memvcs/core/config_loader.py +3 -1
  51. memvcs/core/consistency.py +214 -0
  52. memvcs/core/crypto_verify.py +280 -0
  53. memvcs/core/decay.py +185 -0
  54. memvcs/core/diff.py +158 -143
  55. memvcs/core/distiller.py +277 -0
  56. memvcs/core/encryption.py +169 -0
  57. memvcs/core/federated.py +86 -0
  58. memvcs/core/gardener.py +176 -145
  59. memvcs/core/hooks.py +48 -14
  60. memvcs/core/ipfs_remote.py +39 -0
  61. memvcs/core/knowledge_graph.py +135 -138
  62. memvcs/core/llm/__init__.py +10 -0
  63. memvcs/core/llm/anthropic_provider.py +50 -0
  64. memvcs/core/llm/base.py +27 -0
  65. memvcs/core/llm/factory.py +30 -0
  66. memvcs/core/llm/openai_provider.py +36 -0
  67. memvcs/core/merge.py +260 -170
  68. memvcs/core/objects.py +110 -101
  69. memvcs/core/pack.py +92 -0
  70. memvcs/core/pii_scanner.py +147 -146
  71. memvcs/core/privacy_budget.py +63 -0
  72. memvcs/core/refs.py +132 -115
  73. memvcs/core/remote.py +38 -0
  74. memvcs/core/repository.py +254 -164
  75. memvcs/core/schema.py +155 -113
  76. memvcs/core/staging.py +60 -65
  77. memvcs/core/storage/__init__.py +20 -18
  78. memvcs/core/storage/base.py +74 -70
  79. memvcs/core/storage/gcs.py +70 -68
  80. memvcs/core/storage/local.py +42 -40
  81. memvcs/core/storage/s3.py +105 -110
  82. memvcs/core/temporal_index.py +121 -0
  83. memvcs/core/test_runner.py +101 -93
  84. memvcs/core/trust.py +103 -0
  85. memvcs/core/vector_store.py +56 -36
  86. memvcs/core/zk_proofs.py +26 -0
  87. memvcs/integrations/mcp_server.py +1 -3
  88. memvcs/integrations/web_ui/server.py +25 -26
  89. memvcs/retrieval/__init__.py +22 -0
  90. memvcs/retrieval/base.py +54 -0
  91. memvcs/retrieval/pack.py +128 -0
  92. memvcs/retrieval/recaller.py +105 -0
  93. memvcs/retrieval/strategies.py +314 -0
  94. memvcs/utils/__init__.py +3 -3
  95. memvcs/utils/helpers.py +52 -52
  96. agmem-0.1.1.dist-info/RECORD +0 -67
  97. {agmem-0.1.1.dist-info → agmem-0.1.3.dist-info}/WHEEL +0 -0
  98. {agmem-0.1.1.dist-info → agmem-0.1.3.dist-info}/entry_points.txt +0 -0
  99. {agmem-0.1.1.dist-info → agmem-0.1.3.dist-info}/licenses/LICENSE +0 -0
  100. {agmem-0.1.1.dist-info → agmem-0.1.3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,90 @@
1
+ """
2
+ agmem distill - Episodic-to-semantic distillation pipeline.
3
+
4
+ Converts session logs into compact facts (memory consolidation).
5
+ """
6
+
7
+ import argparse
8
+ from pathlib import Path
9
+
10
+ from ..commands.base import require_repo
11
+ from ..core.distiller import Distiller, DistillerConfig, DistillerResult
12
+
13
+
14
+ class DistillCommand:
15
+ """Episodic-to-semantic distillation."""
16
+
17
+ name = "distill"
18
+ help = "Convert episodic logs into semantic facts (memory consolidation)"
19
+
20
+ @staticmethod
21
+ def add_arguments(parser: argparse.ArgumentParser):
22
+ parser.add_argument(
23
+ "--source",
24
+ "-s",
25
+ default="episodic",
26
+ help="Source directory (default: episodic)",
27
+ )
28
+ parser.add_argument(
29
+ "--target",
30
+ "-t",
31
+ default="semantic/consolidated",
32
+ help="Target directory (default: semantic/consolidated)",
33
+ )
34
+ parser.add_argument(
35
+ "--model",
36
+ "-m",
37
+ help="LLM model for extraction (e.g., gpt-4)",
38
+ )
39
+ parser.add_argument(
40
+ "--no-branch",
41
+ action="store_true",
42
+ help="Do not create safety branch",
43
+ )
44
+ parser.add_argument(
45
+ "--private",
46
+ action="store_true",
47
+ help="Use differential privacy (spend epsilon from budget)",
48
+ )
49
+
50
+ @staticmethod
51
+ def execute(args) -> int:
52
+ repo, code = require_repo()
53
+ if code != 0:
54
+ return code
55
+
56
+ if getattr(args, "private", False):
57
+ from ..core.privacy_budget import load_budget, spend_epsilon
58
+
59
+ spent, max_eps, _ = load_budget(repo.mem_dir)
60
+ epsilon_cost = 0.1
61
+ if not spend_epsilon(repo.mem_dir, epsilon_cost):
62
+ print(f"Privacy budget exceeded (spent {spent:.2f}, max {max_eps}).")
63
+ return 1
64
+ if spent + epsilon_cost > max_eps * 0.8:
65
+ print(f"Privacy budget low: {spent + epsilon_cost:.2f}/{max_eps}")
66
+
67
+ config = DistillerConfig(
68
+ source_dir=args.source,
69
+ target_dir=args.target,
70
+ create_safety_branch=not args.no_branch,
71
+ )
72
+ distiller = Distiller(repo, config)
73
+
74
+ result = distiller.run(
75
+ source=args.source,
76
+ target=args.target,
77
+ model=args.model,
78
+ )
79
+
80
+ print(f"Distiller completed:")
81
+ print(f" Clusters processed: {result.clusters_processed}")
82
+ print(f" Facts extracted: {result.facts_extracted}")
83
+ print(f" Episodes archived: {result.episodes_archived}")
84
+ if result.branch_created:
85
+ print(f" Branch created: {result.branch_created}")
86
+ if result.commit_hash:
87
+ print(f" Commit: {result.commit_hash[:8]}")
88
+ print(f"\n{result.message}")
89
+
90
+ return 0 if result.success else 1
@@ -0,0 +1,53 @@
1
+ """
2
+ agmem federated - Federated memory collaboration.
3
+
4
+ Push local summaries to coordinator; pull merged summaries.
5
+ """
6
+
7
+ import argparse
8
+
9
+ from ..commands.base import require_repo
10
+ from ..core.federated import get_federated_config, produce_local_summary, push_updates, pull_merged
11
+
12
+
13
+ class FederatedCommand:
14
+ """Federated memory collaboration with coordinator."""
15
+
16
+ name = "federated"
17
+ help = "Push/pull federated summaries (coordinator must be configured)"
18
+
19
+ @staticmethod
20
+ def add_arguments(parser: argparse.ArgumentParser):
21
+ parser.add_argument(
22
+ "action",
23
+ choices=["push", "pull"],
24
+ help="Push local summary or pull merged from coordinator",
25
+ )
26
+
27
+ @staticmethod
28
+ def execute(args) -> int:
29
+ repo, code = require_repo()
30
+ if code != 0:
31
+ return code
32
+
33
+ cfg = get_federated_config(repo.root)
34
+ if not cfg:
35
+ print(
36
+ "Federated collaboration not enabled. Set federated.enabled and coordinator_url in config."
37
+ )
38
+ return 1
39
+
40
+ if args.action == "push":
41
+ summary = produce_local_summary(repo.root, cfg["memory_types"])
42
+ msg = push_updates(repo.root, summary)
43
+ print(msg)
44
+ return 0 if "Pushed" in msg else 1
45
+ else:
46
+ data = pull_merged(repo.root)
47
+ if data is None:
48
+ print("Pull failed or coordinator unavailable.")
49
+ return 1
50
+ print("Merged summary from coordinator:")
51
+ for k, v in (data or {}).items():
52
+ print(f" {k}: {v}")
53
+ return 0
memvcs/commands/fsck.py CHANGED
@@ -10,44 +10,39 @@ from ..commands.base import require_repo
10
10
 
11
11
  class FsckCommand:
12
12
  """Check and repair repository consistency."""
13
-
14
- name = 'fsck'
15
- help = 'Check and repair repository consistency (remove dangling vectors)'
16
-
13
+
14
+ name = "fsck"
15
+ help = "Check and repair repository consistency (remove dangling vectors)"
16
+
17
17
  @staticmethod
18
18
  def add_arguments(parser: argparse.ArgumentParser):
19
19
  parser.add_argument(
20
- '--dry-run',
21
- action='store_true',
22
- help='Show what would be done without making changes'
23
- )
24
- parser.add_argument(
25
- '--verbose', '-v',
26
- action='store_true',
27
- help='Show detailed output'
20
+ "--dry-run", action="store_true", help="Show what would be done without making changes"
28
21
  )
22
+ parser.add_argument("--verbose", "-v", action="store_true", help="Show detailed output")
29
23
  parser.add_argument(
30
- '--fix',
31
- action='store_true',
32
- help='Actually remove dangling entries (required to make changes)'
24
+ "--fix",
25
+ action="store_true",
26
+ help="Actually remove dangling entries (required to make changes)",
33
27
  )
34
-
28
+
35
29
  @staticmethod
36
30
  def execute(args) -> int:
37
31
  repo, code = require_repo()
38
32
  if code != 0:
39
33
  return code
40
-
34
+
41
35
  print("Running file system consistency check...")
42
-
36
+
43
37
  issues_found = 0
44
38
  issues_fixed = 0
45
-
39
+
46
40
  # Check vector store for dangling entries
47
41
  try:
48
42
  from ..core.vector_store import VectorStore
49
- vs = VectorStore(repo.root / '.mem')
50
-
43
+
44
+ vs = VectorStore(repo.root / ".mem")
45
+
51
46
  vector_issues, vector_fixed = FsckCommand._check_vectors(
52
47
  repo, vs, args.dry_run, args.verbose, args.fix
53
48
  )
@@ -58,64 +53,66 @@ class FsckCommand:
58
53
  print("Vector store not available, skipping vector check")
59
54
  except Exception as e:
60
55
  print(f"Warning: Vector store check failed: {e}")
61
-
56
+
62
57
  # Check object store integrity
63
58
  obj_issues, obj_fixed = FsckCommand._check_objects(
64
59
  repo, args.dry_run, args.verbose, args.fix
65
60
  )
66
61
  issues_found += obj_issues
67
62
  issues_fixed += obj_fixed
68
-
63
+
69
64
  # Check refs integrity
70
- ref_issues, ref_fixed = FsckCommand._check_refs(
71
- repo, args.dry_run, args.verbose, args.fix
72
- )
65
+ ref_issues, ref_fixed = FsckCommand._check_refs(repo, args.dry_run, args.verbose, args.fix)
73
66
  issues_found += ref_issues
74
67
  issues_fixed += ref_fixed
75
-
68
+
69
+ # Cryptographic verification (Merkle + signature)
70
+ crypto_issues = FsckCommand._check_crypto(repo, args.verbose)
71
+ issues_found += crypto_issues
72
+
76
73
  # Print summary
77
74
  print()
78
75
  print("=" * 40)
79
76
  print("FSCK Summary")
80
77
  print("=" * 40)
81
78
  print(f"Issues found: {issues_found}")
82
-
79
+
83
80
  if args.fix:
84
81
  print(f"Issues fixed: {issues_fixed}")
85
82
  elif issues_found > 0:
86
83
  print("\nRun with --fix to repair issues")
87
-
84
+
88
85
  if issues_found == 0:
89
86
  print("Repository is healthy!")
90
-
87
+
91
88
  return 0 if issues_found == 0 else 1
92
-
89
+
93
90
  @staticmethod
94
91
  def _check_vectors(repo, vs, dry_run: bool, verbose: bool, fix: bool) -> tuple:
95
92
  """Check for dangling vector entries."""
96
93
  print("\nChecking vector store...")
97
-
98
- current_dir = repo.root / 'current'
94
+
95
+ current_dir = repo.root / "current"
99
96
  entries = vs.get_all_entries()
100
-
97
+
101
98
  dangling = []
102
-
99
+
103
100
  for entry in entries:
104
- path = entry['path']
101
+ path = entry["path"]
105
102
  full_path = current_dir / path
106
-
103
+
107
104
  if not full_path.exists():
108
105
  dangling.append(entry)
109
106
  if verbose:
110
107
  print(f" Dangling: {path} (rowid: {entry['rowid']})")
111
-
108
+
112
109
  if dangling:
113
110
  print(f" Found {len(dangling)} dangling vector entries")
114
-
111
+
115
112
  if fix and not dry_run:
116
113
  fixed = 0
117
114
  for entry in dangling:
118
- if vs.delete_entry(entry['rowid']):
115
+ if vs.delete_entry(entry["rowid"]):
119
116
  fixed += 1
120
117
  print(f" Removed {fixed} dangling entries")
121
118
  return len(dangling), fixed
@@ -123,22 +120,22 @@ class FsckCommand:
123
120
  print(" (dry-run: no changes made)")
124
121
  else:
125
122
  print(" Vector store is consistent")
126
-
123
+
127
124
  return len(dangling), 0
128
-
125
+
129
126
  @staticmethod
130
127
  def _check_objects(repo, dry_run: bool, verbose: bool, fix: bool) -> tuple:
131
128
  """Check object store integrity."""
132
129
  print("\nChecking object store...")
133
-
130
+
134
131
  issues = 0
135
-
132
+
136
133
  # Check if all referenced blobs exist
137
- for obj_type in ['blob', 'tree', 'commit', 'tag']:
138
- obj_dir = repo.root / '.mem' / 'objects' / obj_type
134
+ for obj_type in ["blob", "tree", "commit", "tag"]:
135
+ obj_dir = repo.root / ".mem" / "objects" / obj_type
139
136
  if not obj_dir.exists():
140
137
  continue
141
-
138
+
142
139
  for prefix_dir in obj_dir.iterdir():
143
140
  if not prefix_dir.is_dir():
144
141
  continue
@@ -146,6 +143,7 @@ class FsckCommand:
146
143
  try:
147
144
  # Try to read and decompress
148
145
  import zlib
146
+
149
147
  compressed = obj_file.read_bytes()
150
148
  zlib.decompress(compressed)
151
149
  except Exception as e:
@@ -153,51 +151,78 @@ class FsckCommand:
153
151
  if verbose:
154
152
  hash_id = prefix_dir.name + obj_file.name
155
153
  print(f" Corrupted {obj_type}: {hash_id[:8]}...")
156
-
154
+
157
155
  if issues == 0:
158
156
  print(" Object store is consistent")
159
157
  else:
160
158
  print(f" Found {issues} corrupted objects")
161
-
159
+
162
160
  return issues, 0 # Object repair not implemented
163
-
161
+
164
162
  @staticmethod
165
163
  def _check_refs(repo, dry_run: bool, verbose: bool, fix: bool) -> tuple:
166
164
  """Check refs integrity."""
167
165
  print("\nChecking refs...")
168
-
166
+
169
167
  issues = 0
170
-
168
+
171
169
  # Check if HEAD points to valid commit
172
170
  head = repo.refs.get_head()
173
- if head['type'] == 'branch':
174
- branch_commit = repo.refs.get_branch_commit(head['value'])
171
+ if head["type"] == "branch":
172
+ branch_commit = repo.refs.get_branch_commit(head["value"])
175
173
  if not branch_commit:
176
174
  issues += 1
177
175
  if verbose:
178
176
  print(f" HEAD branch '{head['value']}' has no commit")
179
- elif not repo.object_store.exists(branch_commit, 'commit'):
177
+ elif not repo.object_store.exists(branch_commit, "commit"):
180
178
  issues += 1
181
179
  if verbose:
182
180
  print(f" HEAD points to missing commit: {branch_commit[:8]}")
183
- elif head['type'] == 'detached':
184
- if not repo.object_store.exists(head['value'], 'commit'):
181
+ elif head["type"] == "detached":
182
+ if not repo.object_store.exists(head["value"], "commit"):
185
183
  issues += 1
186
184
  if verbose:
187
185
  print(f" Detached HEAD points to missing commit")
188
-
186
+
189
187
  # Check all branches
190
188
  branches = repo.refs.list_branches()
191
189
  for branch in branches:
192
190
  commit_hash = repo.refs.get_branch_commit(branch)
193
- if commit_hash and not repo.object_store.exists(commit_hash, 'commit'):
191
+ if commit_hash and not repo.object_store.exists(commit_hash, "commit"):
194
192
  issues += 1
195
193
  if verbose:
196
194
  print(f" Branch '{branch}' points to missing commit")
197
-
195
+
198
196
  if issues == 0:
199
197
  print(" Refs are consistent")
200
198
  else:
201
199
  print(f" Found {issues} ref issues")
202
-
200
+
203
201
  return issues, 0
202
+
203
+ @staticmethod
204
+ def _check_crypto(repo, verbose: bool) -> int:
205
+ """Verify Merkle/signature on branch tips. Returns number of issues."""
206
+ print("\nChecking commit signatures...")
207
+ try:
208
+ from ..core.crypto_verify import verify_commit, load_public_key
209
+ except ImportError:
210
+ if verbose:
211
+ print(" Crypto verification not available")
212
+ return 0
213
+ issues = 0
214
+ pub = load_public_key(repo.mem_dir)
215
+ for branch in repo.refs.list_branches():
216
+ ch = repo.refs.get_branch_commit(branch)
217
+ if not ch:
218
+ continue
219
+ ok, err = verify_commit(repo.object_store, ch, public_key_pem=pub, mem_dir=repo.mem_dir)
220
+ if not ok:
221
+ issues += 1
222
+ if verbose:
223
+ print(f" {branch} ({ch[:8]}): {err}")
224
+ if issues == 0:
225
+ print(" Commit signatures consistent")
226
+ else:
227
+ print(f" Found {issues} commit(s) with verification issues")
228
+ return issues
memvcs/commands/garden.py CHANGED
@@ -10,96 +10,101 @@ from ..core.gardener import Gardener, GardenerConfig
10
10
 
11
11
  class GardenCommand:
12
12
  """Run the Gardener reflection loop."""
13
-
14
- name = 'garden'
15
- help = 'Synthesize episodic memories into semantic insights'
16
-
13
+
14
+ name = "garden"
15
+ help = "Synthesize episodic memories into semantic insights"
16
+
17
17
  @staticmethod
18
18
  def add_arguments(parser: argparse.ArgumentParser):
19
19
  parser.add_argument(
20
- '--force',
21
- action='store_true',
22
- help='Run even if episode threshold not met'
20
+ "--force", action="store_true", help="Run even if episode threshold not met"
23
21
  )
24
22
  parser.add_argument(
25
- '--threshold',
23
+ "--threshold",
26
24
  type=int,
27
25
  default=50,
28
- help='Number of episodes before auto-triggering (default: 50)'
26
+ help="Number of episodes before auto-triggering (default: 50)",
29
27
  )
30
28
  parser.add_argument(
31
- '--dry-run',
32
- action='store_true',
33
- help='Show what would be done without making changes'
29
+ "--dry-run", action="store_true", help="Show what would be done without making changes"
34
30
  )
35
31
  parser.add_argument(
36
- '--no-commit',
37
- action='store_true',
38
- help='Do not auto-commit generated insights'
32
+ "--no-commit", action="store_true", help="Do not auto-commit generated insights"
39
33
  )
40
34
  parser.add_argument(
41
- '--llm',
42
- choices=['openai', 'none'],
43
- default='none',
44
- help='LLM provider for summarization (default: none)'
35
+ "--llm",
36
+ choices=["openai", "none"],
37
+ default="none",
38
+ help="LLM provider for summarization (default: none)",
45
39
  )
40
+ parser.add_argument("--model", help="LLM model to use (e.g., gpt-3.5-turbo)")
46
41
  parser.add_argument(
47
- '--model',
48
- help='LLM model to use (e.g., gpt-3.5-turbo)'
42
+ "--private",
43
+ action="store_true",
44
+ help="Use differential privacy (spend epsilon from budget)",
49
45
  )
50
-
46
+
51
47
  @staticmethod
52
48
  def execute(args) -> int:
53
49
  repo, code = require_repo()
54
50
  if code != 0:
55
51
  return code
56
-
52
+
53
+ if getattr(args, "private", False):
54
+ from ..core.privacy_budget import load_budget, spend_epsilon
55
+
56
+ spent, max_eps, _ = load_budget(repo.mem_dir)
57
+ epsilon_cost = 0.1
58
+ if not spend_epsilon(repo.mem_dir, epsilon_cost):
59
+ print(f"Privacy budget exceeded (spent {spent:.2f}, max {max_eps}).")
60
+ return 1
61
+
57
62
  # Build config
58
63
  config = GardenerConfig(
59
64
  threshold=args.threshold,
60
65
  auto_commit=not args.no_commit,
61
- llm_provider=args.llm if args.llm != 'none' else None,
62
- llm_model=args.model
66
+ llm_provider=args.llm if args.llm != "none" else None,
67
+ llm_model=args.model,
63
68
  )
64
-
69
+
65
70
  # Create gardener
66
71
  gardener = Gardener(repo, config)
67
-
72
+
68
73
  # Show status
69
74
  episode_count = gardener.get_episode_count()
70
75
  print(f"Episodic files: {episode_count}/{config.threshold}")
71
-
76
+
72
77
  if args.dry_run:
73
78
  if gardener.should_run() or args.force:
74
79
  episodes = gardener.load_episodes()
75
80
  clusters = gardener.cluster_episodes(episodes)
76
-
81
+
77
82
  print(f"\nWould process {len(episodes)} episodes into {len(clusters)} clusters:")
78
83
  for cluster in clusters:
79
84
  print(f" - {cluster.topic}: {len(cluster.episodes)} episodes")
80
-
85
+
81
86
  print("\nRun without --dry-run to execute.")
82
87
  else:
83
88
  print("\nThreshold not met. Use --force to run anyway.")
84
89
  return 0
85
-
90
+
86
91
  # Run gardener
87
92
  if not gardener.should_run() and not args.force:
88
93
  print("\nThreshold not met. Use --force to run anyway.")
89
94
  return 0
90
-
95
+
91
96
  print("\nRunning Gardener...")
92
97
  result = gardener.run(force=args.force)
93
-
98
+
94
99
  if result.success:
95
100
  print(f"\nGardener completed:")
96
101
  print(f" Clusters found: {result.clusters_found}")
97
102
  print(f" Insights generated: {result.insights_generated}")
98
103
  print(f" Episodes archived: {result.episodes_archived}")
99
-
104
+
100
105
  if result.commit_hash:
101
106
  print(f" Commit: {result.commit_hash[:8]}")
102
-
107
+
103
108
  print(f"\n{result.message}")
104
109
  return 0
105
110
  else:
memvcs/commands/gc.py ADDED
@@ -0,0 +1,51 @@
1
+ """
2
+ agmem gc - Garbage collection.
3
+
4
+ Remove unreachable objects; optionally repack.
5
+ """
6
+
7
+ import argparse
8
+
9
+ from ..commands.base import require_repo
10
+ from ..core.pack import run_gc
11
+
12
+
13
+ class GcCommand:
14
+ """Garbage collect unreachable objects."""
15
+
16
+ name = "gc"
17
+ help = "Remove unreachable objects (garbage collection)"
18
+
19
+ @staticmethod
20
+ def add_arguments(parser: argparse.ArgumentParser):
21
+ parser.add_argument(
22
+ "--dry-run",
23
+ action="store_true",
24
+ help="Report what would be removed without deleting",
25
+ )
26
+ parser.add_argument(
27
+ "--prune-days",
28
+ type=int,
29
+ default=90,
30
+ metavar="N",
31
+ help="Consider reflog entries within N days (default 90)",
32
+ )
33
+
34
+ @staticmethod
35
+ def execute(args) -> int:
36
+ repo, code = require_repo()
37
+ if code != 0:
38
+ return code
39
+
40
+ gc_prune_days = getattr(args, "prune_days", 90)
41
+ deleted, freed = run_gc(
42
+ repo.mem_dir,
43
+ repo.object_store,
44
+ gc_prune_days=gc_prune_days,
45
+ dry_run=args.dry_run,
46
+ )
47
+ if args.dry_run:
48
+ print(f"Would remove {deleted} unreachable object(s) ({freed} bytes).")
49
+ else:
50
+ print(f"Removed {deleted} unreachable object(s) ({freed} bytes reclaimed).")
51
+ return 0