loop-memory 0.4.16__tar.gz → 0.4.17__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 (170) hide show
  1. {loop_memory-0.4.16 → loop_memory-0.4.17}/PKG-INFO +5 -2
  2. {loop_memory-0.4.16 → loop_memory-0.4.17}/README.md +4 -1
  3. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/jobs/dream.py +20 -3
  4. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/storage/sqlite_store.py +15 -6
  5. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory.egg-info/PKG-INFO +5 -2
  6. {loop_memory-0.4.16 → loop_memory-0.4.17}/pyproject.toml +1 -1
  7. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_dream_2026_10_25.py +38 -1
  8. {loop_memory-0.4.16 → loop_memory-0.4.17}/LICENSE +0 -0
  9. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/__init__.py +0 -0
  10. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/backends/__init__.py +0 -0
  11. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/backends/embedding.py +0 -0
  12. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/backends/sentence_embedder.py +0 -0
  13. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/backends/vector_store.py +0 -0
  14. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/cli/__init__.py +0 -0
  15. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/cli/_common.py +0 -0
  16. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/cli/commands/__init__.py +0 -0
  17. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/cli/commands/cognitive.py +0 -0
  18. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/cli/commands/diag.py +0 -0
  19. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/cli/commands/doctor.py +0 -0
  20. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/cli/commands/graph.py +0 -0
  21. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/cli/commands/hooks.py +0 -0
  22. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/cli/commands/read.py +0 -0
  23. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/cli/commands/rules.py +0 -0
  24. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/cli/commands/serve.py +0 -0
  25. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/cli/commands/write.py +0 -0
  26. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/cli/main.py +0 -0
  27. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/decorators.py +0 -0
  28. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/doctor.py +0 -0
  29. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/engine/__init__.py +0 -0
  30. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/engine/loop.py +0 -0
  31. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/engine/reflect.py +0 -0
  32. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/examples/__init__.py +0 -0
  33. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/examples/demo.py +0 -0
  34. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/export/__init__.py +0 -0
  35. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/export/memory_md.py +0 -0
  36. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/graph/__init__.py +0 -0
  37. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/graph/build.py +0 -0
  38. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/graph/extract.py +0 -0
  39. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/ingest/__init__.py +0 -0
  40. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/ingest/loader.py +0 -0
  41. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/ingest/pipeline.py +0 -0
  42. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/jobs/__init__.py +0 -0
  43. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/jobs/cognitive.py +0 -0
  44. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/jobs/compact.py +0 -0
  45. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/jobs/consolidate.py +0 -0
  46. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/jobs/contradiction.py +0 -0
  47. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/jobs/evolution.py +0 -0
  48. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/jobs/graph.py +0 -0
  49. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/jobs/llm_compact_pass.py +0 -0
  50. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/jobs/llm_consolidate.py +0 -0
  51. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/jobs/scheduler.py +0 -0
  52. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/llm/__init__.py +0 -0
  53. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/llm/base.py +0 -0
  54. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/llm/openai_adapter.py +0 -0
  55. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/llm/providers.py +0 -0
  56. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/mcp/__init__.py +0 -0
  57. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/memory/__init__.py +0 -0
  58. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/memory/types.py +0 -0
  59. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/privacy/__init__.py +0 -0
  60. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/privacy/private.py +0 -0
  61. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/privacy/redact.py +0 -0
  62. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/py.typed +0 -0
  63. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/sdk.py +0 -0
  64. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/sdk_extensions.py +0 -0
  65. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/security/__init__.py +0 -0
  66. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/security/secrets.py +0 -0
  67. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/serve/__init__.py +0 -0
  68. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/serve/app.py +0 -0
  69. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/serve/handlers.py +0 -0
  70. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/serve/routes/_shared.py +0 -0
  71. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/serve/routes/admin.py +0 -0
  72. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/serve/routes/cognitive.py +0 -0
  73. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/serve/routes/export.py +0 -0
  74. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/serve/routes/graph.py +0 -0
  75. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/serve/routes/insights.py +0 -0
  76. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/serve/routes/memories.py +0 -0
  77. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/serve/routes/sessions.py +0 -0
  78. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/serve/routes/system.py +0 -0
  79. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/serve/routes/wiki.py +0 -0
  80. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/serve/static/__init__.py +0 -0
  81. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/serve/static/index.html +0 -0
  82. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/serve/watcher.py +0 -0
  83. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/storage/__init__.py +0 -0
  84. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/storage/retrieval.py +0 -0
  85. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/storage/snapshot.py +0 -0
  86. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/wiki/__init__.py +0 -0
  87. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/wiki/backfill.py +0 -0
  88. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/wiki/classifier.py +0 -0
  89. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/wiki/prompts.py +0 -0
  90. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory/wiki/scope.py +0 -0
  91. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory.egg-info/SOURCES.txt +0 -0
  92. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory.egg-info/dependency_links.txt +0 -0
  93. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory.egg-info/entry_points.txt +0 -0
  94. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory.egg-info/requires.txt +0 -0
  95. {loop_memory-0.4.16 → loop_memory-0.4.17}/loop_memory.egg-info/top_level.txt +0 -0
  96. {loop_memory-0.4.16 → loop_memory-0.4.17}/setup.cfg +0 -0
  97. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_admin_ingest_route.py +0 -0
  98. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_agent_memory_api.py +0 -0
  99. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_agent_memory_sdk.py +0 -0
  100. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_agents_init.py +0 -0
  101. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_audit_triggers_2026_09_27.py +0 -0
  102. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_auth_token_rotate.py +0 -0
  103. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_busy_timeout_2026_09_24.py +0 -0
  104. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_cache_cap_2026_09_23.py +0 -0
  105. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_cli_audit_supersede.py +0 -0
  106. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_cli_export_okf.py +0 -0
  107. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_cli_init.py +0 -0
  108. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_cli_memory_stats.py +0 -0
  109. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_cli_recall_as_of.py +0 -0
  110. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_cli_recall_paths.py +0 -0
  111. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_cli_recall_verbose.py +0 -0
  112. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_cli_rules.py +0 -0
  113. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_cli_snapshot.py +0 -0
  114. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_cli_v7.py +0 -0
  115. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_cli_version.py +0 -0
  116. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_content_hash_dedup_2026_10_18.py +0 -0
  117. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_contradictions.py +0 -0
  118. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_decorators_2026_10_18.py +0 -0
  119. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_doctor_2026_10_12.py +0 -0
  120. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_dream_cli_http_2026_10_25.py +0 -0
  121. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_evolution.py +0 -0
  122. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_evolution_quality.py +0 -0
  123. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_expanduser.py +0 -0
  124. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_export_ask.py +0 -0
  125. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_export_okf.py +0 -0
  126. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_graph.py +0 -0
  127. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_healthz_readyz_2026_09_24.py +0 -0
  128. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_import_okf_2026_09_27.py +0 -0
  129. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_ingest.py +0 -0
  130. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_latest_only_2026_10_25.py +0 -0
  131. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_llm_consolidator.py +0 -0
  132. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_llm_fingerprint_not_persisted.py +0 -0
  133. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_llm_providers.py +0 -0
  134. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_llm_test_endpoint.py +0 -0
  135. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_loop.py +0 -0
  136. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_mcp.py +0 -0
  137. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_memories_pagination.py +0 -0
  138. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_memory_stats.py +0 -0
  139. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_openclaw_loader.py +0 -0
  140. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_package_version.py +0 -0
  141. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_path_safety_2026_09_23.py +0 -0
  142. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_recall_as_of.py +0 -0
  143. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_recall_budget_2026_10_05.py +0 -0
  144. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_recall_paths.py +0 -0
  145. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_recall_provenance.py +0 -0
  146. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_recall_validity_http_2026_10_18.py +0 -0
  147. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_reflection.py +0 -0
  148. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_repo_framing.py +0 -0
  149. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_retrieval_channels_2026_10_18.py +0 -0
  150. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_routes_audit_2026_09_13.py +0 -0
  151. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_routes_audit_2026_09_20.py +0 -0
  152. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_score_api.py +0 -0
  153. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_scoring_v2.py +0 -0
  154. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_secrets.py +0 -0
  155. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_serve_app.py +0 -0
  156. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_serve_handlers.py +0 -0
  157. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_session_order.py +0 -0
  158. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_short_query.py +0 -0
  159. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_snapshot.py +0 -0
  160. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_store.py +0 -0
  161. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_subgraph_hops_2026_09_23.py +0 -0
  162. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_subject_key_2026_10_25.py +0 -0
  163. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_summarization.py +0 -0
  164. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_supersession_chain.py +0 -0
  165. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_universal_memory.py +0 -0
  166. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_vector_store.py +0 -0
  167. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_watcher.py +0 -0
  168. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_wiki_classifier.py +0 -0
  169. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_wiki_export_escape.py +0 -0
  170. {loop_memory-0.4.16 → loop_memory-0.4.17}/tests/test_wiki_prompts.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: loop-memory
