memware 0.2.1__tar.gz → 0.2.2__tar.gz

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 (68) hide show
  1. {memware-0.2.1 → memware-0.2.2}/CHANGELOG.md +10 -0
  2. {memware-0.2.1 → memware-0.2.2}/PKG-INFO +1 -1
  3. {memware-0.2.1 → memware-0.2.2}/docs/backup.md +6 -3
  4. {memware-0.2.1 → memware-0.2.2}/src/memware/__init__.py +1 -1
  5. {memware-0.2.1 → memware-0.2.2}/src/memware/backup.py +23 -12
  6. {memware-0.2.1 → memware-0.2.2}/tests/test_backup.py +48 -8
  7. {memware-0.2.1 → memware-0.2.2}/.claude-plugin/marketplace.json +0 -0
  8. {memware-0.2.1 → memware-0.2.2}/.github/CODEOWNERS +0 -0
  9. {memware-0.2.1 → memware-0.2.2}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  10. {memware-0.2.1 → memware-0.2.2}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  11. {memware-0.2.1 → memware-0.2.2}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  12. {memware-0.2.1 → memware-0.2.2}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  13. {memware-0.2.1 → memware-0.2.2}/.github/dependabot.yml +0 -0
  14. {memware-0.2.1 → memware-0.2.2}/.github/workflows/ci.yml +0 -0
  15. {memware-0.2.1 → memware-0.2.2}/.github/workflows/release.yml +0 -0
  16. {memware-0.2.1 → memware-0.2.2}/.gitignore +0 -0
  17. {memware-0.2.1 → memware-0.2.2}/.pre-commit-config.yaml +0 -0
  18. {memware-0.2.1 → memware-0.2.2}/CODE_OF_CONDUCT.md +0 -0
  19. {memware-0.2.1 → memware-0.2.2}/CONTRIBUTING.md +0 -0
  20. {memware-0.2.1 → memware-0.2.2}/LICENSE +0 -0
  21. {memware-0.2.1 → memware-0.2.2}/README.md +0 -0
  22. {memware-0.2.1 → memware-0.2.2}/RELEASING.md +0 -0
  23. {memware-0.2.1 → memware-0.2.2}/SECURITY.md +0 -0
  24. {memware-0.2.1 → memware-0.2.2}/docs/design.md +0 -0
  25. {memware-0.2.1 → memware-0.2.2}/docs/eval.md +0 -0
  26. {memware-0.2.1 → memware-0.2.2}/docs/integrations.md +0 -0
  27. {memware-0.2.1 → memware-0.2.2}/docs/keeping-memory-clean.md +0 -0
  28. {memware-0.2.1 → memware-0.2.2}/docs/upstream-hermes-pr.md +0 -0
  29. {memware-0.2.1 → memware-0.2.2}/eval/questions.example.jsonl +0 -0
  30. {memware-0.2.1 → memware-0.2.2}/integrations/claude-code/.claude-plugin/plugin.json +0 -0
  31. {memware-0.2.1 → memware-0.2.2}/integrations/claude-code/README.md +0 -0
  32. {memware-0.2.1 → memware-0.2.2}/integrations/claude-code/hooks/hooks.json +0 -0
  33. {memware-0.2.1 → memware-0.2.2}/integrations/hermes/README.md +0 -0
  34. {memware-0.2.1 → memware-0.2.2}/integrations/hermes/memware/__init__.py +0 -0
  35. {memware-0.2.1 → memware-0.2.2}/integrations/hermes/memware/plugin.yaml +0 -0
  36. {memware-0.2.1 → memware-0.2.2}/integrations/hermes/upstream/README.md +0 -0
  37. {memware-0.2.1 → memware-0.2.2}/integrations/hermes/upstream/plugins/memory/memware/README.md +0 -0
  38. {memware-0.2.1 → memware-0.2.2}/integrations/hermes/upstream/plugins/memory/memware/__init__.py +0 -0
  39. {memware-0.2.1 → memware-0.2.2}/integrations/hermes/upstream/plugins/memory/memware/config_schema.py +0 -0
  40. {memware-0.2.1 → memware-0.2.2}/integrations/hermes/upstream/plugins/memory/memware/plugin.yaml +0 -0
  41. {memware-0.2.1 → memware-0.2.2}/integrations/hermes/upstream/tests/plugins/memory/test_memware_provider.py +0 -0
  42. {memware-0.2.1 → memware-0.2.2}/integrations/hermes/upstream/website/docs/user-guide/features/memory-providers.memware.md +0 -0
  43. {memware-0.2.1 → memware-0.2.2}/pyproject.toml +0 -0
  44. {memware-0.2.1 → memware-0.2.2}/scripts/leak-check.sh +0 -0
  45. {memware-0.2.1 → memware-0.2.2}/src/memware/cli.py +0 -0
  46. {memware-0.2.1 → memware-0.2.2}/src/memware/config.py +0 -0
  47. {memware-0.2.1 → memware-0.2.2}/src/memware/eval.py +0 -0
  48. {memware-0.2.1 → memware-0.2.2}/src/memware/index.py +0 -0
  49. {memware-0.2.1 → memware-0.2.2}/src/memware/ingest/__init__.py +0 -0
  50. {memware-0.2.1 → memware-0.2.2}/src/memware/ingest/claude_code.py +0 -0
  51. {memware-0.2.1 → memware-0.2.2}/src/memware/ingest/generic.py +0 -0
  52. {memware-0.2.1 → memware-0.2.2}/src/memware/ledger.py +0 -0
  53. {memware-0.2.1 → memware-0.2.2}/src/memware/mcp_server.py +0 -0
  54. {memware-0.2.1 → memware-0.2.2}/src/memware/passage.py +0 -0
  55. {memware-0.2.1 → memware-0.2.2}/src/memware/py.typed +0 -0
  56. {memware-0.2.1 → memware-0.2.2}/src/memware/review.py +0 -0
  57. {memware-0.2.1 → memware-0.2.2}/src/memware/store.py +0 -0
  58. {memware-0.2.1 → memware-0.2.2}/tests/__init__.py +0 -0
  59. {memware-0.2.1 → memware-0.2.2}/tests/conftest.py +0 -0
  60. {memware-0.2.1 → memware-0.2.2}/tests/test_cli.py +0 -0
  61. {memware-0.2.1 → memware-0.2.2}/tests/test_eval.py +0 -0
  62. {memware-0.2.1 → memware-0.2.2}/tests/test_hermes_plugin.py +0 -0
  63. {memware-0.2.1 → memware-0.2.2}/tests/test_hermes_upstream_plugin.py +0 -0
  64. {memware-0.2.1 → memware-0.2.2}/tests/test_index.py +0 -0
  65. {memware-0.2.1 → memware-0.2.2}/tests/test_ingest.py +0 -0
  66. {memware-0.2.1 → memware-0.2.2}/tests/test_ledger.py +0 -0
  67. {memware-0.2.1 → memware-0.2.2}/tests/test_passage.py +0 -0
  68. {memware-0.2.1 → memware-0.2.2}/tests/test_review.py +0 -0
