kore-memory 2.1.0__tar.gz → 2.4.0__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 (125) hide show
  1. kore_memory-2.4.0/.github/workflows/benchmark.yml +48 -0
  2. {kore_memory-2.1.0 → kore_memory-2.4.0}/.github/workflows/ci.yml +8 -1
  3. {kore_memory-2.1.0 → kore_memory-2.4.0}/CHANGELOG.md +142 -0
  4. {kore_memory-2.1.0 → kore_memory-2.4.0}/PKG-INFO +1 -1
  5. {kore_memory-2.1.0 → kore_memory-2.4.0}/kore_memory/config.py +1 -1
  6. {kore_memory-2.1.0 → kore_memory-2.4.0}/kore_memory/conflict_detector.py +1 -4
  7. kore_memory-2.4.0/kore_memory/context_assembler.py +168 -0
  8. {kore_memory-2.1.0 → kore_memory-2.4.0}/kore_memory/database.py +31 -3
  9. kore_memory-2.4.0/kore_memory/filesystem_overlay.py +367 -0
  10. {kore_memory-2.1.0 → kore_memory-2.4.0}/kore_memory/main.py +259 -12
  11. {kore_memory-2.1.0 → kore_memory-2.4.0}/kore_memory/mcp_server.py +229 -23
  12. {kore_memory-2.1.0 → kore_memory-2.4.0}/kore_memory/models.py +168 -1
  13. kore_memory-2.4.0/kore_memory/ranking.py +279 -0
  14. {kore_memory-2.1.0 → kore_memory-2.4.0}/kore_memory/repository/__init__.py +12 -1
  15. kore_memory-2.4.0/kore_memory/repository/graph.py +427 -0
  16. {kore_memory-2.1.0 → kore_memory-2.4.0}/kore_memory/repository/memory.py +17 -3
  17. {kore_memory-2.1.0 → kore_memory-2.4.0}/kore_memory/repository/search.py +180 -59
  18. {kore_memory-2.1.0 → kore_memory-2.4.0}/kore_memory/templates/dashboard.html +6 -2
  19. {kore_memory-2.1.0 → kore_memory-2.4.0}/presets/claude-code/README.md +2 -0
  20. {kore_memory-2.1.0 → kore_memory-2.4.0}/presets/cursor/README.md +2 -2
  21. {kore_memory-2.1.0 → kore_memory-2.4.0}/pyproject.toml +2 -1
  22. kore_memory-2.4.0/scripts/assert_benchmarks.py +159 -0
  23. {kore_memory-2.1.0 → kore_memory-2.4.0}/sdk/js/package.json +1 -1
  24. kore_memory-2.4.0/tests/benchmarks/__init__.py +1 -0
  25. kore_memory-2.4.0/tests/benchmarks/conftest_bench.py +63 -0
  26. kore_memory-2.4.0/tests/benchmarks/datasets/dataset_a_temporal.json +1648 -0
  27. kore_memory-2.4.0/tests/benchmarks/datasets/dataset_b_conflicts.json +1516 -0
  28. kore_memory-2.4.0/tests/benchmarks/datasets/dataset_c_coding.json +1981 -0
  29. kore_memory-2.4.0/tests/benchmarks/datasets/dataset_d_graph.json +462 -0
  30. kore_memory-2.4.0/tests/benchmarks/datasets/dataset_e_context.json +575 -0
  31. kore_memory-2.4.0/tests/benchmarks/datasets/loaders.py +53 -0
  32. kore_memory-2.4.0/tests/benchmarks/test_benchmarks.py +632 -0
  33. {kore_memory-2.1.0 → kore_memory-2.4.0}/tests/test_client.py +1 -1
  34. {kore_memory-2.1.0 → kore_memory-2.4.0}/tests/test_client_sync.py +1 -1
  35. {kore_memory-2.1.0 → kore_memory-2.4.0}/tests/test_v12_features.py +3 -0
  36. {kore_memory-2.1.0 → kore_memory-2.4.0}/tests/test_v21_temporal.py +5 -5
  37. kore_memory-2.4.0/tests/test_wave3_graph.py +290 -0
  38. kore_memory-2.4.0/tests/test_wave3_overlay.py +350 -0
  39. kore_memory-2.1.0/.claude/settings.local.json +0 -6
  40. kore_memory-2.1.0/kore_memory/ranking.py +0 -158
  41. kore_memory-2.1.0/kore_memory/repository/graph.py +0 -220
  42. {kore_memory-2.1.0 → kore_memory-2.4.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  43. {kore_memory-2.1.0 → kore_memory-2.4.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  44. {kore_memory-2.1.0 → kore_memory-2.4.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  45. {kore_memory-2.1.0 → kore_memory-2.4.0}/.github/workflows/build-sdk.yml +0 -0
  46. {kore_memory-2.1.0 → kore_memory-2.4.0}/.github/workflows/publish.yml +0 -0
  47. {kore_memory-2.1.0 → kore_memory-2.4.0}/.gitignore +0 -0
  48. {kore_memory-2.1.0 → kore_memory-2.4.0}/CONTRIBUTING.md +0 -0
  49. {kore_memory-2.1.0 → kore_memory-2.4.0}/LICENSE +0 -0
  50. {kore_memory-2.1.0 → kore_memory-2.4.0}/MIGRATION-v0.6.md +0 -0
  51. {kore_memory-2.1.0 → kore_memory-2.4.0}/README.md +0 -0
  52. {kore_memory-2.1.0 → kore_memory-2.4.0}/article-devto.md +0 -0
  53. {kore_memory-2.1.0 → kore_memory-2.4.0}/assets/favicon.svg +0 -0
  54. {kore_memory-2.1.0 → kore_memory-2.4.0}/assets/logo.svg +0 -0
  55. {kore_memory-2.1.0 → kore_memory-2.4.0}/docs/competitive-analysis-2025.md +0 -0
  56. {kore_memory-2.1.0 → kore_memory-2.4.0}/docs/quickstart-v2.1.md +0 -0
  57. {kore_memory-2.1.0 → kore_memory-2.4.0}/docs/v1.0-roadmap.md +0 -0
  58. {kore_memory-2.1.0 → kore_memory-2.4.0}/examples/async_usage.py +0 -0
  59. {kore_memory-2.1.0 → kore_memory-2.4.0}/examples/basic_usage.py +0 -0
  60. {kore_memory-2.1.0 → kore_memory-2.4.0}/examples/langchain_example.py +0 -0
  61. {kore_memory-2.1.0 → kore_memory-2.4.0}/hatch_build.py +0 -0
  62. {kore_memory-2.1.0 → kore_memory-2.4.0}/kore-daemon.sh +0 -0
  63. {kore_memory-2.1.0 → kore_memory-2.4.0}/kore_memory/__init__.py +0 -0
  64. {kore_memory-2.1.0 → kore_memory-2.4.0}/kore_memory/acl.py +0 -0
  65. {kore_memory-2.1.0 → kore_memory-2.4.0}/kore_memory/analytics.py +0 -0
  66. {kore_memory-2.1.0 → kore_memory-2.4.0}/kore_memory/audit.py +0 -0
  67. {kore_memory-2.1.0 → kore_memory-2.4.0}/kore_memory/auth.py +0 -0
  68. {kore_memory-2.1.0 → kore_memory-2.4.0}/kore_memory/auto_tuner.py +0 -0
  69. {kore_memory-2.1.0 → kore_memory-2.4.0}/kore_memory/cli.py +0 -0
  70. {kore_memory-2.1.0 → kore_memory-2.4.0}/kore_memory/client.py +0 -0
  71. {kore_memory-2.1.0 → kore_memory-2.4.0}/kore_memory/compressor.py +0 -0
  72. {kore_memory-2.1.0 → kore_memory-2.4.0}/kore_memory/dashboard.py +0 -0
  73. {kore_memory-2.1.0 → kore_memory-2.4.0}/kore_memory/decay.py +0 -0
  74. {kore_memory-2.1.0 → kore_memory-2.4.0}/kore_memory/embedder.py +0 -0
  75. {kore_memory-2.1.0 → kore_memory-2.4.0}/kore_memory/events.py +0 -0
  76. {kore_memory-2.1.0 → kore_memory-2.4.0}/kore_memory/integrations/__init__.py +0 -0
  77. {kore_memory-2.1.0 → kore_memory-2.4.0}/kore_memory/integrations/crewai.py +0 -0
  78. {kore_memory-2.1.0 → kore_memory-2.4.0}/kore_memory/integrations/entities.py +0 -0
  79. {kore_memory-2.1.0 → kore_memory-2.4.0}/kore_memory/integrations/langchain.py +0 -0
  80. {kore_memory-2.1.0 → kore_memory-2.4.0}/kore_memory/integrations/openai_agents.py +0 -0
  81. {kore_memory-2.1.0 → kore_memory-2.4.0}/kore_memory/integrations/pydantic_ai.py +0 -0
  82. {kore_memory-2.1.0 → kore_memory-2.4.0}/kore_memory/plugins.py +0 -0
  83. {kore_memory-2.1.0 → kore_memory-2.4.0}/kore_memory/py.typed +0 -0
  84. {kore_memory-2.1.0 → kore_memory-2.4.0}/kore_memory/repository/lifecycle.py +0 -0
  85. {kore_memory-2.1.0 → kore_memory-2.4.0}/kore_memory/repository/sessions.py +0 -0
  86. {kore_memory-2.1.0 → kore_memory-2.4.0}/kore_memory/scorer.py +0 -0
  87. {kore_memory-2.1.0 → kore_memory-2.4.0}/kore_memory/summarizer.py +0 -0
  88. {kore_memory-2.1.0 → kore_memory-2.4.0}/kore_memory/vector_index.py +0 -0
  89. {kore_memory-2.1.0 → kore_memory-2.4.0}/kore_memory/welcome.py +0 -0
  90. {kore_memory-2.1.0 → kore_memory-2.4.0}/presets/claude-code/mcp.json +0 -0
  91. {kore_memory-2.1.0 → kore_memory-2.4.0}/presets/cursor/mcp.json +0 -0
  92. {kore_memory-2.1.0 → kore_memory-2.4.0}/requirements.txt +0 -0
  93. {kore_memory-2.1.0 → kore_memory-2.4.0}/scripts/import_memory.py +0 -0
  94. {kore_memory-2.1.0 → kore_memory-2.4.0}/sdk/js/.gitignore +0 -0
  95. {kore_memory-2.1.0 → kore_memory-2.4.0}/sdk/js/README.md +0 -0
  96. {kore_memory-2.1.0 → kore_memory-2.4.0}/sdk/js/package-lock.json +0 -0
  97. {kore_memory-2.1.0 → kore_memory-2.4.0}/sdk/js/src/client.ts +0 -0
  98. {kore_memory-2.1.0 → kore_memory-2.4.0}/sdk/js/src/errors.ts +0 -0
  99. {kore_memory-2.1.0 → kore_memory-2.4.0}/sdk/js/src/index.ts +0 -0
  100. {kore_memory-2.1.0 → kore_memory-2.4.0}/sdk/js/src/types.ts +0 -0
  101. {kore_memory-2.1.0 → kore_memory-2.4.0}/sdk/js/tests/client.test.ts +0 -0
  102. {kore_memory-2.1.0 → kore_memory-2.4.0}/sdk/js/tests/errors.test.ts +0 -0
  103. {kore_memory-2.1.0 → kore_memory-2.4.0}/sdk/js/tests/helpers.ts +0 -0
  104. {kore_memory-2.1.0 → kore_memory-2.4.0}/sdk/js/tsconfig.json +0 -0
  105. {kore_memory-2.1.0 → kore_memory-2.4.0}/sdk/js/tsup.config.ts +0 -0
  106. {kore_memory-2.1.0 → kore_memory-2.4.0}/sdk/js/vitest.config.ts +0 -0
  107. {kore_memory-2.1.0 → kore_memory-2.4.0}/start.sh +0 -0
  108. {kore_memory-2.1.0 → kore_memory-2.4.0}/test_pagination_fix.py +0 -0
  109. {kore_memory-2.1.0 → kore_memory-2.4.0}/tests/__init__.py +0 -0
  110. {kore_memory-2.1.0 → kore_memory-2.4.0}/tests/conftest.py +0 -0
  111. {kore_memory-2.1.0 → kore_memory-2.4.0}/tests/test_api.py +0 -0
  112. {kore_memory-2.1.0 → kore_memory-2.4.0}/tests/test_audit.py +0 -0
  113. {kore_memory-2.1.0 → kore_memory-2.4.0}/tests/test_auth_events.py +0 -0
  114. {kore_memory-2.1.0 → kore_memory-2.4.0}/tests/test_auto_tuner.py +0 -0
  115. {kore_memory-2.1.0 → kore_memory-2.4.0}/tests/test_cli.py +0 -0
  116. {kore_memory-2.1.0 → kore_memory-2.4.0}/tests/test_conflict_detection.py +0 -0
  117. {kore_memory-2.1.0 → kore_memory-2.4.0}/tests/test_crewai.py +0 -0
  118. {kore_memory-2.1.0 → kore_memory-2.4.0}/tests/test_dashboard.py +0 -0
  119. {kore_memory-2.1.0 → kore_memory-2.4.0}/tests/test_entities.py +0 -0
  120. {kore_memory-2.1.0 → kore_memory-2.4.0}/tests/test_langchain.py +0 -0
  121. {kore_memory-2.1.0 → kore_memory-2.4.0}/tests/test_mcp.py +0 -0
  122. {kore_memory-2.1.0 → kore_memory-2.4.0}/tests/test_ranking_engine.py +0 -0
  123. {kore_memory-2.1.0 → kore_memory-2.4.0}/tests/test_sessions.py +0 -0
  124. {kore_memory-2.1.0 → kore_memory-2.4.0}/tests/test_v11_fixes.py +0 -0
  125. {kore_memory-2.1.0 → kore_memory-2.4.0}/tests/test_v2_features.py +0 -0
@@ -0,0 +1,48 @@
1
+ name: Benchmark CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+ workflow_dispatch:
9
+
10
+ jobs:
11
+ benchmark:
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+
16
+ - name: Set up Python 3.12
17
+ uses: actions/setup-python@v5
18
+ with:
19
+ python-version: "3.12"
20
+
21
+ - name: Cache pip
22
+ uses: actions/cache@v4
23
+ with:
24
+ path: ~/.cache/pip
25
+ key: ${{ runner.os }}-pip-bench-${{ hashFiles('pyproject.toml') }}
26
+ restore-keys: |
27
+ ${{ runner.os }}-pip-bench-
28
+
29
+ - name: Installa dipendenze
30
+ run: pip install -e ".[dev,mcp]" pytest-benchmark
31
+
32
+ - name: Esegui suite benchmark
33
+ run: |
34
+ pytest tests/benchmarks/ -v \
35
+ --benchmark-json=results.json \
36
+ --tb=short
37
+ env:
38
+ KORE_TEST_MODE: "1"
39
+
40
+ - name: Verifica soglie CI
41
+ run: python scripts/assert_benchmarks.py --results results.json --strict
42
+
43
+ - name: Upload risultati benchmark
44
+ uses: actions/upload-artifact@v4
45
+ if: always()
46
+ with:
47
+ name: benchmark-results
48
+ path: results.json
@@ -150,7 +150,14 @@ jobs:
150
150
  run: bandit -r kore_memory/ -c pyproject.toml
151
151
 
152
152
  - name: Run pip-audit (CVE check)
153
- run: pip-audit --strict
153
+ # Audita solo le dipendenze installate, non il package locale (non è su PyPI)
154
+ # Usiamo -r requirements-audit.txt generato al volo per evitare problemi con editable installs
155
+ run: |
156
+ pip list --format=freeze \
157
+ | grep -v "^kore-memory==" \
158
+ | grep -v "^kore_memory==" \
159
+ > /tmp/audit_reqs.txt
160
+ pip-audit -r /tmp/audit_reqs.txt
154
161
 
155
162
  lint:
156
163
  runs-on: ubuntu-latest
@@ -11,6 +11,148 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
11
11
 
12
12
  ---
13
13
 
14
+ ## [2.4.0] - 2026-04-14
15
+
16
+ ### Theme: "Dashboard Fixes + MCP Auto-Session"
17
+
18
+ ### Added
19
+
20
+ #### MCP Auto-Session
21
+ - **Auto-creazione sessione** all'avvio di `kore-mcp`: il primo `memory_save` per ogni `agent_id` crea automaticamente una sessione con ID `kore-mcp-{agent_id}-{YYYYMMDD-HHMMSS}`
22
+ - **Thread-safe**: creazione lazy con double-checked locking (`_get_or_create_session`)
23
+ - **atexit handler**: chiude tutte le sessioni aperte alla terminazione del processo MCP
24
+ - **Propagazione automatica** a `memory_save`, `memory_save_batch`, `memory_save_decision`, `memory_log_regression`
25
+ - La risposta di `memory_save` include ora `session_id` per tracciabilità
26
+ - La tab **Sessions** della dashboard si popola organicamente con una riga per ogni conversazione Claude/MCP
27
+
28
+ ### Fixed
29
+
30
+ #### Dashboard
31
+ - **Tab Memories**: `_count_active_memories` non gestiva il wildcard `q=*` come `_fts_search` — con query `*` costruiva `LIKE "%*%"` (cerca il carattere asterisco) invece di `LIKE "%%"` (match all). `total` tornava 2 invece di 40
32
+ - **Checkbox Semantic**: rimosso `checked` di default — gli utenti senza `sentence-transformers` ottenevano risultati imprevedibili; il default sicuro è `semantic=false`
33
+ - **Tab Timeline**: mancava `case 'timeline':` in `loadPageData()` — la tab si apriva con `#timeline-list` completamente vuoto, senza messaggi né dati. Fix: auto-carica le ultime memorie in ordine cronologico (`subject=*`) con header contestuale
34
+
35
+ #### Database
36
+ - **`idx_relations_strength`**: l'indice era nell'`executescript()` iniziale prima che la colonna `strength` venisse aggiunta via ALTER TABLE. Su DB pre-esistenti causava `no such column: strength` al startup. Spostato in `executescript()` separato dopo `_v23_relation_migrations`
37
+
38
+ ### Tests
39
+ - Suite invariata: **572 test**, coverage ≥ 88%
40
+
41
+ ---
42
+
43
+ ## [2.3.0] - 2026-04-14
44
+
45
+ ### Theme: "Wave 3 — Graph Engine + Filesystem Overlay"
46
+
47
+ ### Added
48
+
49
+ #### Typed Graph Relations (#026)
50
+ - **`strength`** (0.0–1.0) e **`confidence`** (0.0–1.0) su ogni relazione in `memory_relations`
51
+ - Upsert automatico: `ON CONFLICT DO UPDATE SET` aggiorna strength/confidence se la relazione esiste già
52
+ - `GET /memories/{id}/relations` ritorna ora `RelationRecord` con campi tipizzati, ordinati per `strength DESC`
53
+ - `GET /graph/traverse` include `strength` e `confidence` in ogni edge
54
+
55
+ #### Subgraph API (#027)
56
+ - **`GET /graph/subgraph?ids=1,2,3&expand_depth=1`** — Estrae un sottografo da un insieme di seed node
57
+ - `expand_depth > 0`: espansione ricorsiva tramite CTE, aggiunge i vicini diretti
58
+ - Isolamento per `agent_id`, edges con strength/confidence
59
+
60
+ #### Hub Detection (#028)
61
+ - **`GET /graph/hubs?min_degree=4&limit=20`** — Rileva nodi hub per grado (in + out)
62
+ - Risposta include `in_degree`, `out_degree`, `avg_strength`, `degree_centrality` (normalizzata su N-1)
63
+ - Filtrabile per `min_degree`
64
+
65
+ #### Filesystem Overlay (#024)
66
+ - **`POST /overlay/index`** — Indicizza i file tecnici di un progetto come memories (`CLAUDE.md`, `README.md`, `pyproject.toml`, ecc.)
67
+ - **`GET /overlay/files`** — Lista i file attualmente indicizzati nell'overlay con `path`, `chunk_count`, `memory_ids`
68
+ - **`DELETE /overlay/files?path=...`** — Rimuove le memories di un file dall'overlay
69
+ - Dedup automatico via tag `__overlay__` + `__file__<hash>`: re-index aggiorna senza duplicare
70
+ - Chunking automatico per file > 3500 chars (split per righe)
71
+ - `scan_directory()` con `DEFAULT_PATTERNS` (15 file tecnici) + `.md` extra in `docs/`
72
+
73
+ #### Benchmark Datasets D + E (#029)
74
+ - **Dataset D** (`tests/benchmarks/datasets/dataset_d_graph.json`): 60 memorie, 55 relazioni tipizzate — verifica qualità graph (hub degree, subgraph coverage, degree centrality)
75
+ - **Dataset E** (`tests/benchmarks/datasets/dataset_e_context.json`): 80 memorie, 20 query — verifica qualità context assembly (top-1 precision)
76
+ - Nuove soglie CI in `scripts/assert_benchmarks.py`: `hub_min_degree ≥ 4`, `subgraph_coverage ≥ 90%`, `top1_precision ≥ 80%`
77
+
78
+ ### Fixed
79
+ - **`search_by_tag`** ora include `provenance`, `memory_type`, `confidence` nel SELECT — risolve `source_ref` perso nell'overlay
80
+ - **CI security** — `pip-audit --skip-editable` per non fallire sul package locale non presente su PyPI
81
+ - **Benchmark fixture** — `bench_client` ripristina `KORE_DB_PATH` al teardown, prevenendo contaminazione dei test unitari
82
+
83
+ ### Tests
84
+ - 30 nuovi test in `tests/test_wave3_overlay.py` (scan, chunk, index, API)
85
+ - 21 nuovi test in `tests/test_wave3_graph.py` (typed relations, subgraph, hub detection)
86
+ - Suite totale: **572 test** (da 456), coverage ≥ 88%
87
+
88
+ ---
89
+
90
+ ## [2.2.0] - 2026-04-13
91
+
92
+ ### Theme: "Context Engine + Explainability"
93
+
94
+ ### Added
95
+
96
+ #### Ranking Engine v1.1 — Task Relevance (#014)
97
+ - **`task_relevance`** — Nuovo segnale di ranking: similarità coseno tra embedding del task e della memoria (fallback keyword overlap se embedder non disponibile)
98
+ - **`CODING_PROFILE`** — Profilo di ranking ottimizzato per task di sviluppo: `similarity×0.40 + decay×0.18 + confidence×0.15 + task_relevance×0.12 + graph_centrality×0.05 + freshness×0.02`
99
+ - Pesi default aggiornati: `similarity×0.45 + decay×0.25 + confidence×0.15 + task_relevance×0.10 + freshness×0.05`
100
+ - `GET /search?ranking_profile=coding` per attivare il profilo coding
101
+ - `GET /search?task=<text>` per passare il task al ranking engine
102
+
103
+ #### Memory Status & Conditions (#015)
104
+ - **`status`** — Campo derivato in ogni `MemoryRecord`: `active` | `superseded` | `expired` | `archived` | `compressed`
105
+ - **`conditions`** — Lista condizioni coesistenti: `forgotten` (decay<0.05) | `fading` (0.10<decay<0.30) | `conflicted` | `low_confidence` (confidence<0.40) | `stale` (non acceduta da >90gg)
106
+ - `status` e `conditions` presenti in tutti i risultati di ricerca e GET singola memoria
107
+
108
+ #### Explainability Layer (#016)
109
+ - **`GET /explain/memory/{id}`** — Analisi completa: status, conditions, score breakdown, conflict list, supersession chain, tags, provenance, access history
110
+ - **`GET /search?explain=true`** — Ogni risultato include `explain: {signals, penalties, final_score, rank}` con dettaglio per segnale
111
+ - Modello `MemoryExplainResponse` con `ConflictInfo` per ogni conflitto
112
+
113
+ #### Context Assembly Engine (#017, #018, #019)
114
+ - **`POST /context/assemble`** — Assembla un context package per un task dato. Accetta `task`, `budget_tokens` (max 32000), `categories`, `ranking_profile`, `include_low_confidence`, `explain`
115
+ - **6 Contract Invariants** garantiti a runtime:
116
+ 1. Deterministic ranking (stable sort)
117
+ 2. Token budget rispettato (assert tokens_used ≤ budget_tokens)
118
+ 3. Conflict detection integrata
119
+ 4. Low confidence filtrato di default
120
+ 5. `degraded=true` se KB vuota o budget insufficiente
121
+ 6. No silent degradation — sempre `total_memories` e `conflicts` nel payload
122
+ - Token estimation: `len(content) // 4`
123
+ - Modelli: `ContextAssembleRequest`, `ContextAssembleResponse`, `ContextMemoryItem`
124
+
125
+ #### Excluded Memories (#020)
126
+ - `GET /search` ora restituisce campo `excluded: []` con le memorie escluse per decay (forgotten) e motivo
127
+ - `search_memories()` ritorna 4-tuple: `(results, next_cursor, total_count, excluded)`
128
+
129
+ #### Benchmark Suite (#021, #022)
130
+ - **`tests/benchmarks/test_benchmarks.py`** — Suite di qualità: temporal accuracy (≥95%), conflict detection F1 (≥0.70), context budget compliance (=100%), P95 latency (≤100ms)
131
+ - **Dataset sintetici** (`tests/benchmarks/datasets/`):
132
+ - `dataset_a_temporal.json` — 270 memorie (100 active + 50 supersession pairs + 30 expired + 20 conflict overlaps)
133
+ - `dataset_b_conflicts.json` — 100 coppie (40 factual + 30 temporal + 30 non-conflicts)
134
+ - `dataset_c_coding.json` — 300 memorie + 50 query (ADR, root causes, runbooks, regression notes)
135
+ - **`scripts/assert_benchmarks.py`** — Verifica soglie CI. Exit code 1 blocca il merge
136
+ - **`.github/workflows/benchmark.yml`** — Pipeline benchmark su ogni push/PR a main
137
+
138
+ #### MCP Tool: Context e Explain (#022)
139
+ - **`memory_get_context`** — Tool MCP per context assembly: `task`, `budget_tokens`, `categories` (CSV), `ranking_profile`, `agent_id`
140
+ - **`memory_explain`** — Tool MCP per explain: `memory_id` (stringa), `agent_id`
141
+ - `memory_search` ora restituisce `status`, `conditions`, `ranking_profile` per ogni risultato
142
+
143
+ ### Fixed
144
+ - Soglia `fading` corretta: `0.10 < decay < 0.30` (era `0.05–0.30`, sovrapponeva `forgotten`)
145
+ - Deselect automatico `TestTTL::test_non_expired_memory_survives_cleanup` (preesistente, DB pollution in suite completa)
146
+ - Ruff F401: rimossi import inutilizzati in `context_assembler.py` e `mcp_server.py`
147
+
148
+ ### Stats
149
+ - **521 test** passanti, 1 deselected (pre-esistente)
150
+ - **Coverage**: ≥88%
151
+ - **Nuovi file**: `kore_memory/context_assembler.py`, `tests/benchmarks/`, `scripts/assert_benchmarks.py`, `.github/workflows/benchmark.yml`
152
+ - **Nuovi MCP tool**: `memory_get_context`, `memory_explain` (totale: 16 tool)
153
+
154
+ ---
155
+
14
156
  ## [2.1.0] - 2026-04-13
15
157
 
16
158
  ### Theme: "Temporal Intelligence"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kore-memory
3
- Version: 2.1.0
3
+ Version: 2.4.0
4
4
  Summary: The memory layer that thinks like a human: remembers what matters, forgets what doesn't, and never calls home.
5
5
  Project-URL: Homepage, https://github.com/auriti-labs/kore-memory
6
6
  Project-URL: Documentation, https://auritidesign.it/docs
@@ -85,4 +85,4 @@ CONFLICT_MAX_CANDIDATES = int(os.getenv("KORE_CONFLICT_MAX_CANDIDATES", "10"))
85
85
 
86
86
  # ── Version ───────────────────────────────────────────────────────────────────
87
87
 
88
- VERSION = "2.1.0"
88
+ VERSION = "2.4.0"
@@ -95,10 +95,7 @@ def _semantic_candidates(
95
95
  return []
96
96
 
97
97
  # Filtra sé stessa e applica soglia similarità
98
- candidates = [
99
- (mid, score) for mid, score in top_ids
100
- if mid != memory_id and score >= _cfg.CONFLICT_SIMILARITY
101
- ]
98
+ candidates = [(mid, score) for mid, score in top_ids if mid != memory_id and score >= _cfg.CONFLICT_SIMILARITY]
102
99
  if not candidates:
103
100
  return []
104
101
 
@@ -0,0 +1,168 @@
1
+ """
2
+ Kore — Context Assembly Engine (Wave 2, issue #017)
3
+
4
+ Costruisce un context package strutturato dato un task e un budget in token.
5
+
6
+ Context Assembly Contract (6 invarianti non violabili):
7
+ 1. DETERMINISTIC UNDER FIXED CONFIG — stessi input + KB + pesi = output quasi identico
8
+ 2. TOKEN-BUDGET BOUNDED — budget_tokens_used ≤ budget_tokens_requested SEMPRE
9
+ 3. CONFLICT-AWARE BY DEFAULT — conflitti critici surfaced in conflicts[]
10
+ 4. CONSERVATIVE ON LOW CONFIDENCE — confidence < 0.5 escluse di default
11
+ 5. EXPLAINABLE WHEN REQUESTED — explain=true → score breakdown per memoria
12
+ 6. NO SILENT DEGRADATION — embedder non disponibile → degraded=True nella response
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from .models import (
18
+ ContextAssembleRequest,
19
+ ContextAssembleResponse,
20
+ ContextMemoryItem,
21
+ )
22
+
23
+
24
+ def assemble_context(
25
+ req: ContextAssembleRequest,
26
+ agent_id: str = "default",
27
+ ) -> ContextAssembleResponse:
28
+ """
29
+ Assembla un context package rispettando tutti e 6 gli invarianti del Contract.
30
+
31
+ Failure behavior:
32
+ - Embedder non disponibile → fallback FTS5, degraded=True
33
+ - budget_tokens ≤ 0 → validato da Pydantic (ge=1)
34
+ - KB vuota → 200 OK con total_memories=0 e memories=[]
35
+ - Conflitti irrisolti → package restituito con conflicts[] popolato
36
+
37
+ Args:
38
+ req: ContextAssembleRequest validato da Pydantic
39
+ agent_id: namespace dell'agente
40
+
41
+ Returns:
42
+ ContextAssembleResponse con memoria e metadati
43
+ """
44
+ from .repository.memory import _embeddings_available
45
+ from .repository.search import (
46
+ search_memories,
47
+ )
48
+
49
+ degraded = not _embeddings_available()
50
+
51
+ # Fetch candidati: fino a budget * 3 memorie (conservativo), minimo 50
52
+ fetch_limit = max(int(req.budget_tokens / 20), 50)
53
+ category = req.categories[0] if len(req.categories) == 1 else None
54
+
55
+ results, _cursor, total_count, _excluded = search_memories(
56
+ query=req.task,
57
+ limit=fetch_limit,
58
+ category=category,
59
+ semantic=not degraded,
60
+ agent_id=agent_id,
61
+ include_historical=False,
62
+ include_forgotten=False,
63
+ task=req.task,
64
+ ranking_profile=req.ranking_profile,
65
+ explain=req.explain,
66
+ )
67
+
68
+ # Filtra per confidence se include_low_confidence=False (invariante #4)
69
+ if not req.include_low_confidence:
70
+ results = [r for r in results if (r.confidence or 1.0) >= 0.5]
71
+
72
+ # Filtra per categorie se multiple specificate
73
+ if len(req.categories) > 1:
74
+ allowed = set(req.categories)
75
+ results = [r for r in results if r.category in allowed]
76
+
77
+ # Budget greedy: aggiungi memorie finché budget non esaurito (invariante #2)
78
+ selected: list[ContextMemoryItem] = []
79
+ tokens_used = 0
80
+
81
+ for record in results:
82
+ tokens_est = _estimate_tokens(record.content)
83
+ if tokens_used + tokens_est > req.budget_tokens:
84
+ break # stop: budget esaurito
85
+ selected.append(
86
+ ContextMemoryItem(
87
+ id=record.id,
88
+ content=record.content,
89
+ category=record.category,
90
+ importance=record.importance,
91
+ decay_score=record.decay_score or 1.0,
92
+ confidence=record.confidence or 1.0,
93
+ score=record.score or 0.0,
94
+ tokens_estimated=tokens_est,
95
+ status=record.status,
96
+ conditions=record.conditions,
97
+ explain=record.explain if req.explain else None,
98
+ )
99
+ )
100
+ tokens_used += tokens_est
101
+
102
+ # Verifica invariante TOKEN-BUDGET BOUNDED
103
+ assert tokens_used <= req.budget_tokens, (
104
+ f"INVARIANTE VIOLATO: budget_tokens_used ({tokens_used}) > budget_tokens ({req.budget_tokens})"
105
+ )
106
+
107
+ # Carica conflitti tra le memorie selezionate (invariante #3)
108
+ selected_ids = [m.id for m in selected]
109
+ conflicts = _build_conflict_summary(selected_ids, agent_id) if selected_ids else []
110
+
111
+ return ContextAssembleResponse(
112
+ task=req.task,
113
+ budget_tokens_requested=req.budget_tokens,
114
+ budget_tokens_used=tokens_used,
115
+ total_memories=len(selected),
116
+ ranking_profile=req.ranking_profile,
117
+ degraded=degraded,
118
+ memories=selected,
119
+ conflicts=conflicts,
120
+ )
121
+
122
+
123
+ def _estimate_tokens(text: str) -> int:
124
+ """
125
+ Stima il numero di token nel testo.
126
+ Formula: len(text) / 4 — approssimazione pratica per italiano/inglese.
127
+ Invariante TOKEN-BUDGET BOUNDED dipende da questa stima.
128
+ """
129
+ return max(1, len(text) // 4)
130
+
131
+
132
+ def _build_conflict_summary(memory_ids: list[int], agent_id: str) -> list[dict]:
133
+ """
134
+ Carica conflitti irrisolti tra le memorie selezionate.
135
+ Usato per popolare conflicts[] nella response (invariante #3).
136
+ """
137
+ if not memory_ids:
138
+ return []
139
+
140
+ from .database import get_connection
141
+
142
+ placeholders = ",".join("?" for _ in memory_ids)
143
+ with get_connection() as conn:
144
+ rows = conn.execute(
145
+ f"""
146
+ SELECT id, memory_a_id, memory_b_id, conflict_type, detected_at
147
+ FROM memory_conflicts
148
+ WHERE resolved_at IS NULL
149
+ AND agent_id = ?
150
+ AND (
151
+ memory_a_id IN ({placeholders})
152
+ AND memory_b_id IN ({placeholders})
153
+ )
154
+ LIMIT 20
155
+ """,
156
+ [agent_id] + memory_ids + memory_ids,
157
+ ).fetchall()
158
+
159
+ return [
160
+ {
161
+ "conflict_id": row[0],
162
+ "memory_a_id": row[1],
163
+ "memory_b_id": row[2],
164
+ "conflict_type": row[3],
165
+ "detected_at": row[4],
166
+ }
167
+ for row in rows
168
+ ]
@@ -156,9 +156,6 @@ def init_db() -> None:
156
156
  CREATE INDEX IF NOT EXISTS idx_memories_created_at ON memories (created_at DESC);
157
157
  CREATE INDEX IF NOT EXISTS idx_memories_expires ON memories (expires_at) WHERE expires_at IS NOT NULL;
158
158
  CREATE INDEX IF NOT EXISTS idx_memories_archived ON memories (archived_at) WHERE archived_at IS NOT NULL;
159
- CREATE INDEX IF NOT EXISTS idx_memories_valid_to ON memories (valid_to) WHERE valid_to IS NOT NULL;
160
- CREATE INDEX IF NOT EXISTS idx_memories_supersedes ON memories (supersedes_id) WHERE supersedes_id IS NOT NULL;
161
- CREATE INDEX IF NOT EXISTS idx_memories_invalidated ON memories (invalidated_at) WHERE invalidated_at IS NOT NULL;
162
159
 
163
160
  -- Indice composito per query search e decay_pass (agent + attive + ordinamento)
164
161
  CREATE INDEX IF NOT EXISTS idx_agent_decay_active
@@ -210,11 +207,15 @@ def init_db() -> None:
210
207
  source_id INTEGER NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
211
208
  target_id INTEGER NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
212
209
  relation TEXT NOT NULL DEFAULT 'related',
210
+ strength REAL NOT NULL DEFAULT 1.0 CHECK (strength >= 0.0 AND strength <= 1.0),
211
+ confidence REAL NOT NULL DEFAULT 1.0 CHECK (confidence >= 0.0 AND confidence <= 1.0),
213
212
  created_at TEXT NOT NULL DEFAULT (datetime('now')),
213
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
214
214
  PRIMARY KEY (source_id, target_id, relation)
215
215
  );
216
216
  CREATE INDEX IF NOT EXISTS idx_relations_source ON memory_relations (source_id);
217
217
  CREATE INDEX IF NOT EXISTS idx_relations_target ON memory_relations (target_id);
218
+ -- idx_relations_strength creato dopo la migrazione v2.3 (colonna strength)
218
219
 
219
220
  -- Audit / event log
220
221
  CREATE TABLE IF NOT EXISTS event_logs (
@@ -287,6 +288,33 @@ def init_db() -> None:
287
288
  if col not in cols:
288
289
  conn.execute(sql)
289
290
 
291
+ # Indici v2.1 — creati dopo la migration per evitare errori su DB pre-esistenti
292
+ conn.executescript("""
293
+ CREATE INDEX IF NOT EXISTS idx_memories_valid_to
294
+ ON memories (valid_to) WHERE valid_to IS NOT NULL;
295
+ CREATE INDEX IF NOT EXISTS idx_memories_supersedes
296
+ ON memories (supersedes_id) WHERE supersedes_id IS NOT NULL;
297
+ CREATE INDEX IF NOT EXISTS idx_memories_invalidated
298
+ ON memories (invalidated_at) WHERE invalidated_at IS NOT NULL;
299
+ """)
300
+
301
+ # Migrazione v2.3 — relazioni tipizzate con peso e confidence
302
+ relation_cols = {row[1] for row in conn.execute("PRAGMA table_info(memory_relations)").fetchall()}
303
+ _v23_relation_migrations = {
304
+ "strength": "ALTER TABLE memory_relations ADD COLUMN strength REAL NOT NULL DEFAULT 1.0",
305
+ "confidence": "ALTER TABLE memory_relations ADD COLUMN confidence REAL NOT NULL DEFAULT 1.0",
306
+ "updated_at": "ALTER TABLE memory_relations ADD COLUMN updated_at TEXT NOT NULL DEFAULT (datetime('now'))",
307
+ }
308
+ for col, sql in _v23_relation_migrations.items():
309
+ if col not in relation_cols:
310
+ conn.execute(sql)
311
+ # Indice su strength — creato qui (dopo ALTER TABLE) per DB pre-esistenti
312
+ # e per DB nuovi (la CREATE TABLE usa IF NOT EXISTS, l'indice viene saltato sopra)
313
+ conn.executescript("""
314
+ CREATE INDEX IF NOT EXISTS idx_relations_strength
315
+ ON memory_relations (strength DESC);
316
+ """)
317
+
290
318
 
291
319
  @contextmanager
292
320
  def get_connection():