agmem 0.1.2__py3-none-any.whl → 0.1.4__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 (48) hide show
  1. {agmem-0.1.2.dist-info → agmem-0.1.4.dist-info}/METADATA +144 -14
  2. {agmem-0.1.2.dist-info → agmem-0.1.4.dist-info}/RECORD +48 -28
  3. memvcs/cli.py +10 -0
  4. memvcs/commands/add.py +6 -0
  5. memvcs/commands/audit.py +59 -0
  6. memvcs/commands/clone.py +7 -0
  7. memvcs/commands/daemon.py +45 -0
  8. memvcs/commands/distill.py +24 -0
  9. memvcs/commands/federated.py +59 -0
  10. memvcs/commands/fsck.py +31 -0
  11. memvcs/commands/garden.py +22 -0
  12. memvcs/commands/gc.py +66 -0
  13. memvcs/commands/merge.py +55 -1
  14. memvcs/commands/prove.py +66 -0
  15. memvcs/commands/pull.py +27 -0
  16. memvcs/commands/resolve.py +130 -0
  17. memvcs/commands/timeline.py +27 -0
  18. memvcs/commands/verify.py +74 -23
  19. memvcs/commands/when.py +27 -0
  20. memvcs/core/audit.py +124 -0
  21. memvcs/core/compression_pipeline.py +157 -0
  22. memvcs/core/consistency.py +9 -9
  23. memvcs/core/crypto_verify.py +291 -0
  24. memvcs/core/distiller.py +47 -29
  25. memvcs/core/encryption.py +169 -0
  26. memvcs/core/federated.py +147 -0
  27. memvcs/core/gardener.py +47 -29
  28. memvcs/core/ipfs_remote.py +200 -0
  29. memvcs/core/knowledge_graph.py +77 -5
  30. memvcs/core/llm/__init__.py +10 -0
  31. memvcs/core/llm/anthropic_provider.py +50 -0
  32. memvcs/core/llm/base.py +27 -0
  33. memvcs/core/llm/factory.py +30 -0
  34. memvcs/core/llm/openai_provider.py +36 -0
  35. memvcs/core/merge.py +36 -23
  36. memvcs/core/objects.py +39 -19
  37. memvcs/core/pack.py +278 -0
  38. memvcs/core/privacy_budget.py +63 -0
  39. memvcs/core/remote.py +229 -3
  40. memvcs/core/repository.py +82 -2
  41. memvcs/core/temporal_index.py +9 -0
  42. memvcs/core/trust.py +103 -0
  43. memvcs/core/vector_store.py +15 -1
  44. memvcs/core/zk_proofs.py +158 -0
  45. {agmem-0.1.2.dist-info → agmem-0.1.4.dist-info}/WHEEL +0 -0
  46. {agmem-0.1.2.dist-info → agmem-0.1.4.dist-info}/entry_points.txt +0 -0
  47. {agmem-0.1.2.dist-info → agmem-0.1.4.dist-info}/licenses/LICENSE +0 -0
  48. {agmem-0.1.2.dist-info → agmem-0.1.4.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,59 @@
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(
42
+ repo.root,
43
+ cfg["memory_types"],
44
+ use_dp=cfg.get("use_dp", False),
45
+ dp_epsilon=cfg.get("dp_epsilon") or 0.1,
46
+ dp_delta=cfg.get("dp_delta") or 1e-5,
47
+ )
48
+ msg = push_updates(repo.root, summary)
49
+ print(msg)
50
+ return 0 if "Pushed" in msg else 1
51
+ else:
52
+ data = pull_merged(repo.root)
53
+ if data is None:
54
+ print("Pull failed or coordinator unavailable.")
55
+ return 1
56
+ print("Merged summary from coordinator:")
57
+ for k, v in (data or {}).items():
58
+ print(f" {k}: {v}")
59
+ return 0
memvcs/commands/fsck.py CHANGED
@@ -66,6 +66,10 @@ class FsckCommand:
66
66
  issues_found += ref_issues
67
67
  issues_fixed += ref_fixed
68
68
 
69
+ # Cryptographic verification (Merkle + signature)
70
+ crypto_issues = FsckCommand._check_crypto(repo, args.verbose)
71
+ issues_found += crypto_issues
72
+
69
73
  # Print summary
70
74
  print()
71
75
  print("=" * 40)
@@ -195,3 +199,30 @@ class FsckCommand:
195
199
  print(f" Found {issues} ref issues")
196
200
 
197
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
@@ -38,6 +38,11 @@ class GardenCommand:
38
38
  help="LLM provider for summarization (default: none)",
39
39
  )