@@ -6,6 +6,16 @@ All notable changes to this project are documented here. The format follows
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.2.2] - 2026-09-03
10
+
11
+ ### Fixed
12
+ - Backup retention now **promotes**: a snapshot ages forward through the 1/3/7/14-day tiers
13
+ instead of being pruned between them. The previous rule pruned every snapshot at age ~2, so a
14
+ daily-backup loop only ever kept a 0- and 1-day-old and never built a 3/7/14-day-old. Retention
15
+ keeps the newest plus the oldest per age band, always has a fresh ~1-day-old, drifts within
16
+ older tiers, prunes past the largest tier, and stays bounded (~5). Verified with a 30-day loop.
17
+
18
+
9
19
  ## [0.2.1] - 2026-09-03
10
20
 
11
21
  ## [0.2.0] - 2026-09-03
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: memware
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: Memory for AI agents that only remembers the latest truth: a bi-temporal belief ledger plus a transcript index, in one SQLite file.
5
5
  Project-URL: Homepage, https://github.com/ericwalisko/memware
6
6
  Project-URL: Issues, https://github.com/ericwalisko/memware/issues
@@ -35,9 +35,12 @@ memware backup # writes memware-YYYYMMDD-HHMMSS.db, prunes to the retent
35
35
  # and (by default) mirrors transcripts into <dest>/transcripts