3
- Version: 0.4.16
3
+ Version: 0.4.17
4
4
  Summary: A general-purpose, local memory system for every AI agent you run. Loop Memory auto-captures conversations from Codex / Claude / Hermes / OpenClaw, scores them by importance × recency × usage × feedback, distils them into a curated wiki, and serves everything from a single web UI.
5
5
  Author: Loop Memory contributors <loop-memory@users.noreply.github.com>
6
6
  License-Expression: MIT
@@ -470,7 +470,10 @@ never-used stale row is expired. Age alone never decides — that is the
470
470
  whole point of "In Defense of Forgotten" (arXiv 2605.27366).
471
471
 
472
472
  The pass is a **dry run by default**: `--apply` (CLI) or `"apply": true`
473
- (HTTP) is what actually writes.
473
+ (HTTP) is what actually writes. A dry run changes **no memory data** --
474
+ no expiry, no supersession, no rescore -- it only appends a
475
+ `cognitive_audit` row per proposed action (`action="suggest"`), which
476
+ is the reviewable plan the dashboard shows before you commit.
474
477
 
475
478
  ```bash
476
479
  loop-memory dream --json # machine-readable dry run
@@ -426,7 +426,10 @@ never-used stale row is expired. Age alone never decides — that is the
426
426
  whole point of "In Defense of Forgotten" (arXiv 2605.27366).
427
427
 
428
428
  The pass is a **dry run by default**: `--apply` (CLI) or `"apply": true`
429
- (HTTP) is what actually writes.
429
+ (HTTP) is what actually writes. A dry run changes **no memory data** --
430
+ no expiry, no supersession, no rescore -- it only appends a
431
+ `cognitive_audit` row per proposed action (`action="suggest"`), which
432
+ is the reviewable plan the dashboard shows before you commit.
430
433
 
431
434
  ```bash
