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.
- {agmem-0.1.1.dist-info → agmem-0.1.3.dist-info}/METADATA +157 -16
- agmem-0.1.3.dist-info/RECORD +105 -0
- memvcs/__init__.py +1 -1
- memvcs/cli.py +45 -31
- memvcs/commands/__init__.py +9 -9
- memvcs/commands/add.py +83 -76
- memvcs/commands/audit.py +59 -0
- memvcs/commands/blame.py +46 -53
- memvcs/commands/branch.py +13 -33
- memvcs/commands/checkout.py +27 -32
- memvcs/commands/clean.py +18 -23
- memvcs/commands/clone.py +11 -1
- memvcs/commands/commit.py +40 -39
- memvcs/commands/daemon.py +109 -76
- memvcs/commands/decay.py +77 -0
- memvcs/commands/diff.py +56 -57
- memvcs/commands/distill.py +90 -0
- memvcs/commands/federated.py +53 -0
- memvcs/commands/fsck.py +86 -61
- memvcs/commands/garden.py +40 -35
- memvcs/commands/gc.py +51 -0
- memvcs/commands/graph.py +41 -48
- memvcs/commands/init.py +16 -24
- memvcs/commands/log.py +25 -40
- memvcs/commands/merge.py +69 -27
- memvcs/commands/pack.py +129 -0
- memvcs/commands/prove.py +66 -0
- memvcs/commands/pull.py +31 -1
- memvcs/commands/push.py +4 -2
- memvcs/commands/recall.py +145 -0
- memvcs/commands/reflog.py +13 -22
- memvcs/commands/remote.py +1 -0
- memvcs/commands/repair.py +66 -0
- memvcs/commands/reset.py +23 -33
- memvcs/commands/resolve.py +130 -0
- memvcs/commands/resurrect.py +82 -0
- memvcs/commands/search.py +3 -4
- memvcs/commands/serve.py +2 -1
- memvcs/commands/show.py +66 -36
- memvcs/commands/stash.py +34 -34
- memvcs/commands/status.py +27 -35
- memvcs/commands/tag.py +23 -47
- memvcs/commands/test.py +30 -44
- memvcs/commands/timeline.py +111 -0
- memvcs/commands/tree.py +26 -27
- memvcs/commands/verify.py +110 -0
- memvcs/commands/when.py +115 -0
- memvcs/core/access_index.py +167 -0
- memvcs/core/audit.py +124 -0
- memvcs/core/config_loader.py +3 -1
- memvcs/core/consistency.py +214 -0
- memvcs/core/crypto_verify.py +280 -0
- memvcs/core/decay.py +185 -0
- memvcs/core/diff.py +158 -143
- memvcs/core/distiller.py +277 -0
- memvcs/core/encryption.py +169 -0
- memvcs/core/federated.py +86 -0
- memvcs/core/gardener.py +176 -145
- memvcs/core/hooks.py +48 -14
- memvcs/core/ipfs_remote.py +39 -0
- memvcs/core/knowledge_graph.py +135 -138
- memvcs/core/llm/__init__.py +10 -0
- memvcs/core/llm/anthropic_provider.py +50 -0
- memvcs/core/llm/base.py +27 -0
- memvcs/core/llm/factory.py +30 -0
- memvcs/core/llm/openai_provider.py +36 -0
- memvcs/core/merge.py +260 -170
- memvcs/core/objects.py +110 -101
- memvcs/core/pack.py +92 -0
- memvcs/core/pii_scanner.py +147 -146
- memvcs/core/privacy_budget.py +63 -0
- memvcs/core/refs.py +132 -115
- memvcs/core/remote.py +38 -0
- memvcs/core/repository.py +254 -164
- memvcs/core/schema.py +155 -113
- memvcs/core/staging.py +60 -65
- memvcs/core/storage/__init__.py +20 -18
- memvcs/core/storage/base.py +74 -70
- memvcs/core/storage/gcs.py +70 -68
- memvcs/core/storage/local.py +42 -40
- memvcs/core/storage/s3.py +105 -110
- memvcs/core/temporal_index.py +121 -0
- memvcs/core/test_runner.py +101 -93
- memvcs/core/trust.py +103 -0
- memvcs/core/vector_store.py +56 -36
- memvcs/core/zk_proofs.py +26 -0
- memvcs/integrations/mcp_server.py +1 -3
- memvcs/integrations/web_ui/server.py +25 -26
- memvcs/retrieval/__init__.py +22 -0
- memvcs/retrieval/base.py +54 -0
- memvcs/retrieval/pack.py +128 -0
- memvcs/retrieval/recaller.py +105 -0
- memvcs/retrieval/strategies.py +314 -0
- memvcs/utils/__init__.py +3 -3
- memvcs/utils/helpers.py +52 -52
- agmem-0.1.1.dist-info/RECORD +0 -67
- {agmem-0.1.1.dist-info → agmem-0.1.3.dist-info}/WHEEL +0 -0
- {agmem-0.1.1.dist-info → agmem-0.1.3.dist-info}/entry_points.txt +0 -0
- {agmem-0.1.1.dist-info → agmem-0.1.3.dist-info}/licenses/LICENSE +0 -0
- {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 =
|
|
15
|
-
help =
|
|
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
|
-
|
|
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
|
-
|
|
31
|
-
action=
|
|
32
|
-
help=
|
|
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
|
-
|
|
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 /
|
|
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[
|
|
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[
|
|
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 [
|
|
138
|
-
obj_dir = repo.root /
|
|
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[
|
|
174
|
-
branch_commit = repo.refs.get_branch_commit(head[
|
|
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,
|
|
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[
|
|
184
|
-
if not repo.object_store.exists(head[
|
|
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,
|
|
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 =
|
|
15
|
-
help =
|
|
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
|
-
|
|
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
|
-
|
|
23
|
+
"--threshold",
|
|
26
24
|
type=int,
|
|
27
25
|
default=50,
|
|
28
|
-
help=
|
|
26
|
+
help="Number of episodes before auto-triggering (default: 50)",
|
|
29
27
|
)
|
|
30
28
|
parser.add_argument(
|
|
31
|
-
|
|
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
|
-
|
|
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
|
-
|
|
42
|
-
choices=[
|
|
43
|
-
default=
|
|
44
|
-
help=
|
|
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
|
-
|
|
48
|
-
|
|
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 !=
|
|
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
|