40
40
  parser.add_argument("--model", help="LLM model to use (e.g., gpt-3.5-turbo)")
41
+ parser.add_argument(
42
+ "--private",
43
+ action="store_true",
44
+ help="Use differential privacy (spend epsilon from budget)",
45
+ )
41
46
 
42
47
  @staticmethod
43
48
  def execute(args) -> int:
@@ -45,12 +50,29 @@ class GardenCommand:
45
50
  if code != 0:
46
51
  return code
47
52
 
53
+ use_dp = getattr(args, "private", False)
54
+ dp_epsilon = None
55
+ dp_delta = None
56
+ if use_dp:
57
+ from ..core.privacy_budget import load_budget, spend_epsilon
58
+
59
+ spent, max_eps, delta = 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
+ dp_epsilon = 0.05
65
+ dp_delta = delta
66
+
48
67
  # Build config
49
68
  config = GardenerConfig(
50
69
  threshold=args.threshold,
51
70
  auto_commit=not args.no_commit,
52
71
  llm_provider=args.llm if args.llm != "none" else None,
53
72
  llm_model=args.model,
73
+ use_dp=use_dp,
74
+ dp_epsilon=dp_epsilon,
75
+ dp_delta=dp_delta,
54
76
  )
55
77
 
56
78
  # Create gardener
memvcs/commands/gc.py ADDED
@@ -0,0 +1,66 @@
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, run_repack
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
+ parser.add_argument(
34
+ "--repack",
35
+ action="store_true",
36
+ help="After GC, pack reachable loose objects into a pack file",
37
+ )
38
+
39
+ @staticmethod
40
+ def execute(args) -> int:
41
+ repo, code = require_repo()
42
+ if code != 0:
43
+ return code
44
+
45
+ gc_prune_days = getattr(args, "prune_days", 90)
46
+ deleted, freed = run_gc(
47
+ repo.mem_dir,
48
+ repo.object_store,
49
+ gc_prune_days=gc_prune_days,
50
+ dry_run=args.dry_run,
51
+ )
52
+ if args.dry_run:
53
+ print(f"Would remove {deleted} unreachable object(s) ({freed} bytes).")
54
+ else:
55
+ print(f"Removed {deleted} unreachable object(s) ({freed} bytes reclaimed).")
56
+
57
+ if getattr(args, "repack", False) and not args.dry_run:
58
+ packed, repack_freed = run_repack(
59
+ repo.mem_dir,
60
+ repo.object_store,
61
+ gc_prune_days=gc_prune_days,
62
+ dry_run=False,
63
+ )
64
+ if packed > 0:
65
+ print(f"Packed {packed} object(s) into pack file ({repack_freed} bytes from loose).")
66
+ return 0
memvcs/commands/merge.py CHANGED
@@ -23,6 +23,11 @@ class MergeCommand:
23
23
  "--no-commit", action="store_true", help="Perform merge but do not commit"
24
24
  )
25
25
  parser.add_argument("--abort", action="store_true", help="Abort the current merge")
26
+ parser.add_argument(
27
+ "--yes",
28
+ action="store_true",
29
+ help="Accept conditionally trusted branch commits without prompting",
30
+ )
26
31
 
27
32
  @staticmethod
28
33
  def execute(args) -> int:
@@ -53,6 +58,24 @@ class MergeCommand:
53
58
  print(f"Error: Branch '{args.branch}' not found.")
54
59
  return 1
55
60
 
61
+ # Trust check for branch tip (may be from another agent)
62
+ other_commit_hash = repo.refs.get_branch_commit(args.branch)
63
+ if other_commit_hash:
64
+ from ..core.objects import Commit
65
+ from ..core.trust import find_verifying_key, get_trust_level
66
+
67
+ other_commit = Commit.load(repo.object_store, other_commit_hash)
68
+ if other_commit and other_commit.metadata:
69
+ key_pem = find_verifying_key(repo.mem_dir, other_commit.metadata)
70
+ if key_pem is not None:
71
+ level = get_trust_level(repo.mem_dir, key_pem)
72
+ if level == "untrusted":
73
+ print(f"Merge blocked: branch '{args.branch}' signed by untrusted key.")
74
+ return 1
75
+ if level == "conditional" and not getattr(args, "yes", False):
76
+ print("Branch signed by conditionally trusted key. Use --yes to merge.")
77
+ return 1
78
+
56
79
  # Perform merge
57
80
  engine = MergeEngine(repo)
58
81
  result = engine.merge(args.branch, message=args.message)
@@ -61,16 +84,47 @@ class MergeCommand:
61
84
  print(f"Merge successful: {result.message}")
62
85
  if result.commit_hash:
63
86
  print(f" Commit: {result.commit_hash[:8]}")
87
+ try:
88
+ from ..core.audit import append_audit
89
+
90
+ append_audit(
91
+ repo.mem_dir, "merge", {"branch": args.branch, "commit": result.commit_hash}
92
+ )
93
+ except Exception:
94
+ pass
64
95
  return 0
65
96
  else:
66
97
  print(f"Merge failed: {result.message}")
67
98
 
68
99
  if result.conflicts:
100
+ # Persist conflicts for agmem resolve
101
+ try:
102
+ import json
103
+
104
+ merge_dir = repo.mem_dir / "merge"
105
+ merge_dir.mkdir(parents=True, exist_ok=True)
106
+ conflicts_data = [
107
+ {
108
+ "path": c.path,
109
+ "message": c.message,
110
+ "memory_type": getattr(c, "memory_type", None),
111
+ "payload": getattr(c, "payload", None),
112
+ "ours_content": c.ours_content,
113
+ "theirs_content": c.theirs_content,
114
+ "base_content": c.base_content,
115
+ }
116
+ for c in result.conflicts
117
+ ]
118
+ (merge_dir / "conflicts.json").write_text(json.dumps(conflicts_data, indent=2))
119
+ except Exception:
120
+ pass
69
121
  print()
70
122
  print("Conflicts:")
71
123
  for conflict in result.conflicts:
72
124
  print(f" {conflict.path}")
73
125
  print()
74
- print("Resolve conflicts and run 'agmem commit' to complete the merge.")
126
+ print(
127
+ "Resolve conflicts with 'agmem resolve' or edit files and run 'agmem commit'."
128
+ )
75
129
 
76
130
  return 1
@@ -0,0 +1,66 @@
1
+ """
2
+ agmem prove - Generate zero-knowledge proofs (stub).
3
+
4
+ Prove properties of memory (keyword, freshness) without revealing content.
5
+ """
6
+
7
+ import argparse
8
+ from pathlib import Path
9
+
10
+ from ..commands.base import require_repo
11
+ from ..core.zk_proofs import prove_keyword_containment, prove_memory_freshness
12
+
13
+
14
+ class ProveCommand:
15
+ """Generate zk proofs for memory properties."""
16
+
17
+ name = "prove"
18
+ help = "Prove a property of memory without revealing content (zk stub)"
19
+
20
+ @staticmethod
21
+ def add_arguments(parser: argparse.ArgumentParser):
22
+ parser.add_argument(
23
+ "--memory", "-m", required=True, help="Memory file path (under current/)"
24
+ )
25
+ parser.add_argument(
26
+ "--property",
27
+ "-p",
28
+ choices=["keyword", "freshness"],
29
+ required=True,
30
+ help="Property to prove",
31
+ )
32
+ parser.add_argument("--value", "-v", help="Value (e.g. keyword or ISO date)")
33
+ parser.add_argument("--output", "-o", help="Output proof file path")
34
+
35
+ @staticmethod
36
+ def execute(args) -> int:
37
+ repo, code = require_repo()
38
+ if code != 0:
39
+ return code
40
+
41
+ path = repo.current_dir / args.memory
42
+ if not path.exists():
43
+ print(f"Memory file not found: {args.memory}")
44
+ return 1
45
+
46
+ out = args.output or "proof.bin"
47
+ out_path = Path(out)
48
+ if not out_path.is_absolute():
49
+ out_path = repo.root / out_path
50
+
51
+ if args.property == "keyword":
52
+ if not args.value:
53
+ print("--value required for keyword (the keyword)")
54
+ return 1
55
+ ok = prove_keyword_containment(path, args.value, out_path)
56
+ else:
57
+ if not args.value:
58
+ print("--value required for freshness (ISO date)")
59
+ return 1
60
+ ok = prove_memory_freshness(path, args.value, out_path, mem_dir=repo.mem_dir)
61
+
62
+ if not ok:
63
+ print("Proof generation failed (keyword not in file, or signing key not set for freshness).")
64
+ return 1
65
+ print(f"Proof written to {out_path}")
66
+ return 0
memvcs/commands/pull.py CHANGED
@@ -25,6 +25,11 @@ class PullCommand:
25
25
  nargs="?",
26
26
  help="Branch to pull (default: all)",
27
27
  )
28
+ parser.add_argument(
29
+ "--yes",
30
+ action="store_true",
31
+ help="Accept conditionally trusted remote commits without prompting",
32
+ )
28
33
 
29
34
  @staticmethod
30
35
  def execute(args) -> int:
@@ -51,6 +56,28 @@ class PullCommand:
51
56
  remote_ref = f"{args.remote}/{current_branch}"
52
57
  remote_hash = repo.resolve_ref(remote_ref)
53
58
  if remote_hash:
59
+ from memvcs.core.crypto_verify import verify_commit_optional
60
+
61
+ verify_commit_optional(
62
+ repo.object_store, remote_hash, mem_dir=repo.mem_dir, strict=False
63
+ )
64
+ # Trust check: block or require confirmation for untrusted/conditional
65
+ from memvcs.core.objects import Commit
66
+ from memvcs.core.trust import find_verifying_key, get_trust_level
67
+
68
+ remote_commit = Commit.load(repo.object_store, remote_hash)
69
+ if remote_commit and remote_commit.metadata:
70
+ key_pem = find_verifying_key(repo.mem_dir, remote_commit.metadata)
71
+ if key_pem is not None:
72
+ level = get_trust_level(repo.mem_dir, key_pem)
73
+ if level == "untrusted":
74
+ print(f"Pull blocked: remote commit signed by untrusted key.")
75
+ return 1
76
+ if level == "conditional" and not getattr(args, "yes", False):
77
+ print(
78
+ "Remote commit from conditionally trusted key. Use --yes to merge."
79
+ )
80
+ return 1
54
81
  from memvcs.core.merge import MergeEngine
55
82
 
56
83
  merge_engine = MergeEngine(repo)