432
435
  loop-memory dream --json # machine-readable dry run
@@ -30,7 +30,9 @@ What this job does
30
30
  pass:
31
31
 
32
32
  1. **Decay.** Rescore every row with the existing exponential
33
- half-life model so recency-sensitive ordering stays honest.
33
+ half-life model so recency-sensitive ordering stays honest. A dry
34
+ run computes the new scores and reports how many rows *would*
35
+ change, but writes nothing (audit 2026-10-25, fix).
34
36
 
35
37
  2. **Utility-aware selective forgetting.** A row is a *forgetting
36
38
  candidate* once it is past its own TTL (or past the sweep's
@@ -48,7 +50,12 @@ pass:
48
50
  ``recall(latest_only=True)`` can collapse to one answer.
49
51
 
50
52
  Everything is opt-in: ``apply=False`` (the default) produces a dry-run
51
- report and touches nothing.
53
+ report and **changes no memory data** -- no expiry, no supersession,
54
+ no rescore. The only write a dry run performs is one
55
+ ``cognitive_audit`` row per proposed action (with ``action="suggest"``
56
+ instead of ``"applied"``), because that row *is* the reviewable plan:
57
+ it is what lets the dashboard show what a real run would do before
58
+ you commit to it. Use ``record_audit=False`` to suppress even that.
52
59
  """
53
60
 
54
61
  from __future__ import annotations
@@ -282,7 +289,17 @@ class Dreamer:
282
289
  def _stage_decay(self, report: DreamReport) -> None:
283
290
  t = time.time()
284
291
  try:
285
- report.rescored = self.store.rescore_all(self.half_life_days)
292
+ # Audit 2026-10-25 (fix) -- the decay stage used to write
293
+ # unconditionally, so a *dry run* still rewrote the
294
+ # ``score`` column of every memory. That broke this
295
+ # module's own contract ("nothing is written unless
296
+ # apply=True") and the README / CHANGELOG wording. The
297
+ # store call now runs in dry-run mode, which computes the
298
+ # new scores and reports how many rows *would* change
299
+ # without touching the database.
300
+ report.rescored = self.store.rescore_all(
301
+ self.half_life_days, dry_run=not self.apply
302
+ )
286
303
  except Exception: # pragma: no cover - decay must never abort the pass
287
304
  log.warning("dream: rescore_all failed; continuing", exc_info=True)
288
305
  report.stages["decay"] = round((time.time() - t) * 1000, 1)
@@ -5110,10 +5110,18 @@ class MemoryStore:
5110
5110
  blended = 0.35 * importance + 0.65 * recency
5111
5111
  return max(0.0, min(1.0, blended))
5112
5112
 
5113
- def rescore_all(self, half_life_days: float = 30.0) -> int:
5113
+ def rescore_all(self, half_life_days: float = 30.0,
5114
+ dry_run: bool = False) -> int:
5114
5115
  """Recompute score for every memory using v2 (importance × recency
5115
5116
  × usage × feedback). One row at a time keeps WAL writes small
5116
- and lets us skip rows whose components didn't change."""
5117
+ and lets us skip rows whose components didn't change.
5118
+
5119
+ ``dry_run=True`` computes every new score but writes nothing,
5120
+ and returns how many rows *would* change. The dream pass uses
5121
+ it to honour its own "nothing is written unless apply=True"
5122
+ contract while still reporting the size of the decay job
5123
+ (audit 2026-10-25).
5124
+ """
5117
5125
  updated = 0
5118
5126
  now = time.time()
5119
5127
  with self._conn() as c:
@@ -5143,10 +5151,11 @@ class MemoryStore:
5143
5151
  "SELECT score FROM memories WHERE id=?", (r["id"],)
5144
5152
  ).fetchone()
5145
5153
  if cur is None or abs((cur["score"] or 0) - new) > 1e-3:
5146
- c.execute(
5147
- "UPDATE memories SET score=? WHERE id=?",
5148
- (new, r["id"]),
5149
- )
5154
+ if not dry_run:
5155
+ c.execute(
5156
+ "UPDATE memories SET score=? WHERE id=?",
5157
+ (new, r["id"]),
5158
+ )
5150
5159
  updated += 1
5151
5160
  return updated
5152
5161
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: loop-memory
3
- Version: 0.4.16
3
+ Version: 0.4.17
4
4
  Summary: A general-purpose, local memory system for every AI agent you run. Loop Memory auto-captures conversations from Codex / Claude / Hermes / OpenClaw, scores them by importance × recency × usage × feedback, distils them into a curated wiki, and serves everything from a single web UI.
5
5
  Author: Loop Memory contributors <loop-memory@users.noreply.github.com>
6
6
  License-Expression: MIT
@@ -470,7 +470,10 @@ never-used stale row is expired. Age alone never decides — that is the
470
470
  whole point of "In Defense of Forgotten" (arXiv 2605.27366).
471
471
 
472
472
  The pass is a **dry run by default**: `--apply` (CLI) or `"apply": true`
473
- (HTTP) is what actually writes.
473
+ (HTTP) is what actually writes. A dry run changes **no memory data** --
474
+ no expiry, no supersession, no rescore -- it only appends a
475
+ `cognitive_audit` row per proposed action (`action="suggest"`), which
476
+ is the reviewable plan the dashboard shows before you commit.
474
477
 
475
478
  ```bash
476
479
  loop-memory dream --json # machine-readable dry run
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "loop-memory"
7
- version = "0.4.16"
7
+ version = "0.4.17"
8
8
  description = "A general-purpose, local memory system for every AI agent you run. Loop Memory auto-captures conversations from Codex / Claude / Hermes / OpenClaw, scores them by importance × recency × usage × feedback, distils them into a curated wiki, and serves everything from a single web UI."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -8,7 +8,9 @@ contract:
8
8
  "what did we believe back then?";
9
9
  * **utility-aware, not age-only** -- a row that is past its deadline
10
10
  but is recalled often is *protected*;
11
- * **dry run by default** -- nothing is written unless ``apply=True``;
11
+ * **dry run by default** -- no memory *data* is changed unless
12
+ ``apply=True`` (the only write a dry run performs is the
13
+ ``cognitive_audit`` ``suggest`` row that records the plan);
12
14
  * **audited** -- every decision lands in ``cognitive_audit``.
13
15
  """
14
16
  from __future__ import annotations
@@ -84,6 +86,41 @@ class DreamPassTests(unittest.TestCase):
84
86
  self.assertFalse(report.applied)
85
87
  self.assertIsNone(self.store.get_memory(m.id).invalid_at)
86
88
 
89
+ def test_dry_run_does_not_touch_the_score_column(self) -> None:
90
+ """Regression (audit 2026-10-25, fix): the decay stage used to
91
+ call ``rescore_all`` unconditionally, so a dry run still
92
+ rewrote ``memories.score`` for every row -- breaking the
93
+ "nothing is written unless apply=True" contract."""
94
+ m = self.store.upsert_memory(kind="fact", text="a fact worth scoring")
95
+ with self.store._conn() as c:
96
+ c.execute("UPDATE memories SET score=0.01 WHERE id=?", (m.id,))
97
+ before = self.store.get_memory(m.id).score
98
+ self.assertEqual(before, 0.01)
99
+
100
+ report = dream(self.store, apply=False)
101
+ self.assertEqual(
102
+ self.store.get_memory(m.id).score, before,
103
+ "a dry run must not rewrite the score column",
104
+ )
105
+ # ...but it still reports the size of the decay job.
106
+ self.assertGreaterEqual(report.rescored, 1)
107
+
108
+ def test_apply_does_rescore(self) -> None:
109
+ m = self.store.upsert_memory(kind="fact", text="a fact worth scoring")
110
+ with self.store._conn() as c:
111
+ c.execute("UPDATE memories SET score=0.01 WHERE id=?", (m.id,))
112
+ report = dream(self.store, apply=True)
113
+ self.assertGreaterEqual(report.rescored, 1)
114
+ self.assertNotEqual(self.store.get_memory(m.id).score, 0.01)
115
+
116
+ def test_rescore_all_dry_run_writes_nothing(self) -> None:
117
+ m = self.store.upsert_memory(kind="fact", text="a fact worth scoring")
118
+ with self.store._conn() as c:
119
+ c.execute("UPDATE memories SET score=0.01 WHERE id=?", (m.id,))
120
+ n = self.store.rescore_all(dry_run=True)
121
+ self.assertGreaterEqual(n, 1)
122
+ self.assertEqual(self.store.get_memory(m.id).score, 0.01)
123
+
87
124
  def test_dry_run_reports_the_action(self) -> None:
88
125
  self.store.upsert_memory(
89
126
  kind="fact", text="the old build server is decommissioned",
File without changes
File without changes