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,280 @@
1
+ """
2
+ Cryptographic commit verification for agmem.
3
+
4
+ Merkle tree over commit blobs, optional Ed25519 signing of Merkle root.
5
+ Verification on checkout, pull, and via verify/fsck.
6
+ """
7
+
8
+ import hashlib
9
+ import hmac
10
+ import json
11
+ import os
12
+ from pathlib import Path
13
+ from typing import Optional, List, Tuple, Any, Dict
14
+
15
+ from .objects import ObjectStore, Tree, Commit
16
+
17
+ # Ed25519 via cryptography (optional)
18
+ try:
19
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import (
20
+ Ed25519PrivateKey,
21
+ Ed25519PublicKey,
22
+ )
23
+ from cryptography.exceptions import InvalidSignature
24
+ from cryptography.hazmat.primitives import serialization
25
+
26
+ ED25519_AVAILABLE = True
27
+ except ImportError:
28
+ ED25519_AVAILABLE = False
29
+
30
+
31
+ def _collect_blob_hashes_from_tree(store: ObjectStore, tree_hash: str) -> List[str]:
32
+ """Recursively collect all blob hashes from a tree. Returns sorted list for deterministic Merkle."""
33
+ tree = Tree.load(store, tree_hash)
34
+ if not tree:
35
+ return []
36
+ blobs: List[str] = []
37
+ for entry in tree.entries:
38
+ if entry.obj_type == "blob":
39
+ blobs.append(entry.hash)
40
+ elif entry.obj_type == "tree":
41
+ blobs.extend(_collect_blob_hashes_from_tree(store, entry.hash))
42
+ return sorted(blobs)
43
+
44
+
45
+ def _merkle_hash(data: bytes) -> str:
46
+ """SHA-256 hash for Merkle tree nodes."""
47
+ return hashlib.sha256(data).hexdigest()
48
+
49
+
50
+ def build_merkle_tree(blob_hashes: List[str]) -> str:
51
+ """
52
+ Build balanced binary Merkle tree from blob hashes.
53
+ Leaves are hashes of blob hashes (as hex strings); internal nodes hash(left_hex || right_hex).
54
+ Returns root hash (hex).
55
+ """
56
+ if not blob_hashes:
57
+ return _merkle_hash(b"empty")
58
+ # Leaves: hash each blob hash string to fixed-size leaf
59
+ layer = [_merkle_hash(h.encode()) for h in blob_hashes]
60
+ while len(layer) > 1:
61
+ next_layer = []
62
+ for i in range(0, len(layer), 2):
63
+ left = layer[i]
64
+ right = layer[i + 1] if i + 1 < len(layer) else layer[i]
65
+ combined = (left + right).encode()
66
+ next_layer.append(_merkle_hash(combined))
67
+ layer = next_layer
68
+ return layer[0]
69
+
70
+
71
+ def build_merkle_root_for_commit(store: ObjectStore, commit_hash: str) -> Optional[str]:
72
+ """Build Merkle root for a commit's tree. Returns None if commit/tree missing."""
73
+ commit = Commit.load(store, commit_hash)
74
+ if not commit:
75
+ return None
76
+ blobs = _collect_blob_hashes_from_tree(store, commit.tree)
77
+ return build_merkle_tree(blobs)
78
+
79
+
80
+ def merkle_proof(blob_hashes: List[str], target_blob_hash: str) -> Optional[List[Tuple[str, str]]]:
81
+ """
82
+ Produce Merkle proof for a blob: list of (sibling_hash, "L"|"R") from leaf to root.
83
+ Returns None if target not in list.
84
+ """
85
+ if target_blob_hash not in blob_hashes:
86
+ return None
87
+ layer = [_merkle_hash(h.encode()) for h in sorted(blob_hashes)]
88
+ leaf_index = sorted(blob_hashes).index(target_blob_hash)
89
+ proof: List[Tuple[str, str]] = []
90
+ idx = leaf_index
91
+ while len(layer) > 1:
92
+ next_layer = []
93
+ for i in range(0, len(layer), 2):
94
+ left = layer[i]
95
+ right = layer[i + 1] if i + 1 < len(layer) else layer[i]
96
+ combined = (left + right).encode()
97
+ parent = _merkle_hash(combined)
98
+ next_layer.append(parent)
99
+ # If current idx is in this pair, record sibling and advance index
100
+ pair_idx = i // 2
101
+ if idx == i:
102
+ proof.append((right, "R"))
103
+ idx = pair_idx
104
+ elif idx == i + 1:
105
+ proof.append((left, "L"))
106
+ idx = pair_idx
107
+ layer = next_layer
108
+ return proof if proof else []
109
+
110
+
111
+ def verify_merkle_proof(blob_hash: str, proof: List[Tuple[str, str]], expected_root: str) -> bool:
112
+ """Verify a Merkle proof for a blob against expected root."""
113
+ current = _merkle_hash(blob_hash.encode())
114
+ for sibling, side in proof:
115
+ if side == "L":
116
+ current = _merkle_hash((sibling + current).encode())
117
+ else:
118
+ current = _merkle_hash((current + sibling).encode())
119
+ return current == expected_root
120
+
121
+
122
+ # --- Signing (Ed25519) ---
123
+
124
+
125
+ def _keys_dir(mem_dir: Path) -> Path:
126
+ return mem_dir / "keys"
127
+
128
+
129
+ def get_signing_key_paths(mem_dir: Path) -> Tuple[Path, Path]:
130
+ """Return (private_key_path, public_key_path). Private may not exist (env-only)."""
131
+ kd = _keys_dir(mem_dir)
132
+ return (kd / "private.pem", kd / "public.pem")
133
+
134
+
135
+ def ensure_keys_dir(mem_dir: Path) -> Path:
136
+ """Ensure .mem/keys exists; return keys dir."""
137
+ kd = _keys_dir(mem_dir)
138
+ kd.mkdir(parents=True, exist_ok=True)
139
+ return kd
140
+
141
+
142
+ def generate_keypair(mem_dir: Path) -> Tuple[bytes, bytes]:
143
+ """Generate Ed25519 keypair. Returns (private_pem, public_pem). Requires cryptography."""
144
+ if not ED25519_AVAILABLE:
145
+ raise RuntimeError(
146
+ "Signing requires 'cryptography'; install with: pip install cryptography"
147
+ )
148
+ private_key = Ed25519PrivateKey.generate()
149
+ public_key = private_key.public_key()
150
+ private_pem = private_key.private_bytes(
151
+ encoding=serialization.Encoding.PEM,
152
+ format=serialization.PrivateFormat.PKCS8,
153
+ encryption_algorithm=serialization.NoEncryption(),
154
+ )
155
+ public_pem = public_key.public_bytes(
156
+ encoding=serialization.Encoding.PEM,
157
+ format=serialization.PublicFormat.SubjectPublicKeyInfo,
158
+ )
159
+ return (private_pem, public_pem)
160
+
161
+
162
+ def save_public_key(mem_dir: Path, public_pem: bytes) -> Path:
163
+ """Save public key to .mem/keys/public.pem. Returns path."""
164
+ ensure_keys_dir(mem_dir)
165
+ path = _keys_dir(mem_dir) / "public.pem"
166
+ path.write_bytes(public_pem)
167
+ return path
168
+
169
+
170
+ def load_public_key(mem_dir: Path) -> Optional[bytes]:
171
+ """Load public key PEM from .mem/keys/public.pem or config. Returns None if not found."""
172
+ path = _keys_dir(mem_dir) / "public.pem"
173
+ if path.exists():
174
+ return path.read_bytes()
175
+ config_file = mem_dir / "config.json"
176
+ if config_file.exists():
177
+ try:
178
+ config = json.loads(config_file.read_text())
179
+ return config.get("signing", {}).get("public_key_pem")
180
+ except Exception:
181
+ pass
182
+ return None
183
+
184
+
185
+ def load_private_key_from_env() -> Optional[bytes]:
186
+ """Load private key PEM from env AGMEM_SIGNING_PRIVATE_KEY (or path in AGMEM_SIGNING_PRIVATE_KEY_FILE)."""
187
+ pem = os.environ.get("AGMEM_SIGNING_PRIVATE_KEY")
188
+ if pem:
189
+ return pem.encode() if isinstance(pem, str) else pem
190
+ path = os.environ.get("AGMEM_SIGNING_PRIVATE_KEY_FILE")
191
+ if path and os.path.isfile(path):
192
+ return Path(path).read_bytes()
193
+ return None
194
+
195
+
196
+ def sign_merkle_root(root_hex: str, private_key_pem: bytes) -> str:
197
+ """Sign Merkle root (hex string). Returns signature as hex."""
198
+ if not ED25519_AVAILABLE:
199
+ raise RuntimeError("Signing requires 'cryptography'")
200
+ key = serialization.load_pem_private_key(private_key_pem, password=None)
201
+ if not isinstance(key, Ed25519PrivateKey):
202
+ raise TypeError("Ed25519 private key required")
203
+ sig = key.sign(root_hex.encode())
204
+ return sig.hex()
205
+
206
+
207
+ def verify_signature(root_hex: str, signature_hex: str, public_key_pem: bytes) -> bool:
208
+ """Verify signature of Merkle root. Returns True if valid."""
209
+ if not ED25519_AVAILABLE:
210
+ return False
211
+ try:
212
+ key = serialization.load_pem_public_key(public_key_pem)
213
+ if not isinstance(key, Ed25519PublicKey):
214
+ return False
215
+ key.verify(bytes.fromhex(signature_hex), root_hex.encode())
216
+ return True
217
+ except InvalidSignature:
218
+ return False
219
+ except Exception:
220
+ return False
221
+
222
+
223
+ def verify_commit(
224
+ store: ObjectStore,
225
+ commit_hash: str,
226
+ public_key_pem: Optional[bytes] = None,
227
+ *,
228
+ mem_dir: Optional[Path] = None,
229
+ ) -> Tuple[bool, Optional[str]]:
230
+ """
231
+ Verify commit: rebuild Merkle tree from blobs, compare root to stored, verify signature.
232
+ Returns (verified, error_message). verified=True means OK; False + message means tampered or unverified.
233
+ If public_key_pem is None and mem_dir is set, load from mem_dir.
234
+ """
235
+ commit = Commit.load(store, commit_hash)
236
+ if not commit:
237
+ return (False, "commit not found")
238
+ stored_root = (commit.metadata or {}).get("merkle_root")
239
+ stored_sig = (commit.metadata or {}).get("signature")
240
+ if not stored_root:
241
+ return (False, "commit has no merkle_root (unverified)")
242
+ computed_root = build_merkle_root_for_commit(store, commit_hash)
243
+ if not computed_root:
244
+ return (False, "could not build Merkle tree (missing tree/blobs)")
245
+ if not hmac.compare_digest(computed_root, stored_root):
246
+ return (False, "merkle_root mismatch (commit tampered)")
247
+ if not stored_sig:
248
+ return (True, None) # Root matches; no signature (legacy)
249
+ pub = public_key_pem
250
+ if not pub and mem_dir:
251
+ pub = load_public_key(mem_dir)
252
+ if not pub:
253
+ return (False, "signature present but no public key configured")
254
+ if isinstance(pub, str):
255
+ pub = pub.encode()
256
+ if not verify_signature(stored_root, stored_sig, pub):
257
+ return (False, "signature verification failed")
258
+ return (True, None)
259
+
260
+
261
+ def verify_commit_optional(
262
+ store: ObjectStore,
263
+ commit_hash: str,
264
+ mem_dir: Optional[Path] = None,
265
+ *,
266
+ strict: bool = False,
267
+ ) -> None:
268
+ """
269
+ Verify commit; if strict=True raise on failure. If strict=False, only raise on tamper (root mismatch).
270
+ Unverified (no merkle_root) is OK when not strict.
271
+ """
272
+ ok, err = verify_commit(store, commit_hash, None, mem_dir=mem_dir)
273
+ if ok:
274
+ return
275
+ if not err:
276
+ return
277
+ if "tampered" in err or "mismatch" in err or "signature verification failed" in err:
278
+ raise ValueError(f"Commit verification failed: {err}")
279
+ if strict:
280
+ raise ValueError(f"Commit verification failed: {err}")
memvcs/core/decay.py ADDED
@@ -0,0 +1,185 @@
1
+ """
2
+ Decay engine - memory decay and forgetting for agmem.
3
+
4
+ Mimics human forgetting: irrelevant details fade, important ones strengthen.
5
+ Ebbinghaus-inspired time decay + retrieval-induced enhancement.
6
+ """
7
+
8
+ import math
9
+ import shutil
10
+ from pathlib import Path
11
+ from typing import List, Dict, Any, Optional, Tuple
12
+ from dataclasses import dataclass, field
13
+ from datetime import datetime, timedelta
14
+
15
+ from .constants import MEMORY_TYPES
16
+ from .access_index import AccessIndex
17
+ from .objects import Commit
18
+ from .schema import FrontmatterParser
19
+
20
+
21
+ @dataclass
22
+ class DecayConfig:
23
+ """Configuration for decay engine."""
24
+
25
+ episodic_half_life_days: int = 30
26
+ semantic_min_importance: float = 0.3
27
+ access_count_threshold: int = 2
28
+ forgetting_dir: str = "forgetting"
29
+
30
+
31
+ @dataclass
32
+ class DecayCandidate:
33
+ """A memory candidate for decay (archiving)."""
34
+
35
+ path: str
36
+ memory_type: str
37
+ importance: float
38
+ last_access_days: Optional[float]
39
+ access_count: int
40
+ decay_score: float
41
+ reason: str
42
+
43
+
44
+ class DecayEngine:
45
+ """Computes decay scores and archives low-importance memories."""
46
+
47
+ def __init__(self, repo: Any, config: Optional[DecayConfig] = None):
48
+ self.repo = repo
49
+ self.config = config or DecayConfig()
50
+ self.access_index = AccessIndex(repo.mem_dir)
51
+ self.forgetting_dir = repo.mem_dir / self.config.forgetting_dir
52
+ self.current_dir = repo.current_dir
53
+
54
+ def _get_importance(self, path: str, content: str) -> float:
55
+ """Get importance from frontmatter or default."""
56
+ fm, _ = FrontmatterParser.parse(content)
57
+ if fm and fm.importance is not None:
58
+ return float(fm.importance)
59
+ if fm and fm.confidence_score is not None:
60
+ return float(fm.confidence_score)
61
+ return 0.5
62
+
63
+ def _get_access_info(self, path: str) -> Tuple[int, Optional[float]]:
64
+ """Get access count and days since last access."""
65
+ counts = self.access_index.get_access_counts_by_path()
66
+ count = counts.get(path, 0)
67
+ recent = self.access_index.get_recent_accesses(limit=1, path=path)
68
+ if not recent:
69
+ return count, None
70
+ ts_str = recent[0].get("timestamp", "")
71
+ if not ts_str:
72
+ return count, None
73
+ try:
74
+ if ts_str.endswith("Z"):
75
+ ts_str = ts_str[:-1] + "+00:00"
76
+ last = datetime.fromisoformat(ts_str)
77
+ days = (datetime.utcnow() - last.replace(tzinfo=None)).total_seconds() / 86400
78
+ return count, days
79
+ except Exception:
80
+ return count, None
81
+
82
+ def compute_decay_score(
83
+ self,
84
+ path: str,
85
+ content: str,
86
+ memory_type: str,
87
+ ) -> DecayCandidate:
88
+ """
89
+ Compute decay score for a memory.
90
+
91
+ Higher score = more likely to decay (archive).
92
+ Time decay: importance * 0.5^(days/half_life) when never accessed.
93
+ Retrieval-induced enhancement: access boosts strength (lower decay).
94
+ """
95
+ importance = self._get_importance(path, content)
96
+ access_count, last_access_days = self._get_access_info(path)
97
+
98
+ decay_score = 0.0
99
+ reason = ""
100
+
101
+ if "episodic" in memory_type.lower():
102
+ half_life = self.config.episodic_half_life_days
103
+ if last_access_days is not None:
104
+ decay_score = 1.0 - (importance * math.pow(0.5, last_access_days / half_life))
105
+ if access_count < self.config.access_count_threshold:
106
+ decay_score += 0.2
107
+ reason = f"episodic: {last_access_days:.0f}d since access, imp={importance:.2f}"
108
+ else:
109
+ decay_score = 0.5
110
+ reason = "episodic: never accessed"
111
+ else:
112
+ if importance < self.config.semantic_min_importance:
113
+ decay_score = 1.0 - importance
114
+ reason = f"semantic: low importance {importance:.2f}"
115
+ elif (
116
+ access_count < self.config.access_count_threshold
117
+ and last_access_days
118
+ and last_access_days > 60
119
+ ):
120
+ decay_score = 0.4
121
+ reason = "semantic: rarely accessed"
122
+
123
+ return DecayCandidate(
124
+ path=path,
125
+ memory_type=memory_type,
126
+ importance=importance,
127
+ last_access_days=last_access_days,
128
+ access_count=access_count,
129
+ decay_score=decay_score,
130
+ reason=reason,
131
+ )
132
+
133
+ def get_decay_candidates(self) -> List[DecayCandidate]:
134
+ """Get list of memories that would be archived (dry-run)."""
135
+ candidates = []
136
+ if not self.current_dir.exists():
137
+ return candidates
138
+
139
+ for subdir in MEMORY_TYPES:
140
+ dir_path = self.current_dir / subdir
141
+ if not dir_path.exists():
142
+ continue
143
+ for f in dir_path.rglob("*"):
144
+ if not f.is_file() or f.suffix.lower() not in (".md", ".txt"):
145
+ continue
146
+ try:
147
+ rel_path = str(f.relative_to(self.current_dir))
148
+ content = f.read_text(encoding="utf-8", errors="replace")
149
+ except Exception:
150
+ continue
151
+ cand = self.compute_decay_score(rel_path, content, subdir)
152
+ if cand.decay_score > 0.5:
153
+ candidates.append(cand)
154
+
155
+ candidates.sort(key=lambda x: x.decay_score, reverse=True)
156
+ return candidates
157
+
158
+ def apply_decay(self, candidates: Optional[List[DecayCandidate]] = None) -> int:
159
+ """
160
+ Archive low-importance memories to .mem/forgetting/.
161
+
162
+ Returns count of files archived.
163
+ """
164
+ if candidates is None:
165
+ candidates = self.get_decay_candidates()
166
+ self.forgetting_dir.mkdir(parents=True, exist_ok=True)
167
+ ts = datetime.utcnow().strftime("%Y%m%d-%H%M%S")
168
+ archive_sub = self.forgetting_dir / ts
169
+ archive_sub.mkdir(exist_ok=True)
170
+ count = 0
171
+ for cand in candidates:
172
+ if cand.decay_score <= 0.5:
173
+ continue
174
+ src = self.current_dir / cand.path
175
+ if not src.exists():
176
+ continue
177
+ try:
178
+ safe_name = cand.path.replace("/", "_").replace("..", "_")
179
+ dest = (archive_sub / safe_name).resolve()
180
+ dest.relative_to(self.forgetting_dir.resolve())
181
+ shutil.move(str(src), str(dest))
182
+ count += 1
183
+ except (ValueError, Exception):
184
+ continue
185
+ return count