@@ -0,0 +1,130 @@
1
+ """
2
+ agmem resolve - Structured conflict resolution.
3
+
4
+ Resolve merge conflicts with ours/theirs/both choices; record in audit.
5
+ """
6
+
7
+ import argparse
8
+ import json
9
+ from pathlib import Path
10
+ from typing import Optional
11
+
12
+ from ..commands.base import require_repo
13
+
14
+
15
+ def _path_under_current(path_str: str, current_dir: Path) -> Optional[Path]:
16
+ """Resolve path under current_dir; return None if it escapes (path traversal)."""
17
+ try:
18
+ full = (current_dir / path_str).resolve()
19
+ full.relative_to(current_dir.resolve())
20
+ return full
21
+ except (ValueError, RuntimeError):
22
+ return None
23
+
24
+
25
+ def _resolved_content(choice: str, ours: str, theirs: str) -> str:
26
+ """Return content for choice: ours, theirs, or both (merged)."""
27
+ if choice == "ours":
28
+ return ours
29
+ if choice == "theirs":
30
+ return theirs
31
+ return ours.rstrip() + "\n\n--- merged ---\n\n" + theirs
32
+
33
+
34
+ class ResolveCommand:
35
+ """Resolve merge conflicts interactively (ours/theirs/both)."""
36
+
37
+ name = "resolve"
38
+ help = "Resolve merge conflicts (ours / theirs / both)"
39
+
40
+ @staticmethod
41
+ def add_arguments(parser: argparse.ArgumentParser):
42
+ parser.add_argument(
43
+ "path",
44
+ nargs="?",
45
+ help="Conflict path to resolve (or resolve all if omitted)",
46
+ )
47
+ parser.add_argument(
48
+ "--ours",
49
+ action="store_true",
50
+ help="Resolve with our version",
51
+ )
52
+ parser.add_argument(
53
+ "--theirs",
54
+ action="store_true",
55
+ help="Resolve with their version",
56
+ )
57
+ parser.add_argument(
58
+ "--both",
59
+ action="store_true",
60
+ help="Keep both (append theirs after ours with separator)",
61
+ )
62
+
63
+ @staticmethod
64
+ def execute(args) -> int:
65
+ repo, code = require_repo()
66
+ if code != 0:
67
+ return code
68
+
69
+ merge_dir = repo.mem_dir / "merge"
70
+ conflicts_file = merge_dir / "conflicts.json"
71
+ if not conflicts_file.exists():
72
+ print("No unresolved conflicts.")
73
+ return 0
74
+
75
+ try:
76
+ conflicts = json.loads(conflicts_file.read_text())
77
+ except Exception:
78
+ print("Could not read conflicts file.")
79
+ return 1
80
+
81
+ if not conflicts:
82
+ print("No unresolved conflicts.")
83
+ conflicts_file.unlink(missing_ok=True)
84
+ return 0
85
+
86
+ choice = None
87
+ if args.ours:
88
+ choice = "ours"
89
+ elif args.theirs:
90
+ choice = "theirs"
91
+ elif args.both:
92
+ choice = "both"
93
+
94
+ resolved = 0
95
+ remaining = []
96
+ for c in conflicts:
97
+ path = c.get("path", "")
98
+ if args.path and path != args.path:
99
+ remaining.append(c)
100
+ continue
101
+ if choice is None:
102
+ print(f"Conflict: {path}")
103
+ print(" Use: agmem resolve <path> --ours | --theirs | --both")
104
+ remaining.append(c)
105
+ continue
106
+ ours_content = c.get("ours_content") or ""
107
+ theirs_content = c.get("theirs_content") or ""
108
+ full_path = _path_under_current(path, repo.current_dir)
109
+ if full_path is None:
110
+ print(f"Error: Conflict path escapes repository: {path}")
111
+ remaining.append(c)
112
+ continue
113
+ content = _resolved_content(choice, ours_content, theirs_content)
114
+ full_path.parent.mkdir(parents=True, exist_ok=True)
115
+ full_path.write_text(content, encoding="utf-8")
116
+ resolved += 1
117
+ try:
118
+ from ..core.audit import append_audit
119
+
120
+ append_audit(repo.mem_dir, "resolve", {"path": path, "choice": choice})
121
+ except Exception:
122
+ pass
123
+
124
+ if remaining:
125
+ conflicts_file.write_text(json.dumps(remaining, indent=2))
126
+ else:
127
+ conflicts_file.unlink(missing_ok=True)
128
+ if resolved:
129
+ print(f"Resolved {resolved} conflict(s). Stage and commit to complete.")
130
+ return 0
@@ -28,6 +28,18 @@ class TimelineCommand:
28
28
  default=20,
29
29
  help="Max commits to show (default: 20)",
30
30
  )
31
+ parser.add_argument(
32
+ "--from",
33
+ dest="from_ts",
34
+ metavar="ISO",
35
+ help="Start of time range (ISO 8601, e.g. 2025-01-01)",
36
+ )
37
+ parser.add_argument(
38
+ "--to",
39
+ dest="to_ts",
40
+ metavar="ISO",
41
+ help="End of time range (ISO 8601)",
42
+ )
31
43
 
32
44
  @staticmethod
33
45
  def execute(args) -> int:
@@ -36,6 +48,17 @@ class TimelineCommand:
36
48
  return code
37
49
 
38
50
  filepath = args.file.replace("current/", "").lstrip("/")
51
+ from_ts = getattr(args, "from_ts", None)
52
+ to_ts = getattr(args, "to_ts", None)
53
+ commits_in_range = None
54
+ if from_ts and to_ts:
55
+ try:
56
+ from ..core.temporal_index import TemporalIndex
57
+ ti = TemporalIndex(repo.mem_dir, repo.object_store)
58
+ range_entries = ti.range_query(from_ts, to_ts)
59
+ commits_in_range = {ch for _, ch in range_entries}
60
+ except Exception:
61
+ pass
39
62
 
40
63
  # Walk commit history
41
64
  head = repo.refs.get_head()
@@ -51,6 +74,10 @@ class TimelineCommand:
51
74
  if commit_hash in seen:
52
75
  break
53
76
  seen.add(commit_hash)
77
+ if commits_in_range is not None and commit_hash not in commits_in_range:
78
+ commit = Commit.load(repo.object_store, commit_hash)
79
+ commit_hash = commit.parents[0] if commit and commit.parents else None
80
+ continue
54
81
 
55
82
  commit = Commit.load(repo.object_store, commit_hash)
56
83
  if not commit: