agmem 0.1.1__py3-none-any.whl → 0.1.2__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.2.dist-info}/METADATA +20 -3
- agmem-0.1.2.dist-info/RECORD +86 -0
- memvcs/__init__.py +1 -1
- memvcs/cli.py +35 -31
- memvcs/commands/__init__.py +9 -9
- memvcs/commands/add.py +77 -76
- 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 +4 -1
- memvcs/commands/commit.py +40 -39
- memvcs/commands/daemon.py +81 -76
- memvcs/commands/decay.py +77 -0
- memvcs/commands/diff.py +56 -57
- memvcs/commands/distill.py +74 -0
- memvcs/commands/fsck.py +55 -61
- memvcs/commands/garden.py +28 -37
- memvcs/commands/graph.py +41 -48
- memvcs/commands/init.py +16 -24
- memvcs/commands/log.py +25 -40
- memvcs/commands/merge.py +16 -28
- memvcs/commands/pack.py +129 -0
- memvcs/commands/pull.py +4 -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/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 +59 -0
- memvcs/commands/when.py +115 -0
- memvcs/core/access_index.py +167 -0
- memvcs/core/config_loader.py +3 -1
- memvcs/core/consistency.py +214 -0
- memvcs/core/decay.py +185 -0
- memvcs/core/diff.py +158 -143
- memvcs/core/distiller.py +277 -0
- memvcs/core/gardener.py +164 -132
- memvcs/core/hooks.py +48 -14
- memvcs/core/knowledge_graph.py +134 -138
- memvcs/core/merge.py +248 -171
- memvcs/core/objects.py +95 -96
- memvcs/core/pii_scanner.py +147 -146
- memvcs/core/refs.py +132 -115
- memvcs/core/repository.py +174 -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 +112 -0
- memvcs/core/test_runner.py +101 -93
- memvcs/core/vector_store.py +41 -35
- 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.2.dist-info}/WHEEL +0 -0
- {agmem-0.1.1.dist-info → agmem-0.1.2.dist-info}/entry_points.txt +0 -0
- {agmem-0.1.1.dist-info → agmem-0.1.2.dist-info}/licenses/LICENSE +0 -0
- {agmem-0.1.1.dist-info → agmem-0.1.2.dist-info}/top_level.txt +0 -0
memvcs/commands/daemon.py
CHANGED
|
@@ -14,34 +14,34 @@ from ..commands.base import require_repo
|
|
|
14
14
|
|
|
15
15
|
class DaemonCommand:
|
|
16
16
|
"""Control the auto-sync daemon."""
|
|
17
|
-
|
|
18
|
-
name =
|
|
19
|
-
help =
|
|
20
|
-
|
|
17
|
+
|
|
18
|
+
name = "daemon"
|
|
19
|
+
help = "Start/stop the auto-sync daemon for automatic commits"
|
|
20
|
+
|
|
21
21
|
@staticmethod
|
|
22
22
|
def add_arguments(parser: argparse.ArgumentParser):
|
|
23
23
|
parser.add_argument(
|
|
24
|
-
|
|
25
|
-
choices=[
|
|
26
|
-
help=
|
|
24
|
+
"action",
|
|
25
|
+
choices=["start", "stop", "status", "run"],
|
|
26
|
+
help="Daemon action: start, stop, status, or run (foreground)",
|
|
27
27
|
)
|
|
28
28
|
parser.add_argument(
|
|
29
|
-
|
|
29
|
+
"--debounce",
|
|
30
30
|
type=int,
|
|
31
31
|
default=30,
|
|
32
|
-
help=
|
|
32
|
+
help="Seconds to wait after changes before committing (default: 30)",
|
|
33
33
|
)
|
|
34
|
+
parser.add_argument("--pidfile", help="PID file path (default: .mem/daemon.pid)")
|
|
34
35
|
parser.add_argument(
|
|
35
|
-
|
|
36
|
-
help='PID file path (default: .mem/daemon.pid)'
|
|
36
|
+
"--distill", action="store_true", help="Run distillation pipeline nightly (with daemon)"
|
|
37
37
|
)
|
|
38
|
-
|
|
38
|
+
|
|
39
39
|
@staticmethod
|
|
40
40
|
def execute(args) -> int:
|
|
41
41
|
repo, code = require_repo()
|
|
42
42
|
if code != 0:
|
|
43
43
|
return code
|
|
44
|
-
|
|
44
|
+
|
|
45
45
|
if args.pidfile:
|
|
46
46
|
pid_file = Path(args.pidfile).resolve()
|
|
47
47
|
try:
|
|
@@ -50,21 +50,21 @@ class DaemonCommand:
|
|
|
50
50
|
print("Error: --pidfile must be under repository root")
|
|
51
51
|
return 1
|
|
52
52
|
else:
|
|
53
|
-
pid_file = repo.root /
|
|
54
|
-
|
|
55
|
-
if args.action ==
|
|
56
|
-
return DaemonCommand._start(repo, pid_file, args.debounce)
|
|
57
|
-
elif args.action ==
|
|
53
|
+
pid_file = repo.root / ".mem" / "daemon.pid"
|
|
54
|
+
|
|
55
|
+
if args.action == "start":
|
|
56
|
+
return DaemonCommand._start(repo, pid_file, args.debounce, args.distill)
|
|
57
|
+
elif args.action == "stop":
|
|
58
58
|
return DaemonCommand._stop(pid_file)
|
|
59
|
-
elif args.action ==
|
|
59
|
+
elif args.action == "status":
|
|
60
60
|
return DaemonCommand._status(pid_file)
|
|
61
|
-
elif args.action ==
|
|
62
|
-
return DaemonCommand._run(repo, args.debounce)
|
|
63
|
-
|
|
61
|
+
elif args.action == "run":
|
|
62
|
+
return DaemonCommand._run(repo, args.debounce, pid_file=pid_file, distill=args.distill)
|
|
63
|
+
|
|
64
64
|
return 1
|
|
65
|
-
|
|
65
|
+
|
|
66
66
|
@staticmethod
|
|
67
|
-
def _start(repo, pid_file: Path, debounce: int) -> int:
|
|
67
|
+
def _start(repo, pid_file: Path, debounce: int, distill: bool = False) -> int:
|
|
68
68
|
"""Start daemon in background."""
|
|
69
69
|
# Check if already running
|
|
70
70
|
if pid_file.exists():
|
|
@@ -75,13 +75,13 @@ class DaemonCommand:
|
|
|
75
75
|
return 1
|
|
76
76
|
except OSError:
|
|
77
77
|
pid_file.unlink()
|
|
78
|
-
|
|
78
|
+
|
|
79
79
|
# Fork to background (Unix only)
|
|
80
|
-
if os.name !=
|
|
80
|
+
if os.name != "posix":
|
|
81
81
|
print("Background daemon not supported on this platform.")
|
|
82
82
|
print("Use 'agmem daemon run' to run in foreground.")
|
|
83
83
|
return 1
|
|
84
|
-
|
|
84
|
+
|
|
85
85
|
# First fork
|
|
86
86
|
try:
|
|
87
87
|
pid = os.fork()
|
|
@@ -92,11 +92,11 @@ class DaemonCommand:
|
|
|
92
92
|
except OSError as e:
|
|
93
93
|
print(f"Fork failed: {e}")
|
|
94
94
|
return 1
|
|
95
|
-
|
|
95
|
+
|
|
96
96
|
# Decouple from parent
|
|
97
97
|
os.setsid()
|
|
98
98
|
os.umask(0)
|
|
99
|
-
|
|
99
|
+
|
|
100
100
|
# Second fork
|
|
101
101
|
try:
|
|
102
102
|
pid = os.fork()
|
|
@@ -104,26 +104,26 @@ class DaemonCommand:
|
|
|
104
104
|
sys.exit(0)
|
|
105
105
|
except OSError:
|
|
106
106
|
sys.exit(1)
|
|
107
|
-
|
|
107
|
+
|
|
108
108
|
# Write PID file
|
|
109
109
|
pid_file.write_text(str(os.getpid()))
|
|
110
|
-
|
|
110
|
+
|
|
111
111
|
# Redirect stdio
|
|
112
112
|
sys.stdout.flush()
|
|
113
113
|
sys.stderr.flush()
|
|
114
|
-
|
|
115
|
-
# Run daemon
|
|
116
|
-
return DaemonCommand._run(repo, debounce, pid_file)
|
|
117
|
-
|
|
114
|
+
|
|
115
|
+
# Run daemon in background
|
|
116
|
+
return DaemonCommand._run(repo, debounce, pid_file, distill)
|
|
117
|
+
|
|
118
118
|
@staticmethod
|
|
119
119
|
def _stop(pid_file: Path) -> int:
|
|
120
120
|
"""Stop running daemon."""
|
|
121
121
|
if not pid_file.exists():
|
|
122
122
|
print("No daemon running")
|
|
123
123
|
return 0
|
|
124
|
-
|
|
124
|
+
|
|
125
125
|
pid = int(pid_file.read_text().strip())
|
|
126
|
-
|
|
126
|
+
|
|
127
127
|
try:
|
|
128
128
|
os.kill(pid, signal.SIGTERM)
|
|
129
129
|
print(f"Stopped daemon (PID: {pid})")
|
|
@@ -133,16 +133,16 @@ class DaemonCommand:
|
|
|
133
133
|
print(f"Could not stop daemon: {e}")
|
|
134
134
|
pid_file.unlink()
|
|
135
135
|
return 1
|
|
136
|
-
|
|
136
|
+
|
|
137
137
|
@staticmethod
|
|
138
138
|
def _status(pid_file: Path) -> int:
|
|
139
139
|
"""Show daemon status."""
|
|
140
140
|
if not pid_file.exists():
|
|
141
141
|
print("Daemon is not running")
|
|
142
142
|
return 0
|
|
143
|
-
|
|
143
|
+
|
|
144
144
|
pid = int(pid_file.read_text().strip())
|
|
145
|
-
|
|
145
|
+
|
|
146
146
|
try:
|
|
147
147
|
os.kill(pid, 0)
|
|
148
148
|
print(f"Daemon is running (PID: {pid})")
|
|
@@ -151,9 +151,9 @@ class DaemonCommand:
|
|
|
151
151
|
print("Daemon is not running (stale PID file)")
|
|
152
152
|
pid_file.unlink()
|
|
153
153
|
return 0
|
|
154
|
-
|
|
154
|
+
|
|
155
155
|
@staticmethod
|
|
156
|
-
def _run(repo, debounce: int, pid_file: Path = None) -> int:
|
|
156
|
+
def _run(repo, debounce: int, pid_file: Path = None, distill: bool = False) -> int:
|
|
157
157
|
"""Run daemon in foreground."""
|
|
158
158
|
try:
|
|
159
159
|
from watchdog.observers import Observer
|
|
@@ -161,84 +161,92 @@ class DaemonCommand:
|
|
|
161
161
|
except ImportError:
|
|
162
162
|
print("Daemon requires watchdog. Install with: pip install agmem[daemon]")
|
|
163
163
|
return 1
|
|
164
|
-
|
|
165
|
-
current_dir = repo.root /
|
|
166
|
-
|
|
164
|
+
|
|
165
|
+
current_dir = repo.root / "current"
|
|
166
|
+
|
|
167
167
|
if not current_dir.exists():
|
|
168
168
|
print("No current/ directory to watch")
|
|
169
169
|
return 1
|
|
170
|
-
|
|
170
|
+
|
|
171
171
|
class MemoryFileHandler(FileSystemEventHandler):
|
|
172
172
|
def __init__(self):
|
|
173
173
|
self.last_change = 0
|
|
174
174
|
self.pending = False
|
|
175
|
-
|
|
175
|
+
|
|
176
176
|
def on_any_event(self, event):
|
|
177
177
|
# Ignore .mem and hidden files
|
|
178
|
-
if
|
|
178
|
+
if ".mem" in event.src_path or "/." in event.src_path:
|
|
179
179
|
return
|
|
180
|
-
|
|
180
|
+
|
|
181
181
|
# Ignore directories
|
|
182
182
|
if event.is_directory:
|
|
183
183
|
return
|
|
184
|
-
|
|
184
|
+
|
|
185
185
|
self.last_change = time.time()
|
|
186
186
|
self.pending = True
|
|
187
|
-
|
|
187
|
+
|
|
188
188
|
handler = MemoryFileHandler()
|
|
189
189
|
observer = Observer()
|
|
190
190
|
observer.schedule(handler, str(current_dir), recursive=True)
|
|
191
191
|
observer.start()
|
|
192
|
-
|
|
192
|
+
|
|
193
193
|
print(f"Watching {current_dir} (debounce: {debounce}s)")
|
|
194
194
|
print("Press Ctrl+C to stop")
|
|
195
|
-
|
|
195
|
+
|
|
196
196
|
# Handle signals
|
|
197
197
|
running = True
|
|
198
|
-
|
|
198
|
+
|
|
199
199
|
def signal_handler(signum, frame):
|
|
200
200
|
nonlocal running
|
|
201
201
|
running = False
|
|
202
|
-
|
|
202
|
+
|
|
203
203
|
signal.signal(signal.SIGTERM, signal_handler)
|
|
204
204
|
signal.signal(signal.SIGINT, signal_handler)
|
|
205
|
-
|
|
205
|
+
|
|
206
206
|
try:
|
|
207
207
|
while running:
|
|
208
208
|
time.sleep(1)
|
|
209
|
-
|
|
209
|
+
|
|
210
210
|
if handler.pending:
|
|
211
211
|
elapsed = time.time() - handler.last_change
|
|
212
212
|
if elapsed >= debounce:
|
|
213
213
|
# Auto-commit
|
|
214
214
|
DaemonCommand._auto_commit(repo)
|
|
215
|
+
if distill:
|
|
216
|
+
try:
|
|
217
|
+
from ..core.distiller import Distiller, DistillerConfig
|
|
218
|
+
|
|
219
|
+
d = Distiller(repo, DistillerConfig(create_safety_branch=False))
|
|
220
|
+
d.run()
|
|
221
|
+
except Exception:
|
|
222
|
+
pass
|
|
215
223
|
handler.pending = False
|
|
216
224
|
finally:
|
|
217
225
|
observer.stop()
|
|
218
226
|
observer.join()
|
|
219
|
-
|
|
227
|
+
|
|
220
228
|
if pid_file and pid_file.exists():
|
|
221
229
|
pid_file.unlink()
|
|
222
|
-
|
|
230
|
+
|
|
223
231
|
print("Daemon stopped")
|
|
224
232
|
return 0
|
|
225
|
-
|
|
233
|
+
|
|
226
234
|
@staticmethod
|
|
227
235
|
def _auto_commit(repo):
|
|
228
236
|
"""Perform automatic commit."""
|
|
229
237
|
from datetime import datetime
|
|
230
|
-
|
|
238
|
+
|
|
231
239
|
try:
|
|
232
240
|
# Check for changes
|
|
233
241
|
status = repo.get_status()
|
|
234
|
-
|
|
235
|
-
if not status.get(
|
|
242
|
+
|
|
243
|
+
if not status.get("modified") and not status.get("untracked"):
|
|
236
244
|
return
|
|
237
|
-
|
|
245
|
+
|
|
238
246
|
# Stage all changes in current/ (validate path stays under current/)
|
|
239
|
-
current_dir = repo.root /
|
|
240
|
-
for memory_file in current_dir.glob(
|
|
241
|
-
if memory_file.is_file() and
|
|
247
|
+
current_dir = repo.root / "current"
|
|
248
|
+
for memory_file in current_dir.glob("**/*"):
|
|
249
|
+
if memory_file.is_file() and ".mem" not in str(memory_file):
|
|
242
250
|
try:
|
|
243
251
|
rel_path = memory_file.relative_to(current_dir)
|
|
244
252
|
except ValueError:
|
|
@@ -250,18 +258,15 @@ class DaemonCommand:
|
|
|
250
258
|
repo.stage_file(rel_str)
|
|
251
259
|
except Exception:
|
|
252
260
|
pass
|
|
253
|
-
|
|
261
|
+
|
|
254
262
|
# Commit
|
|
255
|
-
timestamp = datetime.utcnow().strftime(
|
|
256
|
-
repo.commit(
|
|
257
|
-
|
|
258
|
-
{'auto_commit': True}
|
|
259
|
-
)
|
|
260
|
-
|
|
263
|
+
timestamp = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
|
|
264
|
+
repo.commit(f"auto: update memory state ({timestamp})", {"auto_commit": True})
|
|
265
|
+
|
|
261
266
|
print(f"[{timestamp}] Auto-committed changes")
|
|
262
|
-
|
|
267
|
+
|
|
263
268
|
except Exception as e:
|
|
264
269
|
# Write conflict lock if there's an issue
|
|
265
|
-
conflict_file = repo.root /
|
|
270
|
+
conflict_file = repo.root / ".mem" / "CONFLICT.lock"
|
|
266
271
|
conflict_file.write_text(f"Auto-commit failed: {e}")
|
|
267
272
|
print(f"Warning: Auto-commit failed, wrote CONFLICT.lock: {e}")
|
memvcs/commands/decay.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""
|
|
2
|
+
agmem decay - Memory decay and forgetting.
|
|
3
|
+
|
|
4
|
+
Archives low-importance, rarely-accessed memories.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from ..commands.base import require_repo
|
|
11
|
+
from ..core.decay import DecayEngine, DecayConfig
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class DecayCommand:
|
|
15
|
+
"""Memory decay - archive low-importance memories."""
|
|
16
|
+
|
|
17
|
+
name = "decay"
|
|
18
|
+
help = "Archive low-importance, old episodic memories (decay/forgetting)"
|
|
19
|
+
|
|
20
|
+
@staticmethod
|
|
21
|
+
def add_arguments(parser: argparse.ArgumentParser):
|
|
22
|
+
parser.add_argument(
|
|
23
|
+
"--dry-run",
|
|
24
|
+
action="store_true",
|
|
25
|
+
help="Preview what would be archived without making changes",
|
|
26
|
+
)
|
|
27
|
+
parser.add_argument(
|
|
28
|
+
"--apply",
|
|
29
|
+
action="store_true",
|
|
30
|
+
help="Actually archive the memories",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
@staticmethod
|
|
34
|
+
def execute(args) -> int:
|
|
35
|
+
repo, code = require_repo()
|
|
36
|
+
if code != 0:
|
|
37
|
+
return code
|
|
38
|
+
|
|
39
|
+
config = DecayConfig()
|
|
40
|
+
repo_config = repo.get_config()
|
|
41
|
+
decay_cfg = repo_config.get("decay", {})
|
|
42
|
+
if decay_cfg:
|
|
43
|
+
config.episodic_half_life_days = decay_cfg.get(
|
|
44
|
+
"episodic_half_life_days", config.episodic_half_life_days
|
|
45
|
+
)
|
|
46
|
+
config.semantic_min_importance = decay_cfg.get(
|
|
47
|
+
"semantic_min_importance", config.semantic_min_importance
|
|
48
|
+
)
|
|
49
|
+
config.access_count_threshold = decay_cfg.get(
|
|
50
|
+
"access_count_threshold", config.access_count_threshold
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
engine = DecayEngine(repo, config)
|
|
54
|
+
candidates = engine.get_decay_candidates()
|
|
55
|
+
|
|
56
|
+
if not candidates:
|
|
57
|
+
print("No memories eligible for decay.")
|
|
58
|
+
return 0
|
|
59
|
+
|
|
60
|
+
print(f"Found {len(candidates)} memory(ies) eligible for decay:")
|
|
61
|
+
for c in candidates[:20]:
|
|
62
|
+
print(f" - {c.path} (score: {c.decay_score:.2f}) - {c.reason}")
|
|
63
|
+
if len(candidates) > 20:
|
|
64
|
+
print(f" ... and {len(candidates) - 20} more")
|
|
65
|
+
|
|
66
|
+
if args.dry_run:
|
|
67
|
+
print("\nDry run - no changes made. Use --apply to archive.")
|
|
68
|
+
return 0
|
|
69
|
+
|
|
70
|
+
if not args.apply:
|
|
71
|
+
print("\nUse --apply to archive these memories.")
|
|
72
|
+
return 0
|
|
73
|
+
|
|
74
|
+
count = engine.apply_decay(candidates)
|
|
75
|
+
print(f"\nArchived {count} memory(ies) to .mem/forgetting/")
|
|
76
|
+
print("Use 'agmem resurrect <path>' to restore.")
|
|
77
|
+
return 0
|
memvcs/commands/diff.py
CHANGED
|
@@ -13,33 +13,35 @@ from ..core.repository import Repository
|
|
|
13
13
|
|
|
14
14
|
class DiffCommand:
|
|
15
15
|
"""Show differences between commits."""
|
|
16
|
-
|
|
17
|
-
name =
|
|
18
|
-
help =
|
|
19
|
-
|
|
16
|
+
|
|
17
|
+
name = "diff"
|
|
18
|
+
help = "Show changes between commits, commit and working tree, etc."
|
|
19
|
+
|
|
20
20
|
@staticmethod
|
|
21
21
|
def add_arguments(parser: argparse.ArgumentParser):
|
|
22
|
+
parser.add_argument("ref1", nargs="?", help="First commit/branch to compare")
|
|
23
|
+
parser.add_argument("ref2", nargs="?", help="Second commit/branch to compare")
|
|
22
24
|
parser.add_argument(
|
|
23
|
-
|
|
24
|
-
nargs='?',
|
|
25
|
-
help='First commit/branch to compare'
|
|
25
|
+
"--cached", "--staged", action="store_true", help="Show changes staged for commit"
|
|
26
26
|
)
|
|
27
27
|
parser.add_argument(
|
|
28
|
-
|
|
29
|
-
nargs='?',
|
|
30
|
-
help='Second commit/branch to compare'
|
|
28
|
+
"--stat", action="store_true", help="Show diffstat instead of full diff"
|
|
31
29
|
)
|
|
32
30
|
parser.add_argument(
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
"--from",
|
|
32
|
+
"--from-ref",
|
|
33
|
+
dest="from_ref",
|
|
34
|
+
metavar="REF",
|
|
35
|
+
help="Start ref for diff (supports ISO dates)",
|
|
36
36
|
)
|
|
37
37
|
parser.add_argument(
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
"--to",
|
|
39
|
+
"--to-ref",
|
|
40
|
+
dest="to_ref",
|
|
41
|
+
metavar="REF",
|
|
42
|
+
help="End ref for diff (supports ISO dates)",
|
|
41
43
|
)
|
|
42
|
-
|
|
44
|
+
|
|
43
45
|
@staticmethod
|
|
44
46
|
def execute(args) -> int:
|
|
45
47
|
repo, code = require_repo()
|
|
@@ -47,7 +49,7 @@ class DiffCommand:
|
|
|
47
49
|
return code
|
|
48
50
|
|
|
49
51
|
engine = DiffEngine(repo.object_store)
|
|
50
|
-
|
|
52
|
+
|
|
51
53
|
# Determine what to diff
|
|
52
54
|
if args.cached:
|
|
53
55
|
# Diff staged changes against HEAD
|
|
@@ -55,103 +57,100 @@ class DiffCommand:
|
|
|
55
57
|
if not head_commit:
|
|
56
58
|
print("No commits yet. Nothing to diff.")
|
|
57
59
|
return 0
|
|
58
|
-
|
|
60
|
+
|
|
59
61
|
staged_files = repo.staging.get_staged_files()
|
|
60
62
|
if not staged_files:
|
|
61
63
|
print("No staged changes.")
|
|
62
64
|
return 0
|
|
63
|
-
|
|
65
|
+
|
|
64
66
|
# Get staged content
|
|
65
67
|
working_files = {}
|
|
66
68
|
for path, sf in staged_files.items():
|
|
67
69
|
from ..core.objects import Blob
|
|
70
|
+
|
|
68
71
|
blob = Blob.load(repo.object_store, sf.blob_hash)
|
|
69
72
|
if blob:
|
|
70
73
|
working_files[path] = blob.content
|
|
71
|
-
|
|
72
|
-
tree_diff = engine.diff_working_dir(
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
)
|
|
76
|
-
|
|
77
|
-
print(engine.format_diff(tree_diff, 'HEAD', 'staged'))
|
|
74
|
+
|
|
75
|
+
tree_diff = engine.diff_working_dir(head_commit.store(repo.object_store), working_files)
|
|
76
|
+
|
|
77
|
+
print(engine.format_diff(tree_diff, "HEAD", "staged"))
|
|
78
78
|
return 0
|
|
79
|
-
|
|
80
|
-
# Diff between two refs
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
79
|
+
|
|
80
|
+
# Diff between two refs (--from/--to override ref1/ref2)
|
|
81
|
+
ref1 = getattr(args, "from_ref", None) or args.ref1
|
|
82
|
+
ref2 = getattr(args, "to_ref", None) or args.ref2
|
|
83
|
+
if ref1 and ref2:
|
|
84
|
+
commit1 = repo.resolve_ref(ref1)
|
|
85
|
+
commit2 = repo.resolve_ref(ref2)
|
|
86
|
+
|
|
85
87
|
if not commit1:
|
|
86
|
-
print(f"Error: Unknown revision: {
|
|
88
|
+
print(f"Error: Unknown revision: {ref1}")
|
|
87
89
|
return 1
|
|
88
90
|
if not commit2:
|
|
89
|
-
print(f"Error: Unknown revision: {
|
|
91
|
+
print(f"Error: Unknown revision: {ref2}")
|
|
90
92
|
return 1
|
|
91
|
-
|
|
93
|
+
|
|
92
94
|
tree_diff = engine.diff_commits(commit1, commit2)
|
|
93
|
-
|
|
95
|
+
|
|
94
96
|
if args.stat:
|
|
95
97
|
print(f" {tree_diff.added_count} file(s) added")
|
|
96
98
|
print(f" {tree_diff.deleted_count} file(s) deleted")
|
|
97
99
|
print(f" {tree_diff.modified_count} file(s) modified")
|
|
98
100
|
else:
|
|
99
|
-
print(engine.format_diff(tree_diff,
|
|
100
|
-
|
|
101
|
+
print(engine.format_diff(tree_diff, ref1, ref2))
|
|
102
|
+
|
|
101
103
|
return 0
|
|
102
|
-
|
|
104
|
+
|
|
103
105
|
# Diff working tree against a ref
|
|
104
106
|
if args.ref1:
|
|
105
107
|
commit_hash = repo.resolve_ref(args.ref1)
|
|
106
108
|
if not commit_hash:
|
|
107
109
|
print(f"Error: Unknown revision: {args.ref1}")
|
|
108
110
|
return 1
|
|
109
|
-
|
|
111
|
+
|
|
110
112
|
# Get working directory files
|
|
111
113
|
working_files = {}
|
|
112
114
|
for root, dirs, files in os.walk(repo.current_dir):
|
|
113
|
-
dirs[:] = [d for d in dirs if not d.startswith(
|
|
115
|
+
dirs[:] = [d for d in dirs if not d.startswith(".")]
|
|
114
116
|
for filename in files:
|
|
115
117
|
full_path = Path(root) / filename
|
|
116
118
|
rel_path = str(full_path.relative_to(repo.current_dir))
|
|
117
119
|
working_files[rel_path] = full_path.read_bytes()
|
|
118
|
-
|
|
120
|
+
|
|
119
121
|
tree_diff = engine.diff_working_dir(commit_hash, working_files)
|
|
120
|
-
|
|
122
|
+
|
|
121
123
|
if args.stat:
|
|
122
124
|
print(f" {tree_diff.added_count} file(s) added")
|
|
123
125
|
print(f" {tree_diff.deleted_count} file(s) deleted")
|
|
124
126
|
print(f" {tree_diff.modified_count} file(s) modified")
|
|
125
127
|
else:
|
|
126
|
-
print(engine.format_diff(tree_diff, args.ref1,
|
|
127
|
-
|
|
128
|
+
print(engine.format_diff(tree_diff, args.ref1, "working"))
|
|
129
|
+
|
|
128
130
|
return 0
|
|
129
|
-
|
|
131
|
+
|
|
130
132
|
# Default: diff working tree against HEAD
|
|
131
133
|
head_commit = repo.get_head_commit()
|
|
132
134
|
if not head_commit:
|
|
133
135
|
print("No commits yet. Nothing to diff.")
|
|
134
136
|
return 0
|
|
135
|
-
|
|
137
|
+
|
|
136
138
|
# Get working directory files
|
|
137
139
|
working_files = {}
|
|
138
140
|
for root, dirs, files in os.walk(repo.current_dir):
|
|
139
|
-
dirs[:] = [d for d in dirs if not d.startswith(
|
|
141
|
+
dirs[:] = [d for d in dirs if not d.startswith(".")]
|
|
140
142
|
for filename in files:
|
|
141
143
|
full_path = Path(root) / filename
|
|
142
144
|
rel_path = str(full_path.relative_to(repo.current_dir))
|
|
143
145
|
working_files[rel_path] = full_path.read_bytes()
|
|
144
|
-
|
|
145
|
-
tree_diff = engine.diff_working_dir(
|
|
146
|
-
|
|
147
|
-
working_files
|
|
148
|
-
)
|
|
149
|
-
|
|
146
|
+
|
|
147
|
+
tree_diff = engine.diff_working_dir(head_commit.store(repo.object_store), working_files)
|
|
148
|
+
|
|
150
149
|
if args.stat:
|
|
151
150
|
print(f" {tree_diff.added_count} file(s) added")
|
|
152
151
|
print(f" {tree_diff.deleted_count} file(s) deleted")
|
|
153
152
|
print(f" {tree_diff.modified_count} file(s) modified")
|
|
154
153
|
else:
|
|
155
|
-
print(engine.format_diff(tree_diff,
|
|
156
|
-
|
|
154
|
+
print(engine.format_diff(tree_diff, "HEAD", "working"))
|
|
155
|
+
|
|
157
156
|
return 0
|
|
@@ -0,0 +1,74 @@
|
|
|
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
|
+
|
|
45
|
+
@staticmethod
|
|
46
|
+
def execute(args) -> int:
|
|
47
|
+
repo, code = require_repo()
|
|
48
|
+
if code != 0:
|
|
49
|
+
return code
|
|
50
|
+
|
|
51
|
+
config = DistillerConfig(
|
|
52
|
+
source_dir=args.source,
|
|
53
|
+
target_dir=args.target,
|
|
54
|
+
create_safety_branch=not args.no_branch,
|
|
55
|
+
)
|
|
56
|
+
distiller = Distiller(repo, config)
|
|
57
|
+
|
|
58
|
+
result = distiller.run(
|
|
59
|
+
source=args.source,
|
|
60
|
+
target=args.target,
|
|
61
|
+
model=args.model,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
print(f"Distiller completed:")
|
|
65
|
+
print(f" Clusters processed: {result.clusters_processed}")
|
|
66
|
+
print(f" Facts extracted: {result.facts_extracted}")
|
|
67
|
+
print(f" Episodes archived: {result.episodes_archived}")
|
|
68
|
+
if result.branch_created:
|
|
69
|
+
print(f" Branch created: {result.branch_created}")
|
|
70
|
+
if result.commit_hash:
|
|
71
|
+
print(f" Commit: {result.commit_hash[:8]}")
|
|
72
|
+
print(f"\n{result.message}")
|
|
73
|
+
|
|
74
|
+
return 0 if result.success else 1
|