luminary-memory 0.2.2__tar.gz → 0.2.3__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 (124) hide show
  1. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/CHANGELOG.md +12 -1
  2. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/PKG-INFO +8 -2
  3. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/README.md +7 -1
  4. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/docs/api.md +1 -1
  5. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/docs/hermes-integration.md +1 -0
  6. luminary_memory-0.2.3/docs/lifecycle.md +72 -0
  7. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/hermes/README.md +19 -0
  8. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/hermes/SKILL.md +17 -0
  9. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/hermes/hooks/luminary-activity/handler.py +18 -4
  10. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/pyproject.toml +1 -1
  11. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/src/luminary_memory/__init__.py +1 -1
  12. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/src/luminary_memory/api.py +53 -0
  13. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/src/luminary_memory/hermes/config.py +1 -0
  14. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/src/luminary_memory/hermes/config_schema.py +2 -1
  15. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/src/luminary_memory/hermes/provider.py +7 -1
  16. luminary_memory-0.2.3/src/luminary_memory/ingest/llm.py +202 -0
  17. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/website/index.html +24 -14
  18. luminary_memory-0.2.2/docs/lifecycle.md +0 -31
  19. luminary_memory-0.2.2/src/luminary_memory/ingest/llm.py +0 -149
  20. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  21. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  22. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  23. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/.github/workflows/ci.yml +0 -0
  24. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/.github/workflows/pages.yml +0 -0
  25. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/.github/workflows/publish.yml +0 -0
  26. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/.gitignore +0 -0
  27. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/CODE_OF_CONDUCT.md +0 -0
  28. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/CONTRIBUTING.md +0 -0
  29. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/LICENSE +0 -0
  30. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/PLAN-v0.2.0.md +0 -0
  31. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/PLAN-v0.2.1.md +0 -0
  32. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/PLAN.md +0 -0
  33. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/ROADMAP.md +0 -0
  34. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/SECURITY.md +0 -0
  35. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/benchmarks/README.md +0 -0
  36. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/benchmarks/RESULTS.md +0 -0
  37. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/benchmarks/__init__.py +0 -0
  38. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/benchmarks/hermes_provider_bench.py +0 -0
  39. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/benchmarks/metrics.py +0 -0
  40. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/benchmarks/run_benchmarks.py +0 -0
  41. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/benchmarks/synthetic.py +0 -0
  42. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/docs/architecture.md +0 -0
  43. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/docs/backends.md +0 -0
  44. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/docs/cli.md +0 -0
  45. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/docs/index.md +0 -0
  46. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/docs/quickstart.md +0 -0
  47. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/docs/recall.md +0 -0
  48. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/hermes/hooks/luminary-activity/HOOK.yaml +0 -0
  49. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/hermes/hooks/luminary-activity/README.md +0 -0
  50. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/hermes/install.sh +0 -0
  51. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/src/luminary_memory/backends/__init__.py +0 -0
  52. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/src/luminary_memory/backends/base.py +0 -0
  53. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/src/luminary_memory/backends/pgvector.py +0 -0
  54. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/src/luminary_memory/backends/sqlite.py +0 -0
  55. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/src/luminary_memory/budget.py +0 -0
  56. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/src/luminary_memory/cli.py +0 -0
  57. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/src/luminary_memory/config.py +0 -0
  58. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/src/luminary_memory/embeddings/__init__.py +0 -0
  59. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/src/luminary_memory/embeddings/fastembed.py +0 -0
  60. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/src/luminary_memory/export.py +0 -0
  61. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/src/luminary_memory/hermes/__init__.py +0 -0
  62. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/src/luminary_memory/hermes/plugin.yaml +0 -0
  63. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/src/luminary_memory/ingest/__init__.py +0 -0
  64. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/src/luminary_memory/ingest/whitelist.py +0 -0
  65. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/src/luminary_memory/lifecycle/__init__.py +0 -0
  66. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/src/luminary_memory/lifecycle/cleanup.py +0 -0
  67. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/src/luminary_memory/lifecycle/consolidate.py +0 -0
  68. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/src/luminary_memory/lifecycle/prune.py +0 -0
  69. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/src/luminary_memory/lifecycle/runner.py +0 -0
  70. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/src/luminary_memory/recall/__init__.py +0 -0
  71. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/src/luminary_memory/recall/dedup.py +0 -0
  72. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/src/luminary_memory/recall/fusion.py +0 -0
  73. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/src/luminary_memory/recall/graph.py +0 -0
  74. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/src/luminary_memory/recall/keyword.py +0 -0
  75. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/src/luminary_memory/recall/planner.py +0 -0
  76. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/src/luminary_memory/recall/semantic.py +0 -0
  77. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/src/luminary_memory/recall/snippets.py +0 -0
  78. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/src/luminary_memory/recall/temporal.py +0 -0
  79. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/src/luminary_memory/schema.py +0 -0
  80. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/src/luminary_memory/types.py +0 -0
  81. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/__init__.py +0 -0
  82. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/conftest.py +0 -0
  83. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/hermes/test_backup_paths.py +0 -0
  84. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/hermes/test_config.py +0 -0
  85. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/hermes/test_entrypoint.py +0 -0
  86. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/hermes/test_hooks.py +0 -0
  87. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/hermes/test_lifecycle.py +0 -0
  88. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/hermes/test_provider_import.py +0 -0
  89. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/hermes/test_recall.py +0 -0
  90. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/hermes/test_session_boundaries.py +0 -0
  91. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/hermes/test_sync_turn.py +0 -0
  92. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/hermes/test_system_prompt.py +0 -0
  93. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/hermes/test_tools.py +0 -0
  94. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/hermes_stubs/agent/memory_provider.py +0 -0
  95. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/test_api.py +0 -0
  96. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/test_api_extended.py +0 -0
  97. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/test_backend_pgvector.py +0 -0
  98. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/test_backend_sqlite.py +0 -0
  99. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/test_budget.py +0 -0
  100. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/test_cli.py +0 -0
  101. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/test_dedup_jaccard.py +0 -0
  102. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/test_embeddings.py +0 -0
  103. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/test_fusion_rrf.py +0 -0
  104. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/test_ingest_whitelist.py +0 -0
  105. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/test_lifecycle.py +0 -0
  106. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/test_recall_graph.py +0 -0
  107. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/test_recall_keyword.py +0 -0
  108. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/test_recall_orchestrator.py +0 -0
  109. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/test_recall_semantic.py +0 -0
  110. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/test_recall_temporal.py +0 -0
  111. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/test_schema.py +0 -0
  112. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/test_t10_export.py +0 -0
  113. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/test_t11_limit_zero.py +0 -0
  114. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/test_t1_hnsw.py +0 -0
  115. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/test_t3_planner.py +0 -0
  116. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/test_t4_batch.py +0 -0
  117. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/test_t5_enricher.py +0 -0
  118. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/test_t6_tag_scoped.py +0 -0
  119. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/test_t7_updated_at.py +0 -0
  120. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/test_t8_snippets.py +0 -0
  121. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/test_t9_cli_json.py +0 -0
  122. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/website/.nojekyll +0 -0
  123. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/website/css/luminary.css +0 -0
  124. {luminary_memory-0.2.2 → luminary_memory-0.2.3}/website/js/luminary.js +0 -0
@@ -2,6 +2,16 @@
2
2
 
3
3
  All notable changes to this project are documented in this file. The format follows [Keep a Changelog](https://keepachangelog.com/), and the project adheres to [Semantic Versioning](https://semver.org/).
4
4
 
5
+ ## [0.2.3] - 2026-08-18
6
+
7
+ ### Added
8
+
9
+ - **Hermes dashboard config schema** — exposes 18 luminary settings (mode, backend, recall limit, token budget, auto-recall/retain, **LLM memory curation**, **auto-maintain**, LLM endpoint, indicators) in the Hermes dashboard Provider panel. `auto_maintain` added; `ingest_llm` label corrected to "LLM memory curation".
10
+
11
+ ### Changed
12
+
13
+ - **Balanced LLM curation** — the enricher now rejects work/task logs and meta-talk about the memory system itself, while still keeping durable facts (not over-strict). Verified: work logs → `worth_saving: false`; facts → stored.
14
+
5
15
  ## [0.2.2] - 2026-08-18
6
16
 
7
17
  ### Added
@@ -10,7 +20,8 @@ All notable changes to this project are documented in this file. The format foll
10
20
  - `worth_saving` — chit-chat, greetings, and trivial turns are **dropped** instead of polluting the store.
11
21
  - `summary` — kept turns are stored as concise factual summaries (e.g. `"Deploy target is the staging cluster."`) instead of raw `User: ... / Assistant: ...` transcripts.
12
22
  - `entities` / `tags` — attached for richer recall.
13
- - **Transparency log** — provider writes `$HERMES_HOME/luminary/luminary.log` recording initialize/recall/retain/errors.
23
+ - **LLM store maintenance** — `MemoryClient.run_maintenance()` + provider `auto_maintain` (session-end): the LLM reviews the store and **deletes obsolete/contradicted/duplicate facts**, **updates changed ones**, and **keeps** current ones (verified: stale deploy target auto-removed).
24
+ - **Transparency log** — provider writes `$HERMES_HOME/luminary/luminary.log` recording initialize/recall/retain/maintenance/errors.
14
25
  - **`OpenAICompatibleEnricher` User-Agent header** — required by some OpenAI-compatible gateways (e.g. Command Code 403s without it).
15
26
 
16
27
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: luminary-memory
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: A lightweight, self-hosted memory layer for AI agents
5
5
  Project-URL: Homepage, https://github.com/alertxsto/luminary-memory
6
6
  Project-URL: Repository, https://github.com/alertxsto/luminary-memory
@@ -101,6 +101,12 @@ factual summaries (e.g. `"Deploy target is the staging cluster."`) instead of
101
101
  raw `User: ... / Assistant: ...` transcripts. One small LLM call per retained
102
102
  turn; any OpenAI-compatible endpoint works.
103
103
 
104
+ **LLM store maintenance (optional):** set `auto_maintain: true` in the same
105
+ config file to have the provider review the store at every session end — the
106
+ LLM keeps current facts, updates changed ones, and deletes stale, contradicted,
107
+ or duplicate memories, so the store never accumulates outdated or redundant
108
+ facts.
109
+
104
110
  ---
105
111
 
106
112
  ## Quickstart
@@ -262,7 +268,7 @@ Contributions welcome — see [CONTRIBUTING.md](CONTRIBUTING.md).
262
268
 
263
269
  ## Roadmap
264
270
 
265
- See [ROADMAP.md](ROADMAP.md) for the full product roadmap — v0.2.1 (Hermes memory provider), v0.3.0 (intelligence), and v1.0.0 (stable).
271
+ See [ROADMAP.md](ROADMAP.md) for the full product roadmap — v0.2.2 (LLM memory curation & maintenance), v0.3.0 (intelligence), and v1.0.0 (stable).
266
272
 
267
273
  ---
268
274
 
@@ -67,6 +67,12 @@ factual summaries (e.g. `"Deploy target is the staging cluster."`) instead of
67
67
  raw `User: ... / Assistant: ...` transcripts. One small LLM call per retained
68
68
  turn; any OpenAI-compatible endpoint works.
69
69
 
70
+ **LLM store maintenance (optional):** set `auto_maintain: true` in the same
71
+ config file to have the provider review the store at every session end — the
72
+ LLM keeps current facts, updates changed ones, and deletes stale, contradicted,
73
+ or duplicate memories, so the store never accumulates outdated or redundant
74
+ facts.
75
+
70
76
  ---
71
77
 
72
78
  ## Quickstart
@@ -228,7 +234,7 @@ Contributions welcome — see [CONTRIBUTING.md](CONTRIBUTING.md).
228
234
 
229
235
  ## Roadmap
230
236
 
231
- See [ROADMAP.md](ROADMAP.md) for the full product roadmap — v0.2.1 (Hermes memory provider), v0.3.0 (intelligence), and v1.0.0 (stable).
237
+ See [ROADMAP.md](ROADMAP.md) for the full product roadmap — v0.2.2 (LLM memory curation & maintenance), v0.3.0 (intelligence), and v1.0.0 (stable).
232
238
 
233
239
  ---
234
240
 
@@ -14,7 +14,7 @@ pdoc --output-dir docs/api --docformat markdown src/luminary_memory
14
14
 
15
15
  ## Quick surface
16
16
 
17
- - `MemoryClient` — `ingest`, `ingest_batch`, `recall` (tags + snippets + planner), `search`, `get`/`update`/`delete`, `list`, `export`/`import_memories`, `run_lifecycle`, `stats`, `close`.
17
+ - `MemoryClient` — `ingest`, `ingest_batch`, `recall` (tags + snippets + planner), `search`, `get`/`update`/`delete`, `list`, `export`/`import_memories`, `run_lifecycle`, `run_maintenance` (LLM curation), `stats`, `close`.
18
18
  - `Settings` — `LUMINARY_*` env vars, `as_dict()`.
19
19
  - `Memory`, `RecallResult` — `types.py`.
20
20
  - Subpackages `backends/`, `embeddings/`, `ingest/`, `recall/`, `lifecycle/`, `export`.
@@ -49,6 +49,7 @@ The provider reads `$HERMES_HOME/luminary/config.json` (created on first save wi
49
49
  | `llm_timeout` | `60` | Enricher request timeout (seconds) |
50
50
  | `recall_indicator` | `true` | Show `🌙 Luminary — recalled N memories` |
51
51
  | `retain_indicator` | `true` | Show `🌙 Luminary — memory saved` |
52
+ | `auto_maintain` | `false` | **LLM store review at session end** — keeps/updates/deletes stale, contradicted, or duplicate facts (requires `ingest_llm`) |
52
53
 
53
54
  ### LLM memory curation (v0.2.2+)
54
55
 
@@ -0,0 +1,72 @@
1
+ # Lifecycle
2
+
3
+ Two maintenance layers keep the store lean: deterministic passes
4
+ (`run_lifecycle()`) and LLM-driven curation (`run_maintenance()`).
5
+
6
+ ## `run_lifecycle()` — deterministic passes
7
+
8
+ ### cleanup — TTL expiry
9
+
10
+ Removes memories whose `ttl_seconds` has elapsed.
11
+
12
+ ### consolidate — merge near-duplicates
13
+
14
+ Clusters memories by Jaccard similarity (`LUMINARY_CONSOLIDATE_JACCARD_THRESHOLD`, default 0.9) and merges each cluster into a single memory — keeping the longest content, summing access counts, and unioning tags.
15
+
16
+ ### prune — drop low-value memories
17
+
18
+ Removes memories below a minimum importance (`LUMINARY_PRUNE_MIN_IMPORTANCE`, default 0.2).
19
+
20
+ ## `run_maintenance()` — LLM-driven curation (v0.2.2+)
21
+
22
+ Sends the store to the configured LLM enricher, which reviews every memory and
23
+ decides **keep** / **update** / **delete**:
24
+
25
+ - **delete** — obsolete, contradicted, or duplicate facts (e.g. the old
26
+ deploy target after it changed).
27
+ - **update** — facts that changed, with replacement content.
28
+ - **keep** — still-current facts.
29
+
30
+ Requires an LLM enricher (set `ingest_llm: true` + `llm_*` config); no-ops
31
+ otherwise. One LLM call per review run (all memories in a single call).
32
+
33
+ ```python
34
+ client = MemoryClient(db_path="memory.db", enricher=enricher)
35
+ print(client.run_maintenance())
36
+ # {'reviewed': 4, 'deleted': 2, 'updated': 0}
37
+ ```
38
+
39
+ ### Automatic maintenance in the Hermes provider
40
+
41
+ With `auto_maintain: true` in `~/.hermes/luminary/config.json` (plus
42
+ `ingest_llm: true`), the provider runs `run_maintenance()` automatically at
43
+ every session end:
44
+
45
+ ```json
46
+ {
47
+ "ingest_llm": true,
48
+ "llm_base_url": "https://api.commandcode.ai/provider/v1",
49
+ "llm_model": "deepseek/deepseek-v4-flash",
50
+ "llm_api_key": "your-key",
51
+ "auto_maintain": true
52
+ }
53
+ ```
54
+
55
+ Results are recorded in the transparency log
56
+ (`~/.hermes/luminary/luminary.log`): `maintenance {'reviewed': N, 'deleted': N, 'updated': N}`.
57
+
58
+ ## Scheduling
59
+
60
+ Run via cron for a self-maintaining store:
61
+
62
+ ```cron
63
+ 0 4 * * * /usr/local/bin/luminary-memory lifecycle
64
+ ```
65
+
66
+ Or programmatically:
67
+
68
+ ```python
69
+ client = MemoryClient(db_path="memory.db")
70
+ print(client.run_lifecycle())
71
+ client.close()
72
+ ```
@@ -61,6 +61,25 @@ Any OpenAI-compatible endpoint works. Costs one small LLM call per retained
61
61
  turn (temperature 0, strict JSON). If the enricher fails, the provider falls
62
62
  back to storing the turn verbatim — it never blocks.
63
63
 
64
+ ### Store maintenance (auto_maintain)
65
+
66
+ With `auto_maintain: true` (plus `ingest_llm: true`), the provider also
67
+ reviews the store at every session end — the LLM keeps current facts, updates
68
+ changed ones, and **deletes stale, contradicted, or duplicate memories**:
69
+
70
+ ```json
71
+ {
72
+ "ingest_llm": true,
73
+ "llm_base_url": "https://api.commandcode.ai/provider/v1",
74
+ "llm_model": "deepseek/deepseek-v4-flash",
75
+ "llm_api_key": "your-key",
76
+ "auto_maintain": true
77
+ }
78
+ ```
79
+
80
+ Results land in the transparency log:
81
+ `maintenance {'reviewed': N, 'deleted': N, 'updated': N}`.
82
+
64
83
  ## Manual install (no script)
65
84
 
66
85
  ```bash
@@ -76,6 +76,22 @@ client.list(limit=20) # most recent first
76
76
  Use this before re-ingesting — if a fact is already stored, update instead of
77
77
  duplicating.
78
78
 
79
+ ### Store maintenance
80
+
81
+ The store keeps itself clean two ways:
82
+
83
+ ```python
84
+ # deterministic passes (TTL, consolidate, prune)
85
+ client.run_lifecycle()
86
+
87
+ # LLM review: keep current facts, update changed ones, delete stale/duplicates
88
+ client.run_maintenance() # requires an LLM enricher (ingest_llm)
89
+ ```
90
+
91
+ In the Hermes provider, `auto_maintain: true` (plus `ingest_llm: true`) runs
92
+ `run_maintenance()` automatically at every session end — no manual trigger
93
+ needed. Results are logged to `~/.hermes/luminary/luminary.log`.
94
+
79
95
  ## Configuration — Tweaks
80
96
 
81
97
  Provider config lives in `~/.hermes/luminary/config.json` (auto-created,
@@ -92,6 +108,7 @@ Provider config lives in `~/.hermes/luminary/config.json` (auto-created,
92
108
  | `retain_every_n_turns` | `1` | Save every N turns (higher = fewer, batched saves) |
93
109
  | `ingest_llm` | `false` | **LLM memory curation** — drops chit-chat, stores factual summary instead of raw transcript |
94
110
  | `llm_base_url` / `llm_model` / `llm_api_key` | `""` | OpenAI-compatible enricher endpoint/model/key |
111
+ | `auto_maintain` | `false` | **LLM store review at session end** — keeps/updates/deletes stale or duplicate facts (needs `ingest_llm`) |
95
112
  | `recall_indicator` | `true` | Show `🌙 Luminary — recalled N memories` |
96
113
  | `retain_indicator` | `true` | Show `🌙 Luminary — memory saved` |
97
114
  | `backend` | `sqlite` | `sqlite` (zero config) or `pgvector` (scale) |
@@ -67,21 +67,35 @@ def _recent_activity(seconds: int = 30) -> str | None:
67
67
  "FROM memories",
68
68
  (cutoff,),
69
69
  ).fetchone()
70
- conn.close()
71
70
  n = int(row["n"] or 0)
72
71
  new_mem = bool(row["new_mem"])
73
72
  if n == 0:
73
+ conn.close()
74
74
  return None
75
75
  label = "stored" if new_mem else "recalled"
76
76
  noun = "memory" if n == 1 else "memories"
77
- return f"🌙 Luminary — {n} {noun} {label}"
77
+ lines = [f"🌙 Luminary — {n} {noun} {label}"]
78
+ # Show the content of newly stored facts (transparency).
79
+ if new_mem:
80
+ new_rows = conn.execute(
81
+ "SELECT content FROM memories "
82
+ "WHERE strftime('%s', created_at) > ? "
83
+ "ORDER BY id DESC LIMIT 3",
84
+ (cutoff,),
85
+ ).fetchall()
86
+ for r in new_rows:
87
+ content = str(r["content"] or "").replace("\n", " ")[:90]
88
+ if content:
89
+ lines.append(f" • {content}")
90
+ conn.close()
91
+ return "\n".join(lines)
78
92
  except Exception:
79
93
  logging.exception("activity read failed")
80
94
  return None
81
95
 
82
96
 
83
- def handler(event_type: str, context: dict) -> None:
84
- """Hook entry point: surface luminary activity on agent:start/end."""
97
+ def handle(event_type: str, context: dict) -> None:
98
+ """Hook entry point: surface luminary activity on agent:end."""
85
99
  # Only surface on agent:end (after work), cooldown-ish: skip if store idle.
86
100
  if event_type != "agent:end":
87
101
  return
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "luminary-memory"
7
- version = "0.2.2"
7
+ version = "0.2.3"
8
8
  description = "A lightweight, self-hosted memory layer for AI agents"
9
9
  readme = "README.md"
10
10
  license = { text = "Apache-2.0" }
@@ -1,4 +1,4 @@
1
1
  from luminary_memory.api import MemoryClient
2
2
 
3
- __version__ = "0.2.2"
3
+ __version__ = "0.2.3"
4
4
  __all__ = ["MemoryClient", "__version__"]
@@ -254,6 +254,59 @@ class MemoryClient:
254
254
 
255
255
  return run_lifecycle(self.backend, self.settings)
256
256
 
257
+ def run_maintenance(self, review_all: bool = True) -> dict:
258
+ """LLM-driven store maintenance: review memories and prune/update stale facts.
259
+
260
+ Sends the current store (or a recent slice when ``review_all`` is
261
+ false) to the configured LLM enricher, which decides per memory:
262
+ ``keep`` (unchanged), ``update`` (new content), or ``delete``
263
+ (obsolete/contradicted/duplicate). Applies the decisions and returns
264
+ a summary dict.
265
+
266
+ Requires ``ingest_llm`` (an LLM enricher); no-ops otherwise.
267
+ """
268
+ from luminary_memory.ingest.llm import _parse_enrichment_payload
269
+
270
+ if self.enricher is None or isinstance(self.enricher, NoopEnricher):
271
+ return {"skipped": "no LLM enricher configured (set ingest_llm)"}
272
+
273
+ memories = self.list(limit=500)
274
+ if not memories:
275
+ return {"reviewed": 0, "deleted": 0, "updated": 0}
276
+
277
+ raw = self.enricher.review_memories(memories)
278
+ data = _parse_enrichment_payload(raw)
279
+ actions = data.get("actions") or []
280
+ if not isinstance(actions, list):
281
+ return {"reviewed": len(memories), "deleted": 0, "updated": 0, "error": "bad LLM response"}
282
+
283
+ deleted = updated = 0
284
+ by_id = {m.id: m for m in memories}
285
+ for act in actions:
286
+ if not isinstance(act, dict):
287
+ continue
288
+ mid = act.get("id")
289
+ if mid not in by_id:
290
+ continue
291
+ action = act.get("action")
292
+ if action == "delete":
293
+ try:
294
+ self.delete(int(mid))
295
+ deleted += 1
296
+ except Exception: # noqa: BLE001, S110
297
+ pass
298
+ elif action == "update":
299
+ new_content = act.get("content")
300
+ if isinstance(new_content, str) and new_content.strip():
301
+ try:
302
+ m = by_id[mid]
303
+ m.content = new_content.strip()
304
+ self.update(m)
305
+ updated += 1
306
+ except Exception: # noqa: BLE001, S110
307
+ pass
308
+ return {"reviewed": len(memories), "deleted": deleted, "updated": updated}
309
+
257
310
  def export(self, path, include_embeddings: bool = True) -> dict:
258
311
  """Export all memories to *path* (versioned JSON)."""
259
312
  from luminary_memory.export import export_memories
@@ -33,6 +33,7 @@ _DEFAULTS: dict = {
33
33
  "recall_indicator": True,
34
34
  "retain_indicator": True,
35
35
  "extract_on_session_end": False,
36
+ "auto_maintain": False, # LLM store review on session end (needs ingest_llm)
36
37
  }
37
38
 
38
39
 
@@ -38,7 +38,8 @@ CONFIG_SCHEMA = ProviderConfigSchema(
38
38
  {"key": "retain_every_n_turns", "label": "Retain every N turns", "type": "number"},
39
39
  {"key": "retain_user_prefix", "label": "User prefix", "type": "text"},
40
40
  {"key": "retain_assistant_prefix", "label": "Assistant prefix", "type": "text"},
41
- {"key": "ingest_llm", "label": "LLM enrichment", "type": "boolean"},
41
+ {"key": "ingest_llm", "label": "LLM memory curation", "type": "boolean"},
42
+ {"key": "auto_maintain", "label": "Auto-maintain store (LLM)", "type": "boolean"},
42
43
  {"key": "llm_base_url", "label": "LLM base URL", "type": "text"},
43
44
  {"key": "llm_model", "label": "LLM model", "type": "text"},
44
45
  {"key": "llm_timeout", "label": "LLM timeout (s)", "type": "number"},
@@ -393,10 +393,16 @@ class LuminaryMemoryProvider(MemoryProvider):
393
393
  self._turn_counter = 0
394
394
 
395
395
  def on_session_end(self, messages) -> None:
396
- """Flush buffered turns under the current session lineage."""
396
+ """Flush buffered turns; optionally run LLM store maintenance."""
397
397
  if not self._client or self._shutting_down.is_set():
398
398
  return
399
399
  self._flush_session_turns()
400
+ if self._config.get("auto_maintain", False) and self._config.get("ingest_llm", False):
401
+ try:
402
+ result = self._client.run_maintenance()
403
+ self._log.info("maintenance %s", result)
404
+ except Exception:
405
+ logging.getLogger(__name__).exception("LLM maintenance failed")
400
406
 
401
407
  def on_session_switch(self, new_session_id: str, **kwargs) -> None:
402
408
  """Flush the old session, then rebind to the new one."""
@@ -0,0 +1,202 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import re
5
+ from dataclasses import dataclass, field
6
+
7
+
8
+ @dataclass
9
+ class EnrichedContent:
10
+ content: str
11
+ summary: str | None = None
12
+ entities: list[str] = field(default_factory=list)
13
+ tags: list[str] = field(default_factory=list)
14
+ worth_saving: bool = True
15
+
16
+
17
+ class LLMEnricher:
18
+ def enrich(self, text: str) -> EnrichedContent:
19
+ raise NotImplementedError
20
+
21
+ def review_memories(self, memories: list) -> str:
22
+ """Return a JSON actions payload for curating *memories*.
23
+
24
+ Each memory has ``id`` and ``content``. The default implementation
25
+ calls :meth:`enrich`; subclasses may override for a dedicated prompt.
26
+ """
27
+ import json as _json
28
+
29
+ items = "\n".join(
30
+ f'- {{"id": {m.id}, "content": {_json.dumps(str(getattr(m, "content", ""))[:300])}}}'
31
+ for m in memories
32
+ )
33
+ prompt = (
34
+ "Review this memory store. For each item decide: \"keep\", \"update\" "
35
+ "(with replacement content), or \"delete\" (obsolete, contradicted, or "
36
+ "duplicate). Return STRICT JSON: {\"actions\": [{\"id\": N, \"action\": "
37
+ "\"keep|update|delete\", \"content\": \"new content if update\"}]}. "
38
+ "No extra keys, no markdown.\n\nMemories:\n" + items
39
+ )
40
+ return self.enrich(prompt).content
41
+
42
+
43
+ class NoopEnricher(LLMEnricher):
44
+ def enrich(self, text: str) -> EnrichedContent:
45
+ return EnrichedContent(content=text)
46
+
47
+
48
+ _FENCE_RE = re.compile(r"```(?:json)?\s*([\s\S]*?)\s*```", re.IGNORECASE)
49
+
50
+
51
+ def _strip_fences(s: str) -> str:
52
+ m = _FENCE_RE.search(s)
53
+ return m.group(1).strip() if m else s.strip()
54
+
55
+
56
+ def _parse_enrichment_payload(raw: str) -> dict:
57
+ raw = _strip_fences(raw)
58
+ # Try strict JSON first, then best-effort substring extraction.
59
+ try:
60
+ return json.loads(raw)
61
+ except Exception: # noqa: BLE001, S110
62
+ pass
63
+ # Extract first {...} block.
64
+ start = raw.find("{")
65
+ end = raw.rfind("}")
66
+ if start != -1 and end != -1 and end > start:
67
+ try:
68
+ return json.loads(raw[start:end + 1])
69
+ except Exception: # noqa: BLE001, S110
70
+ pass
71
+ return {}
72
+
73
+
74
+ class OpenAICompatibleEnricher(LLMEnricher):
75
+ """Provider-agnostic enricher over any OpenAI-compatible chat/completions endpoint.
76
+
77
+ Uses stdlib ``urllib.request`` so no new runtime dependency is introduced.
78
+ Any failure (network, timeout, malformed body) returns a passthrough
79
+ ``EnrichedContent`` with the original text.
80
+ """
81
+
82
+ def __init__(
83
+ self,
84
+ base_url: str | None = None,
85
+ api_key: str | None = None,
86
+ model: str | None = None,
87
+ timeout: int | None = None,
88
+ ):
89
+ from luminary_memory.config import Settings
90
+
91
+ s = Settings()
92
+ self.base_url = (base_url if base_url is not None else s.llm_base_url or "").rstrip("/")
93
+ self.api_key = api_key if api_key is not None else s.llm_api_key
94
+ self.model = model if model is not None else s.llm_model
95
+ self.timeout = int(timeout if timeout is not None else s.llm_timeout)
96
+
97
+ def _call_llm(self, messages: list[dict]) -> str:
98
+ """Call the OpenAI-compatible endpoint, return the assistant content."""
99
+ import urllib.request
100
+
101
+ url = f"{self.base_url}/chat/completions"
102
+ body = json.dumps({"model": self.model, "messages": messages, "temperature": 0}).encode()
103
+ headers = {
104
+ "Content-Type": "application/json",
105
+ "User-Agent": "luminary-memory/0.2.1",
106
+ }
107
+ if self.api_key:
108
+ headers["Authorization"] = f"Bearer {self.api_key}"
109
+ req = urllib.request.Request(url, data=body, headers=headers, method="POST")
110
+ with urllib.request.urlopen(req, timeout=self.timeout) as resp:
111
+ raw = resp.read()
112
+ payload = json.loads(raw.decode() if isinstance(raw, (bytes, bytearray)) else str(raw))
113
+ try:
114
+ choices = payload.get("choices") or []
115
+ if choices:
116
+ return (choices[0].get("message") or {}).get("content") or ""
117
+ except Exception: # noqa: BLE001, S110
118
+ pass
119
+ return ""
120
+
121
+ def enrich(self, text: str) -> EnrichedContent:
122
+ if not self.base_url:
123
+ return EnrichedContent(content=text)
124
+ try:
125
+ content_str = self._call_llm(
126
+ [
127
+ {
128
+ "role": "system",
129
+ "content": (
130
+ "You are a memory curation helper for an AI agent's long-term memory. "
131
+ "Given a conversation turn (User/Assistant), decide whether it contains "
132
+ "durable, useful facts worth remembering — preferences, decisions, "
133
+ "environment details, project conventions, instructions. "
134
+ "A fact is worth saving if it will still matter days or weeks from now. "
135
+ "Do NOT save: chit-chat, greetings, trivial acknowledgements, or "
136
+ "task/work logs (descriptions of what was just done — completed fixes, "
137
+ "implementations, or meta-talk about the memory system itself). "
138
+ "Return STRICT JSON with exactly these keys:\n"
139
+ "- worth_saving (boolean): true only for durable facts; false for "
140
+ "chit-chat, work logs, and one-off questions.\n"
141
+ "- summary (string): if worth_saving is true, a concise factual summary "
142
+ "in the same language as the turn (e.g. 'User prefers X', 'Deploy target "
143
+ "is Y'). If false, an empty string.\n"
144
+ "- entities (list of strings): key nouns/names mentioned. Empty if false.\n"
145
+ "- tags (list of strings): 1-3 short tags. Empty if false.\n"
146
+ "No extra keys, no markdown."
147
+ ),
148
+ },
149
+ {"role": "user", "content": text},
150
+ ]
151
+ )
152
+ data = _parse_enrichment_payload(content_str)
153
+ summary = data.get("summary")
154
+ entities = data.get("entities") or []
155
+ tags = data.get("tags") or []
156
+ worth = data.get("worth_saving")
157
+ # Normalize to expected types.
158
+ if not isinstance(entities, list):
159
+ entities = []
160
+ if not isinstance(tags, list):
161
+ tags = []
162
+ if not isinstance(summary, str):
163
+ summary = None
164
+ return EnrichedContent(
165
+ content=text,
166
+ summary=summary if summary and summary.strip() else None,
167
+ entities=[str(x) for x in entities if isinstance(x, str) and x.strip()],
168
+ tags=[str(x).strip() for x in tags if isinstance(x, str) and x.strip()],
169
+ worth_saving=bool(worth) if worth is not None else True,
170
+ )
171
+ except Exception: # noqa: BLE001 -- enrichment is best-effort
172
+ return EnrichedContent(content=text)
173
+
174
+ def review_memories(self, memories: list) -> str:
175
+ """Curate the store: return a JSON actions payload (keep/update/delete)."""
176
+ if not self.base_url:
177
+ return "{}"
178
+ try:
179
+ import json as _json
180
+
181
+ items = "\n".join(
182
+ f'- {{"id": {m.id}, "content": {_json.dumps(str(getattr(m, "content", ""))[:300])}}}'
183
+ for m in memories
184
+ )
185
+ return self._call_llm(
186
+ [
187
+ {
188
+ "role": "system",
189
+ "content": (
190
+ "You are a memory curator for an AI agent's long-term memory store. "
191
+ "Review each memory and decide: \"keep\", \"update\" (provide new "
192
+ "content), or \"delete\" (obsolete, contradicted, or duplicate). "
193
+ "Return STRICT JSON: {\"actions\": [{\"id\": N, \"action\": "
194
+ "\"keep|update|delete\", \"content\": \"new content if update\"}]}. "
195
+ "No extra keys, no markdown."
196
+ ),
197
+ },
198
+ {"role": "user", "content": items},
199
+ ]
200
+ )
201
+ except Exception: # noqa: BLE001 -- curation is best-effort
202
+ return "{}"