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.
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/CHANGELOG.md +12 -1
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/PKG-INFO +8 -2
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/README.md +7 -1
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/docs/api.md +1 -1
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/docs/hermes-integration.md +1 -0
- luminary_memory-0.2.3/docs/lifecycle.md +72 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/hermes/README.md +19 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/hermes/SKILL.md +17 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/hermes/hooks/luminary-activity/handler.py +18 -4
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/pyproject.toml +1 -1
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/src/luminary_memory/__init__.py +1 -1
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/src/luminary_memory/api.py +53 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/src/luminary_memory/hermes/config.py +1 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/src/luminary_memory/hermes/config_schema.py +2 -1
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/src/luminary_memory/hermes/provider.py +7 -1
- luminary_memory-0.2.3/src/luminary_memory/ingest/llm.py +202 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/website/index.html +24 -14
- luminary_memory-0.2.2/docs/lifecycle.md +0 -31
- luminary_memory-0.2.2/src/luminary_memory/ingest/llm.py +0 -149
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/.github/workflows/ci.yml +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/.github/workflows/pages.yml +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/.github/workflows/publish.yml +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/.gitignore +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/CODE_OF_CONDUCT.md +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/CONTRIBUTING.md +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/LICENSE +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/PLAN-v0.2.0.md +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/PLAN-v0.2.1.md +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/PLAN.md +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/ROADMAP.md +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/SECURITY.md +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/benchmarks/README.md +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/benchmarks/RESULTS.md +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/benchmarks/__init__.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/benchmarks/hermes_provider_bench.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/benchmarks/metrics.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/benchmarks/run_benchmarks.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/benchmarks/synthetic.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/docs/architecture.md +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/docs/backends.md +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/docs/cli.md +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/docs/index.md +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/docs/quickstart.md +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/docs/recall.md +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/hermes/hooks/luminary-activity/HOOK.yaml +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/hermes/hooks/luminary-activity/README.md +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/hermes/install.sh +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/src/luminary_memory/backends/__init__.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/src/luminary_memory/backends/base.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/src/luminary_memory/backends/pgvector.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/src/luminary_memory/backends/sqlite.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/src/luminary_memory/budget.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/src/luminary_memory/cli.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/src/luminary_memory/config.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/src/luminary_memory/embeddings/__init__.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/src/luminary_memory/embeddings/fastembed.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/src/luminary_memory/export.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/src/luminary_memory/hermes/__init__.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/src/luminary_memory/hermes/plugin.yaml +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/src/luminary_memory/ingest/__init__.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/src/luminary_memory/ingest/whitelist.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/src/luminary_memory/lifecycle/__init__.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/src/luminary_memory/lifecycle/cleanup.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/src/luminary_memory/lifecycle/consolidate.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/src/luminary_memory/lifecycle/prune.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/src/luminary_memory/lifecycle/runner.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/src/luminary_memory/recall/__init__.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/src/luminary_memory/recall/dedup.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/src/luminary_memory/recall/fusion.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/src/luminary_memory/recall/graph.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/src/luminary_memory/recall/keyword.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/src/luminary_memory/recall/planner.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/src/luminary_memory/recall/semantic.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/src/luminary_memory/recall/snippets.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/src/luminary_memory/recall/temporal.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/src/luminary_memory/schema.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/src/luminary_memory/types.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/__init__.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/conftest.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/hermes/test_backup_paths.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/hermes/test_config.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/hermes/test_entrypoint.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/hermes/test_hooks.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/hermes/test_lifecycle.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/hermes/test_provider_import.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/hermes/test_recall.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/hermes/test_session_boundaries.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/hermes/test_sync_turn.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/hermes/test_system_prompt.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/hermes/test_tools.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/hermes_stubs/agent/memory_provider.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/test_api.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/test_api_extended.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/test_backend_pgvector.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/test_backend_sqlite.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/test_budget.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/test_cli.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/test_dedup_jaccard.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/test_embeddings.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/test_fusion_rrf.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/test_ingest_whitelist.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/test_lifecycle.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/test_recall_graph.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/test_recall_keyword.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/test_recall_orchestrator.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/test_recall_semantic.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/test_recall_temporal.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/test_schema.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/test_t10_export.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/test_t11_limit_zero.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/test_t1_hnsw.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/test_t3_planner.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/test_t4_batch.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/test_t5_enricher.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/test_t6_tag_scoped.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/test_t7_updated_at.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/test_t8_snippets.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/tests/test_t9_cli_json.py +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/website/.nojekyll +0 -0
- {luminary_memory-0.2.2 → luminary_memory-0.2.3}/website/css/luminary.css +0 -0
- {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
|
-
- **
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
84
|
-
"""Hook entry point: surface luminary activity on agent:
|
|
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
|
|
@@ -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
|
|
@@ -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
|
|
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
|
|
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 "{}"
|