36
36
  ```
37
37
 
38
- **Retention** keeps the newest snapshot plus the newest one at least 1, 3, 7 and 14 days old —
39
- roughly a 1-, 3-, 7- and 14-day-old backup at all times, not an unbounded pile. Tune with
40
- `backup.keep_days`.
38
+ **Retention promotes.** Because memware only ever *creates* fresh snapshots, retention lets a
39
+ snapshot **age forward** through the tiers rather than pruning it in between: it keeps the newest
40
+ snapshot and the oldest one in each age band `(0,1] (1,3] (3,7] (7,14]`, so the same file serves
41
+ as the ~1-day-old, then the ~3-, ~7-, ~14-day-old as time passes. You always have a fresh
42
+ ~1-day-old; the older tiers drift a little (that is fine and expected); anything past the largest
43
+ tier is pruned. The pile stays bounded (~5). Tune the tiers with `backup.keep_days`.
41
44
 
42
45
  **Transcripts.** `backup.include_transcripts` (on by default) mirrors your transcript source
43
46
  (default `~/.claude/projects`) into `<dest>/transcripts`, additively — an append-only archive
@@ -22,4 +22,4 @@ __all__ = [
22
22
  "history",
23
23
  "reject",
24
24
  ]
25
- __version__ = "0.2.1"
25
+ __version__ = "0.2.2"
@@ -62,24 +62,35 @@ def newest_age_hours(dest_dir: str | os.PathLike[str]) -> float | None:
62
62
  return (_now().timestamp() - _stamp(snaps[0]).timestamp()) / 3600.0
63
63
 
64
64
 
65
+ def _age_days(p: Path, now: datetime) -> float:
66
+ return (now.timestamp() - _stamp(p).timestamp()) / 86400.0
67
+
68
+
65
69
  def apply_retention(dest_dir: str | os.PathLike[str], keep_days: list[int]) -> list[Path]:
66
- """Tiered retention. Always keep the newest snapshot; then for each age bucket in
67
- ``keep_days`` (e.g. [1,3,7,14]) keep the newest snapshot at least that old. Delete the
68
- rest. Returns the deleted paths. This yields, over time, roughly one 1-, 3-, 7- and
69
- 14-day-old backup plus the latest — not an unbounded pile."""
70
- snaps = list_snapshots(dest_dir)
70
+ """Promotion retention: since we only ever *create* fresh snapshots, a snapshot must be
71
+ allowed to **age forward** into the next tier rather than be pruned between tiers.
72
+
73
+ Bands are the contiguous intervals ``(0, d0], (d0, d1], …`` for sorted ``keep_days``
74
+ (default 1,3,7,14). We keep the newest snapshot overall (tomorrow's ~1-day-old) and the
75
+ *oldest* snapshot in each band — the oldest is the one on the leading edge, so as days
76
+ pass it crosses into the next band and stays kept, i.e. the same file serves as the ~1-,
77
+ then ~3-, ~7-, ~14-day-old. Everything else, and anything older than the largest tier,
78
+ is pruned. Result: always a ~1-day-old, roughly one per tier (they drift; that's fine),
79
+ and a bounded pile of ~len(keep_days)+1. Returns the deleted paths."""
80
+ snaps = list_snapshots(dest_dir) # newest first
71
81
  if not snaps:
72
82
  return []
73
83
  now = _now()
74
- keep: set[Path] = {snaps[0]} # newest always
75
- for days in sorted(set(keep_days)):
76
- cutoff = now.timestamp() - days * 86400
77
- aged = [p for p in snaps if _stamp(p).timestamp() <= cutoff]
78
- if aged:
79
- keep.add(aged[0]) # newest that is at least `days` old
84
+ days = sorted(set(int(d) for d in keep_days))
85
+ keep: set[Path] = {snaps[0]} # always the freshest
86
+ edges = [0.0, *[float(d) for d in days]]
87
+ for lo, hi in zip(edges, edges[1:], strict=False):
88
+ band = [p for p in snaps if lo < _age_days(p, now) <= hi]
89
+ if band:
90
+ keep.add(band[-1]) # oldest in the band (snaps are newest-first) -> promotes forward
80
91
  deleted = []
81
92
  for p in snaps:
82
- if p not in keep:
93
+ if p not in keep: # includes anything older than the largest tier
83
94
  p.unlink(missing_ok=True)
84
95
  deleted.append(p)
85
96
  return deleted
@@ -31,20 +31,60 @@ def test_snapshot_is_consistent_and_restore_round_trips(tmp_path):
31
31
  assert s.stats()["turns"] == 5
32
32
 
33
33
 
34
- def test_tiered_retention_keeps_one_per_bucket(tmp_path):
34
+ def _age_all(dest):
35
+ """Rename every snapshot one day older — how we advance the clock against the real _now()."""
36
+ import datetime as dt
37
+
38
+ for p in sorted(dest.glob("memware-*.db")):
39
+ stamp = bk._stamp(p) - dt.timedelta(days=1)
40
+ p.rename(dest / f"memware-{stamp.strftime('%Y%m%d-%H%M%S')}.db")
41
+
42
+
43
+ def _ages(dest):
44
+ import datetime as dt
45
+
46
+ now = dt.datetime.now(dt.UTC)
47
+ return sorted((now - bk._stamp(p)).total_seconds() / 86400.0 for p in bk.list_snapshots(dest))
48
+
49
+
50
+ def test_retention_promotes_and_stays_bounded_over_a_daily_loop(tmp_path):
51
+ """The real usage pattern: one new snapshot per day, retention each day. A snapshot must age
52
+ forward through the tiers (not be pruned between them), there is always a ~1-day-old, the pile
53
+ stays small, and nothing lingers far past the largest tier. Files are dated in real time and
54
+ aged by renaming, so this exercises the true `_now()`-based logic."""
35
55
  import datetime as dt
36
56
 
37
57
  dest = tmp_path / "b"
38
58
  dest.mkdir()
39
- now = dt.datetime.now(dt.UTC)
40
- # one snapshot per day for 20 days
41
- for age in range(20):
42
- stamp = (now - dt.timedelta(days=age)).strftime("%Y%m%d-%H%M%S")
59
+ for _day in range(30):
60
+ _age_all(dest) # yesterday's snapshots become a day older
61
+ stamp = dt.datetime.now(dt.UTC).strftime("%Y%m%d-%H%M%S") # a fresh age-0 today
43
62
  (dest / f"memware-{stamp}.db").write_bytes(b"x")
63
+ bk.apply_retention(dest, [1, 3, 7, 14])
64
+
65
+ ages = _ages(dest)
66
+ assert min(ages) < 1 # always a fresh ~1-day-old
67
+ assert len(ages) <= 6 # bounded, not an unbounded pile
68
+ assert max(ages) <= 15 # nothing lingers far past the largest tier
69
+ assert any(2 <= a <= 9 for a in ages) # a snapshot promoted into the mid tiers (drift ok)
70
+ assert any(a >= 10 for a in ages) # and one aged into the oldest tier
71
+
72
+
73
+ def test_retention_prunes_snapshots_older_than_the_largest_tier(tmp_path):
74
+ dest = tmp_path / "b"
75
+ dest.mkdir()
76
+ for age in (0, 2, 6, 13, 20, 40): # 20 and 40 are past the 14-day tier
77
+ _write_dated(dest, age)
44
78
  bk.apply_retention(dest, [1, 3, 7, 14])
45
- kept = bk.list_snapshots(dest)
46
- # newest + one each at >=1,>=3,>=7,>=14 days = at most 5, far fewer than 20
47
- assert 4 <= len(kept) <= 5
79
+ assert all(a <= 15 for a in _ages(dest))
80
+
81
+
82
+ def _write_dated(dest, age_days):
83
+ import datetime as dt
84
+
85
+ dest.mkdir(parents=True, exist_ok=True)
86
+ stamp = (dt.datetime.now(dt.UTC) - dt.timedelta(days=age_days)).strftime("%Y%m%d-%H%M%S")
87
+ (dest / f"memware-{stamp}.db").write_bytes(b"x")
48
88
 
49
89
 
50
90
  def test_backup_cli_uses_config_dest_and_mirrors_transcripts(tmp_path, capsys, monkeypatch):
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes