epimneme 0.7.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.
- epimneme-0.7.0/.dockerignore +10 -0
- epimneme-0.7.0/.env.example +14 -0
- epimneme-0.7.0/.github/workflows/ci.yml +98 -0
- epimneme-0.7.0/.gitignore +43 -0
- epimneme-0.7.0/AI_TOOLBOX.md +275 -0
- epimneme-0.7.0/ARCHITECTURE.md +208 -0
- epimneme-0.7.0/CHANGELOG.md +34 -0
- epimneme-0.7.0/CONTRIBUTING.md +64 -0
- epimneme-0.7.0/Dockerfile +41 -0
- epimneme-0.7.0/ENGRAM2_HIGH_RECALL_SPEC.md +390 -0
- epimneme-0.7.0/LICENSE +21 -0
- epimneme-0.7.0/Makefile +105 -0
- epimneme-0.7.0/NOTICE +29 -0
- epimneme-0.7.0/PKG-INFO +242 -0
- epimneme-0.7.0/README.md +211 -0
- epimneme-0.7.0/SECURITY.md +37 -0
- epimneme-0.7.0/benchmarks/BENCHMARK_RESULTS.md +503 -0
- epimneme-0.7.0/benchmarks/DATA.md +59 -0
- epimneme-0.7.0/benchmarks/__init__.py +0 -0
- epimneme-0.7.0/benchmarks/ablation_beam.sh +190 -0
- epimneme-0.7.0/benchmarks/analyze_pref_misses.py +55 -0
- epimneme-0.7.0/benchmarks/beam_bench.py +568 -0
- epimneme-0.7.0/benchmarks/beir_bench.py +502 -0
- epimneme-0.7.0/benchmarks/compare_pref.py +37 -0
- epimneme-0.7.0/benchmarks/compare_runs.py +137 -0
- epimneme-0.7.0/benchmarks/convomem_bench.py +804 -0
- epimneme-0.7.0/benchmarks/convomem_diagnose.py +177 -0
- epimneme-0.7.0/benchmarks/data/test_beam_fixture.json +110 -0
- epimneme-0.7.0/benchmarks/data/test_lme_fixture.json +48 -0
- epimneme-0.7.0/benchmarks/data/test_locomo_fixture.json +50 -0
- epimneme-0.7.0/benchmarks/download_data.sh +37 -0
- epimneme-0.7.0/benchmarks/epimneme_client.py +209 -0
- epimneme-0.7.0/benchmarks/lme_e2e_bench.py +760 -0
- epimneme-0.7.0/benchmarks/lme_report.py +95 -0
- epimneme-0.7.0/benchmarks/locomo_bench.py +452 -0
- epimneme-0.7.0/benchmarks/longmemeval_bench.py +590 -0
- epimneme-0.7.0/benchmarks/mabench_bench.py +506 -0
- epimneme-0.7.0/benchmarks/metrics.py +86 -0
- epimneme-0.7.0/benchmarks/requirements.txt +2 -0
- epimneme-0.7.0/benchmarks/run_bench.sh +166 -0
- epimneme-0.7.0/docker-compose.example.yml +94 -0
- epimneme-0.7.0/pyproject.toml +58 -0
- epimneme-0.7.0/scripts/backup.sh +46 -0
- epimneme-0.7.0/scripts/restore.sh +43 -0
- epimneme-0.7.0/src/epimneme/__init__.py +3 -0
- epimneme-0.7.0/src/epimneme/activity.py +171 -0
- epimneme-0.7.0/src/epimneme/auth.py +190 -0
- epimneme-0.7.0/src/epimneme/backup.py +684 -0
- epimneme-0.7.0/src/epimneme/bulk_import.py +496 -0
- epimneme-0.7.0/src/epimneme/core/__init__.py +1 -0
- epimneme-0.7.0/src/epimneme/core/config.py +249 -0
- epimneme-0.7.0/src/epimneme/core/models.py +450 -0
- epimneme-0.7.0/src/epimneme/dashboard.py +11 -0
- epimneme-0.7.0/src/epimneme/dateutil.py +134 -0
- epimneme-0.7.0/src/epimneme/decay.py +82 -0
- epimneme-0.7.0/src/epimneme/dedup.py +127 -0
- epimneme-0.7.0/src/epimneme/fusion.py +1185 -0
- epimneme-0.7.0/src/epimneme/logging.py +40 -0
- epimneme-0.7.0/src/epimneme/manage.py +269 -0
- epimneme-0.7.0/src/epimneme/manager.py +1140 -0
- epimneme-0.7.0/src/epimneme/maxsim.py +194 -0
- epimneme-0.7.0/src/epimneme/migrations/001_migrate_pseudo_entities.py +65 -0
- epimneme-0.7.0/src/epimneme/migrations/002_add_decay_versioning_dedup.py +54 -0
- epimneme-0.7.0/src/epimneme/migrations/003_add_pinned_field.py +23 -0
- epimneme-0.7.0/src/epimneme/migrations/004_add_project_persistent.py +23 -0
- epimneme-0.7.0/src/epimneme/migrations/005_add_session_ordinal.py +55 -0
- epimneme-0.7.0/src/epimneme/migrations/__init__.py +5 -0
- epimneme-0.7.0/src/epimneme/migrations/runner.py +102 -0
- epimneme-0.7.0/src/epimneme/ratelimit.py +122 -0
- epimneme-0.7.0/src/epimneme/reflection.py +361 -0
- epimneme-0.7.0/src/epimneme/rerank.py +142 -0
- epimneme-0.7.0/src/epimneme/scheduler.py +166 -0
- epimneme-0.7.0/src/epimneme/server.py +1792 -0
- epimneme-0.7.0/src/epimneme/skills.py +466 -0
- epimneme-0.7.0/src/epimneme/stores/__init__.py +1 -0
- epimneme-0.7.0/src/epimneme/stores/postgresql.py +2247 -0
- epimneme-0.7.0/src/epimneme/templates/dashboard.html +1803 -0
- epimneme-0.7.0/src/epimneme/textlog.py +62 -0
- epimneme-0.7.0/tests/__init__.py +0 -0
- epimneme-0.7.0/tests/conftest.py +187 -0
- epimneme-0.7.0/tests/test_activity.py +237 -0
- epimneme-0.7.0/tests/test_auth.py +98 -0
- epimneme-0.7.0/tests/test_backup.py +966 -0
- epimneme-0.7.0/tests/test_bulk_import.py +197 -0
- epimneme-0.7.0/tests/test_config.py +178 -0
- epimneme-0.7.0/tests/test_decay.py +179 -0
- epimneme-0.7.0/tests/test_dedup.py +182 -0
- epimneme-0.7.0/tests/test_fusion.py +227 -0
- epimneme-0.7.0/tests/test_hardening.py +177 -0
- epimneme-0.7.0/tests/test_integration.py +552 -0
- epimneme-0.7.0/tests/test_logging.py +109 -0
- epimneme-0.7.0/tests/test_manager.py +547 -0
- epimneme-0.7.0/tests/test_migrations.py +87 -0
- epimneme-0.7.0/tests/test_models.py +160 -0
- epimneme-0.7.0/tests/test_pagination.py +119 -0
- epimneme-0.7.0/tests/test_ratelimit.py +87 -0
- epimneme-0.7.0/tests/test_reflection.py +474 -0
- epimneme-0.7.0/tests/test_rerank.py +88 -0
- epimneme-0.7.0/tests/test_scheduler.py +236 -0
- epimneme-0.7.0/tests/test_server.py +420 -0
- epimneme-0.7.0/tests/test_skills.py +199 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Engram environment variables — copy to .env and edit
|
|
2
|
+
# ------------------------------------------------------------------
|
|
3
|
+
# REQUIRED: PostgreSQL password (must not be the literal "engram")
|
|
4
|
+
ENGRAM_PG_PASSWORD=change_me_to_a_strong_random_string
|
|
5
|
+
|
|
6
|
+
# Domain suffix used by the Traefik labels in docker-compose.yml
|
|
7
|
+
# (e.g. DOMAINNAME=example.com produces engram.example.com)
|
|
8
|
+
DOMAINNAME=example.com
|
|
9
|
+
|
|
10
|
+
# Timezone (see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones)
|
|
11
|
+
TZ=UTC
|
|
12
|
+
|
|
13
|
+
# Optional: set to "1" to allow running with the default password (local dev only)
|
|
14
|
+
# ENGRAM_DEMO_MODE=0
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
env:
|
|
10
|
+
PYTHON_VERSION: "3.12"
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
lint:
|
|
14
|
+
name: Lint
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- uses: actions/setup-python@v5
|
|
20
|
+
with:
|
|
21
|
+
python-version: ${{ env.PYTHON_VERSION }}
|
|
22
|
+
|
|
23
|
+
- name: Install ruff
|
|
24
|
+
run: pip install ruff
|
|
25
|
+
|
|
26
|
+
- name: Ruff check
|
|
27
|
+
run: ruff check src/ tests/
|
|
28
|
+
|
|
29
|
+
- name: Ruff format check
|
|
30
|
+
run: ruff format --check src/ tests/
|
|
31
|
+
|
|
32
|
+
test:
|
|
33
|
+
name: Test
|
|
34
|
+
runs-on: ubuntu-latest
|
|
35
|
+
needs: lint
|
|
36
|
+
|
|
37
|
+
services:
|
|
38
|
+
postgres:
|
|
39
|
+
image: pgvector/pgvector:pg16
|
|
40
|
+
env:
|
|
41
|
+
POSTGRES_USER: engram
|
|
42
|
+
POSTGRES_PASSWORD: engram_test
|
|
43
|
+
POSTGRES_DB: engram_test
|
|
44
|
+
ports:
|
|
45
|
+
- 5432:5432
|
|
46
|
+
options: >-
|
|
47
|
+
--health-cmd "pg_isready -U engram"
|
|
48
|
+
--health-interval 5s
|
|
49
|
+
--health-timeout 5s
|
|
50
|
+
--health-retries 10
|
|
51
|
+
|
|
52
|
+
steps:
|
|
53
|
+
- uses: actions/checkout@v4
|
|
54
|
+
|
|
55
|
+
- uses: actions/setup-python@v5
|
|
56
|
+
with:
|
|
57
|
+
python-version: ${{ env.PYTHON_VERSION }}
|
|
58
|
+
|
|
59
|
+
- name: Install dependencies
|
|
60
|
+
run: |
|
|
61
|
+
pip install --upgrade pip
|
|
62
|
+
pip install .[dev]
|
|
63
|
+
|
|
64
|
+
- name: Run unit tests
|
|
65
|
+
run: pytest tests/ -x -v --tb=short
|
|
66
|
+
|
|
67
|
+
- name: Run unit tests with coverage
|
|
68
|
+
run: |
|
|
69
|
+
pip install pytest-cov
|
|
70
|
+
pytest tests/ --cov=engram --cov-report=term-missing --cov-report=xml
|
|
71
|
+
|
|
72
|
+
- name: Upload coverage
|
|
73
|
+
if: always()
|
|
74
|
+
uses: actions/upload-artifact@v4
|
|
75
|
+
with:
|
|
76
|
+
name: coverage-report
|
|
77
|
+
path: coverage.xml
|
|
78
|
+
|
|
79
|
+
docker:
|
|
80
|
+
name: Docker Build
|
|
81
|
+
runs-on: ubuntu-latest
|
|
82
|
+
needs: test
|
|
83
|
+
if: github.ref == 'refs/heads/main'
|
|
84
|
+
steps:
|
|
85
|
+
- uses: actions/checkout@v4
|
|
86
|
+
|
|
87
|
+
- name: Build Docker image
|
|
88
|
+
run: docker build -t engram:ci .
|
|
89
|
+
|
|
90
|
+
- name: Smoke test container
|
|
91
|
+
run: |
|
|
92
|
+
docker run -d --name engram-smoke \
|
|
93
|
+
-e ENGRAM_PG_HOST=host.docker.internal \
|
|
94
|
+
-e ENGRAM_EMBEDDINGS_ENABLED=0 \
|
|
95
|
+
engram:ci || true
|
|
96
|
+
sleep 3
|
|
97
|
+
docker logs engram-smoke 2>&1 | head -20
|
|
98
|
+
docker rm -f engram-smoke || true
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Database volumes
|
|
2
|
+
db-data/
|
|
3
|
+
|
|
4
|
+
# Python
|
|
5
|
+
__pycache__/
|
|
6
|
+
*.pyc
|
|
7
|
+
*.pyo
|
|
8
|
+
*.egg-info/
|
|
9
|
+
dist/
|
|
10
|
+
build/
|
|
11
|
+
.eggs/
|
|
12
|
+
|
|
13
|
+
# Virtual environments
|
|
14
|
+
.venv/
|
|
15
|
+
venv/
|
|
16
|
+
|
|
17
|
+
# IDE
|
|
18
|
+
.vscode/
|
|
19
|
+
.idea/
|
|
20
|
+
|
|
21
|
+
# Local deployment overrides (copy from docker-compose.example.yml)
|
|
22
|
+
docker-compose.yml
|
|
23
|
+
docker-compose.override.yml
|
|
24
|
+
.env
|
|
25
|
+
|
|
26
|
+
# Backups
|
|
27
|
+
backups/
|
|
28
|
+
|
|
29
|
+
# Activity logs
|
|
30
|
+
logs/
|
|
31
|
+
|
|
32
|
+
# Benchmark data — fetched by benchmarks/download_data.sh
|
|
33
|
+
benchmarks/data/**
|
|
34
|
+
!benchmarks/data/
|
|
35
|
+
!benchmarks/data/test_*_fixture.json
|
|
36
|
+
|
|
37
|
+
# Benchmark run logs, raw result files, and tuning sweeps
|
|
38
|
+
benchmarks/*.log
|
|
39
|
+
benchmarks/results_*.json
|
|
40
|
+
benchmarks/results_*.jsonl
|
|
41
|
+
benchmarks/results_*.tsv
|
|
42
|
+
benchmarks/near_miss_analysis*.txt
|
|
43
|
+
benchmarks/sweep*
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
# Engram AI Toolbox
|
|
2
|
+
|
|
3
|
+
Reference for agents running benchmarks, tuning config, and interpreting results.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Running Benchmarks
|
|
8
|
+
|
|
9
|
+
Always use the wrapper script — it captures git hash, container config, and version automatically:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
cd /docker/appdata/engram
|
|
13
|
+
|
|
14
|
+
# LongMemEval (500q, ~40min with MiniLM, ~55hr with BGE-large)
|
|
15
|
+
nohup ./benchmarks/run_bench.sh lme <version_tag> > /dev/null 2>&1 &
|
|
16
|
+
|
|
17
|
+
# LME with pre-staged data (~2min query-only — see Pre-Staging section below)
|
|
18
|
+
nohup ./benchmarks/run_bench.sh lme <version_tag> --skip-ingest > /dev/null 2>&1 &
|
|
19
|
+
|
|
20
|
+
# LoCoMo (10 conversations, ~3min; ~30sec with --skip-ingest)
|
|
21
|
+
nohup ./benchmarks/run_bench.sh locomo <version_tag> > /dev/null 2>&1 &
|
|
22
|
+
|
|
23
|
+
# MABench FactConsolidation (~30sec)
|
|
24
|
+
./benchmarks/run_bench.sh mabench <version_tag>
|
|
25
|
+
|
|
26
|
+
# BEAM 100K (~10min; ~2min with --skip-ingest)
|
|
27
|
+
nohup ./benchmarks/run_bench.sh beam <version_tag> --split 100K > /dev/null 2>&1 &
|
|
28
|
+
|
|
29
|
+
# ConvoMem (sample=50 per category, ~20min)
|
|
30
|
+
nohup ./benchmarks/run_bench.sh convomem <version_tag> > /dev/null 2>&1 &
|
|
31
|
+
|
|
32
|
+
# BEIR SciFact (~46min full; ~46min query-only with --skip-ingest)
|
|
33
|
+
# Full corpus: 5,183 docs; 300 test queries; metric: nDCG@10
|
|
34
|
+
nohup ./benchmarks/run_bench.sh beir <version_tag> --dataset scifact --no-cleanup > /dev/null 2>&1 &
|
|
35
|
+
|
|
36
|
+
# BEIR skip-ingest (corpus already staged as _beir_scifact project)
|
|
37
|
+
nohup ./benchmarks/run_bench.sh beir <version_tag> --dataset scifact --skip-ingest > /dev/null 2>&1 &
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Logs go to `/tmp/bench_<bench>_<tag>.log`. Results go to `benchmarks/results_engram_<bench>_<tag>_<timestamp>.*`.
|
|
41
|
+
|
|
42
|
+
All runs are appended to `benchmarks/results_history.tsv` for easy comparison.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Pre-Staging Benchmark Data (Fast Iteration)
|
|
47
|
+
|
|
48
|
+
Ingest dominates benchmark runtime. Once staged, data lives in isolated projects with zero
|
|
49
|
+
leakage between questions/conversations. Subsequent runs skip ingest and go straight to queries.
|
|
50
|
+
|
|
51
|
+
| Benchmark | Normal runtime | Query-only runtime | Project pattern |
|
|
52
|
+
|---|---|---|---|
|
|
53
|
+
| LME (500q) | ~40min | ~2min | `_lme_bench_<question_id>` |
|
|
54
|
+
| LoCoMo (10 convs) | ~3min | ~30sec | `_locomo_bench_<sample_id>` |
|
|
55
|
+
| BEAM 100K | ~10min | ~2min | `_beam_bench_<conv_id>` |
|
|
56
|
+
| MABench | ~30sec | — | not worth staging |
|
|
57
|
+
| ConvoMem | ~20min | ❌ | uses timestamp in project name, can't match |
|
|
58
|
+
| BEIR SciFact | ~50min (ingest ~4min, queries ~46min) | ~46min | `_beir_scifact` |
|
|
59
|
+
|
|
60
|
+
**Workflow:**
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
cd /docker/appdata/engram
|
|
64
|
+
|
|
65
|
+
# Step 1: Stage each benchmark once (~40min for LME, then done).
|
|
66
|
+
# Run this after any model/chunk-size change that invalidates existing embeddings.
|
|
67
|
+
python3 -u benchmarks/longmemeval_bench.py \
|
|
68
|
+
benchmarks/data/longmemeval_s_cleaned.json \
|
|
69
|
+
--granularity turn-pair --workers 4 \
|
|
70
|
+
--engram-url http://192.168.90.45:8000 --no-cleanup > /tmp/lme_prestage.log 2>&1 &
|
|
71
|
+
|
|
72
|
+
python3 -u benchmarks/locomo_bench.py \
|
|
73
|
+
benchmarks/data/locomo10.json \
|
|
74
|
+
--granularity dialog --engram-url http://192.168.90.45:8000 --no-cleanup
|
|
75
|
+
|
|
76
|
+
python3 -u benchmarks/beam_bench.py \
|
|
77
|
+
--split 100K --engram-url http://192.168.90.45:8000 --no-cleanup
|
|
78
|
+
|
|
79
|
+
# Step 2: All subsequent runs — just add --skip-ingest.
|
|
80
|
+
# --skip-ingest checks which projects already exist, auto-disables cleanup.
|
|
81
|
+
nohup ./benchmarks/run_bench.sh lme <version_tag> --skip-ingest > /dev/null 2>&1 &
|
|
82
|
+
nohup ./benchmarks/run_bench.sh locomo <version_tag> --skip-ingest > /dev/null 2>&1 &
|
|
83
|
+
nohup ./benchmarks/run_bench.sh beam <version_tag> --skip-ingest > /dev/null 2>&1 &
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**When to re-stage:**
|
|
87
|
+
- After changing `ENGRAM_EMBEDDING_MODEL` or `ENGRAM_CHUNK_SIZE` (old embeddings are stale)
|
|
88
|
+
- After a schema migration that drops the memories table
|
|
89
|
+
- To re-stage: delete staged projects first (`clear_all` or drop+recreate DB), then re-run Step 1
|
|
90
|
+
|
|
91
|
+
**Note:** `run_bench.sh` passes `--engram-url http://192.168.90.45:8000` and all extra args automatically.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Checking Progress
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
tail -5 /tmp/bench_lme_<tag>.log
|
|
99
|
+
tail -5 /tmp/bench_locomo_<tag>.log
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Scoring Results
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
# Quick LME scorecard (compare multiple versions)
|
|
108
|
+
python3 << 'EOF'
|
|
109
|
+
import json, glob
|
|
110
|
+
|
|
111
|
+
def score(path):
|
|
112
|
+
r = [json.loads(l) for l in open(path) if l.strip()]
|
|
113
|
+
s = lambda k: sum(1 for x in r if x['retrieval_results']['metrics']['session'].get(k, 0) >= 1.0)
|
|
114
|
+
n = len(r)
|
|
115
|
+
return n, s('recall_any@1'), s('recall_any@5'), s('recall_any@10'), \
|
|
116
|
+
sum(1 for x in r if x['retrieval_results']['metrics']['session'].get('recall_any@10', 0) < 1.0)
|
|
117
|
+
|
|
118
|
+
for path in sorted(glob.glob('benchmarks/results_engram_lme_*.jsonl')):
|
|
119
|
+
n, h1, h5, h10, miss = score(path)
|
|
120
|
+
tag = path.split('/')[-1]
|
|
121
|
+
print(f"{tag}: R@1={h1}/{n}={h1/n:.4f} R@5={h5/n:.4f} R@10={h10/n:.4f} misses={miss}")
|
|
122
|
+
EOF
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Key Config Variables (engram2.yml)
|
|
128
|
+
|
|
129
|
+
| Variable | Current | Notes |
|
|
130
|
+
|---|---|---|
|
|
131
|
+
| `ENGRAM_EMBEDDING_MODEL` | all-MiniLM-L6-v2 | Switch to BGE-large for higher quality |
|
|
132
|
+
| `ENGRAM_EMBEDDING_DIM` | 384 | Must match model (BGE-large=1024) |
|
|
133
|
+
| `ENGRAM_CHUNK_SIZE` | 1000 | Max chars per chunk (~1150 safe limit for MiniLM) |
|
|
134
|
+
| `ENGRAM_CHUNK_OVERLAP` | 300 | Higher = better boundary coverage, more chunks |
|
|
135
|
+
| `ENGRAM_HNSW_EF_SEARCH` | 200 | Higher = better recall, slower query |
|
|
136
|
+
| `ENGRAM_LLM_RERANK_TOP_N` | 20 | Candidates sent to LLM reranker |
|
|
137
|
+
|
|
138
|
+
### Pure-Math Recall Improvement Knobs (no new models)
|
|
139
|
+
|
|
140
|
+
| Variable | Default | Notes |
|
|
141
|
+
|---|---|---|
|
|
142
|
+
| `ENGRAM_BM25_SIGNAL_ENABLED` | `1` | In-process BM25 as additional RRF signal |
|
|
143
|
+
| `ENGRAM_BM25_SIGNAL_WEIGHT` | `0.5` | RRF weight for BM25 ranked list |
|
|
144
|
+
| `ENGRAM_ENTITY_SIGNAL_ENABLED` | `1` | Proper nouns + numbers overlap as RRF signal |
|
|
145
|
+
| `ENGRAM_ENTITY_SIGNAL_WEIGHT` | `0.3` | RRF weight for entity-overlap list |
|
|
146
|
+
| `ENGRAM_DATE_SIGNAL_WEIGHT` | `0.6` | RRF weight for date-proximity list (temporal queries) |
|
|
147
|
+
| `ENGRAM_RECENCY_SIGNAL_WEIGHT` | `0.2` | RRF weight for session-recency list (recency queries) |
|
|
148
|
+
| `ENGRAM_TURN_PAIR_SIGNAL_WEIGHT` | `0.15` | RRF weight for turn-pair-completeness list |
|
|
149
|
+
| `ENGRAM_MAXSIM_ENABLED` | `0` | Token-level MaxSim rerank (ColBERT-style, same model) |
|
|
150
|
+
| `ENGRAM_MAXSIM_TOP_N` | `20` | Candidates to rerank with MaxSim |
|
|
151
|
+
| `ENGRAM_MAXSIM_CACHE_SIZE` | `2048` | LRU doc-embedding cache entries |
|
|
152
|
+
| `ENGRAM_PRF_ENABLED` | `0` | Pseudo-relevance feedback (Rocchio, vague queries only) |
|
|
153
|
+
| `ENGRAM_PRF_TOP_K` | `5` | Top-K results used for PRF term extraction |
|
|
154
|
+
| `ENGRAM_PRF_N_TERMS` | `8` | Max expansion terms appended to FTS re-query |
|
|
155
|
+
| `ENGRAM_PRF_FTS_WEIGHT` | `0.3` | RRF weight for PRF result list |
|
|
156
|
+
| `ENGRAM_TIEBREAK_ENABLED` | `1` | Gap-aware tiebreaker (fires when top-2 gap ≤ eps) |
|
|
157
|
+
| `ENGRAM_TIEBREAK_EPS` | `0.005` | Score-gap threshold to trigger tiebreaker |
|
|
158
|
+
| `ENGRAM_MMR_ENABLED` | `1` | Session MMR diversification (counting queries only) |
|
|
159
|
+
| `ENGRAM_MMR_LAMBDA` | `0.7` | Relevance weight in MMR (0=diversity, 1=relevance) |
|
|
160
|
+
| `ENGRAM_MMR_SESSION_CAP` | `2` | Max chunks per session_id in output |
|
|
161
|
+
| `ENGRAM_TEMPORAL_HARD_FILTER` | `0` | Pre-filter candidates to target-date window |
|
|
162
|
+
| `ENGRAM_TEMPORAL_HARD_FILTER_SIGMA` | `3.5` | Window half-size in days |
|
|
163
|
+
|
|
164
|
+
Compose file: `/docker/compose/dev/engram2.yml`
|
|
165
|
+
|
|
166
|
+
After changing config, rebuild and restart:
|
|
167
|
+
```bash
|
|
168
|
+
cd /docker/compose/dev
|
|
169
|
+
docker compose -f engram2.yml --env-file /docker/compose/.env up -d --build
|
|
170
|
+
# Wait ~10s then verify
|
|
171
|
+
curl -s http://192.168.90.45:8000/health
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Benchmark Version History
|
|
177
|
+
|
|
178
|
+
| Version | Model | chunk/overlap/ef | LME R@1 | LoCoMo avg | BEAM 100K | BEIR SciFact nDCG@10 | Notes |
|
|
179
|
+
|---|---|---|---|---|---|---|---|
|
|
180
|
+
| v100/v110 | MiniLM | 800/200/ef50 | 0.846 | 0.906 | — | — | Baseline |
|
|
181
|
+
| v120 | BGE-large | 800/200/ef50 | 0.860 | — | 0.3274 | — | +7 LME R@1, worse LoCoMo |
|
|
182
|
+
| v301 | MiniLM | 900/100/ef50 | 0.852 | 0.763 | — | — | TF cap=2 fix |
|
|
183
|
+
| v302 | MiniLM | 1000/300/ef200 | 0.852 | 0.763 | 0.3917 | — | Plateau, best pre-v4 |
|
|
184
|
+
| **v402** | MiniLM | 1000/300/ef200 | **0.856** | **0.785** | **0.4167** | **0.6569** | Multi-signal RRF + MMR gate fix |
|
|
185
|
+
|
|
186
|
+
**BEIR SciFact v402 (2026-05-12):** nDCG@10=0.6569, Recall@100=0.9350, Precision@10=0.0887, Any-hit@100=94.0% (282/300)
|
|
187
|
+
For context: BM25 baseline ≈ 0.665, SentenceBERT ≈ 0.664, DPR ≈ 0.318 — Engram is at BM25 level on this domain-specific IR task.
|
|
188
|
+
|
|
189
|
+
**v402 signals (all enabled by default):**
|
|
190
|
+
- Phase A: BM25 (w=0.5) + entity overlap (w=0.3) fused via RRF
|
|
191
|
+
- Phase B: MaxSim rerank (disabled by default, `ENGRAM_MAXSIM_ENABLED=1`)
|
|
192
|
+
- Phase C: PRF expansion (disabled by default, `ENGRAM_PRF_ENABLED=1`)
|
|
193
|
+
- Phase D: Date-proximity Gaussian boost (w=0.6), session recency (w=0.2), turn-pair (w=0.15)
|
|
194
|
+
- Phase E: MMR diversification (enabled, gate: fires for non-counting queries)
|
|
195
|
+
- Phase F: Gap-aware tiebreaker
|
|
196
|
+
|
|
197
|
+
**v402 clean ablation (BEAM 100K, skip-ingest on staged data):**
|
|
198
|
+
| Variant | avg_recall | Δ vs v402 |
|
|
199
|
+
|---|---|---|
|
|
200
|
+
| v402 (all on) | 0.4167 | baseline |
|
|
201
|
+
| abl-all-off | 0.3917 | −0.025 |
|
|
202
|
+
| abl-no-bm25 | 0.4148 | −0.002 |
|
|
203
|
+
| abl-no-entity | 0.4165 | −0.000 |
|
|
204
|
+
| abl-no-date | 0.4167 | 0.000 |
|
|
205
|
+
| abl-no-recency-turnpair | 0.4181 | +0.001 (recency/turn-pair mildly hurt BEAM) |
|
|
206
|
+
| abl-no-tiebreak-mmr | 0.4043 | −0.012 (tiebreak+MMR are critical) |
|
|
207
|
+
|
|
208
|
+
**Current best**: v402 (git: `public-prep` branch, commit `25d6016`)
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## Known Hard Misses (LME v402)
|
|
213
|
+
|
|
214
|
+
15 questions that fail R@1 consistently:
|
|
215
|
+
- **single-session-preference** (18/30 misses): soft/subjective queries ("Can you suggest accessories for my setup?") — correct session is in top-10 in 13/18 cases but ranking noise prevents rank-1. Small n=30 makes improvements unreliable.
|
|
216
|
+
- **temporal-reasoning** (~22/133 misses, R@1=0.835): time-dependent queries
|
|
217
|
+
- **multi-session** (~21/133 misses, R@1=0.842): 14 are close-rank misses (answer at rank 2-3); often generic sharegpt/ultrachat content outranks personal conversation turns via BM25
|
|
218
|
+
|
|
219
|
+
**Preference boost notes**: Broadening `apply_preference_signal_boost` to fire on advice-seeking queries regardless of vagueness HURT preference (-1 hit) because non-topic preference language in other sessions gets incorrectly boosted. The vague-query gate is intentional.
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## End-to-End LME (Retrieval + Generation)
|
|
224
|
+
|
|
225
|
+
`lme_e2e_bench.py` loads retrieval results from a prior LME run, feeds top-K chunks to
|
|
226
|
+
Gemma4:31b via Ollama, and evaluates generated answers against gold.
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
cd /docker/appdata/engram
|
|
230
|
+
|
|
231
|
+
# Full 500q E2E (top-10 chunks → Gemma4, ~55min)
|
|
232
|
+
python3 benchmarks/lme_e2e_bench.py \
|
|
233
|
+
--retrieval-results benchmarks/results_engram_lme_v402-mmr-fix_20260512_1106.jsonl \
|
|
234
|
+
--out benchmarks/results_engram_lme_e2e_v402.jsonl
|
|
235
|
+
|
|
236
|
+
# Judge pass on misses (adds LLM rescoring of substring-match failures)
|
|
237
|
+
python3 benchmarks/lme_e2e_bench.py \
|
|
238
|
+
--retrieval-results benchmarks/results_engram_lme_v402-mmr-fix_20260512_1106.jsonl \
|
|
239
|
+
--judge --out benchmarks/results_engram_lme_e2e_v402_judged.jsonl
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
**v402 E2E results (retrieval + Gemma4:31b, top-K=10, corrected scoring):**
|
|
243
|
+
|
|
244
|
+
| Type | N | Exact | Smart-Norm | Judge | **Final** | Retrieval R@1 |
|
|
245
|
+
|---|---|---|---|---|---|---|
|
|
246
|
+
| single-session-user | 70 | 0.800 | +2 | +4 | **0.843** | 0.957 |
|
|
247
|
+
| knowledge-update | 78 | 0.654 | +5 | +2 | **0.821** | 0.990 |
|
|
248
|
+
| single-session-assistant | 56 | 0.268 | +1 | +3 | **0.429** | 0.929 |
|
|
249
|
+
| multi-session | 133 | 0.293 | +10 | +15 | **0.451** | 0.985 |
|
|
250
|
+
| temporal-reasoning | 133 | 0.195 | +5 | +25 | **0.429** | 0.962 |
|
|
251
|
+
| single-session-preference | 30 | 0.000 | +1 | +6 | **0.067** | 0.933 |
|
|
252
|
+
| **TOTAL** | **500** | **0.374** | **+24** | **+55** | **0.532** | **0.856** |
|
|
253
|
+
|
|
254
|
+
**Key finding:** 174/313 misses (56%) are "Unknown" responses — the LLM cannot extract the
|
|
255
|
+
answer from the provided chunks even though retrieval is excellent. This is an extraction
|
|
256
|
+
problem, not a retrieval problem. The assistant (50% Unknown) and preference (73% Unknown)
|
|
257
|
+
categories are hardest; user-facts (84.3%) and knowledge-updates (82.1%) are our strong suit.
|
|
258
|
+
|
|
259
|
+
**Judge details:** Gemma4:31b acting as LLM judge with leniency (paraphrases, number formats,
|
|
260
|
+
minor wording). 55/115 judgeable misses rescued. "Unknown" responses skipped (can't judge).
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
## Important Code Locations
|
|
265
|
+
|
|
266
|
+
| File | What's there |
|
|
267
|
+
|---|---|
|
|
268
|
+
| `src/engram/rerank.py` | TF cap (`_TF_CAP = 2`), keyword reranker |
|
|
269
|
+
| `src/engram/manager.py` | `apply_turn_pair_boost()`, embedding prefix handling |
|
|
270
|
+
| `src/engram/config.py` | All config defaults |
|
|
271
|
+
| `benchmarks/run_bench.sh` | Benchmark launcher (use this!) |
|
|
272
|
+
| `benchmarks/lme_e2e_bench.py` | End-to-end LME (retrieval + generation + judge) |
|
|
273
|
+
| `benchmarks/results_history.tsv` | One-line summary of every run |
|
|
274
|
+
| `/docker/compose/dev/engram2.yml` | Container config (model, chunk size, etc.) |
|
|
275
|
+
| `/tmp/bench_<bench>_<tag>.log` | Live benchmark log |
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
Engram is a persistent memory service for AI coding agents. It exposes three surfaces:
|
|
4
|
+
|
|
5
|
+
1. **MCP over SSE** at `/sse` + `/messages` — for MCP-compatible clients (VS Code, Cursor, Claude Desktop, n8n).
|
|
6
|
+
2. **REST API** at `/api/*` — for scripts, automation, and non-MCP clients.
|
|
7
|
+
3. **Web dashboard** at `/` — for humans, intended to sit behind SSO/OAuth at your reverse proxy.
|
|
8
|
+
|
|
9
|
+
Everything runs as a single FastAPI process against a single PostgreSQL database with the `pgvector` extension.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Request Lifecycle
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
client ──► (reverse proxy / OAuth) ──► FastAPI ──► AuthMiddleware
|
|
17
|
+
│
|
|
18
|
+
┌────────────┴───────────┐
|
|
19
|
+
▼ ▼
|
|
20
|
+
MemoryManager FastMCP (SSE)
|
|
21
|
+
│ │
|
|
22
|
+
└────────┬───────────────┘
|
|
23
|
+
▼
|
|
24
|
+
PostgreSQL + pgvector
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Authentication
|
|
28
|
+
|
|
29
|
+
Two paths, resolved by `auth.get_auth`:
|
|
30
|
+
|
|
31
|
+
1. **Bearer token** (`Authorization: Bearer engram_…`) — validated against the `api_keys` table in constant time. Keys are project-scoped (`agent`) or global (`admin`).
|
|
32
|
+
2. **OAuth passthrough** — the reverse proxy sets `X-Forwarded-User` after its own auth. Trusted as admin.
|
|
33
|
+
|
|
34
|
+
`ENGRAM_DEMO_MODE=1` short-circuits both with an unauthenticated admin identity for local development.
|
|
35
|
+
|
|
36
|
+
MCP requests reuse the same resolver via `get_mcp_auth(ctx)`, which reads the `Authorization` header from the SSE handshake.
|
|
37
|
+
|
|
38
|
+
### Project scoping
|
|
39
|
+
|
|
40
|
+
Every tenant-facing operation calls `AuthContext.enforce_project_access(project_name)`. Agents can "claim" a project name that doesn't yet exist — this makes self-service onboarding possible without an admin interventions step.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Storage Layer (`stores/postgresql.py`)
|
|
45
|
+
|
|
46
|
+
A single `PostgresStore` class wraps a `psycopg` async connection pool and owns all SQL. Tables:
|
|
47
|
+
|
|
48
|
+
| Table | Purpose |
|
|
49
|
+
|---|---|
|
|
50
|
+
| `projects` | Tenant namespaces. `persistent_memories` flag exempts all memories from decay/GC. |
|
|
51
|
+
| `sessions` | Agent working sessions — task, summary, handoff notes. |
|
|
52
|
+
| `memories` | The core table. 20+ columns: kind, content, subject, `embedding vector(384)`, `simhash bigint`, decay state, version chain, `pinned`, `obsolete`, tags (JSONB), `content_tsv` (tsvector). |
|
|
53
|
+
| `memory_access` | Access log used by the decay model. |
|
|
54
|
+
| `entities` | Graph nodes — file, module, concept, tool, person, library, config, command. |
|
|
55
|
+
| `relationships` | Directed graph edges. |
|
|
56
|
+
| `memory_entities` | Link table associating memories with the entities they mention. |
|
|
57
|
+
| `api_keys` | Hashed keys only. Prefix is visible for identification. |
|
|
58
|
+
| `schema_migrations` | Applied migration versions. |
|
|
59
|
+
|
|
60
|
+
### Indexes
|
|
61
|
+
|
|
62
|
+
- `memories.embedding` — HNSW, `vector_cosine_ops`, `m=16, ef_construction=64`.
|
|
63
|
+
- `memories.content_tsv` — GiST for full-text search.
|
|
64
|
+
- `memories.content` — trigram GIN for fuzzy matching.
|
|
65
|
+
- Multicolumn covering indexes on `(project_id, kind)`, `(from_entity)`, `(to_entity)`.
|
|
66
|
+
|
|
67
|
+
### Migrations
|
|
68
|
+
|
|
69
|
+
`migrations/runner.py` applies numbered Python migration modules in order and records each in `schema_migrations`. Current migrations:
|
|
70
|
+
|
|
71
|
+
1. Entity tracking bootstrap (pseudo-entity cleanup)
|
|
72
|
+
2. Decay fields, versioning, simhash
|
|
73
|
+
3. Pinned memories
|
|
74
|
+
4. Per-project persistent flag
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## MemoryManager (`manager.py`)
|
|
79
|
+
|
|
80
|
+
The unified API consumed by both the REST handlers and MCP tools.
|
|
81
|
+
|
|
82
|
+
### `remember()` pipeline
|
|
83
|
+
|
|
84
|
+
1. **SimHash dedup** — O(1) Hamming distance against recent memories in the same project. Configurable threshold (default 3 bits).
|
|
85
|
+
2. **Embedding** — generated in a thread-pool (non-blocking async).
|
|
86
|
+
3. **Semantic dedup** — cosine distance against existing embeddings above `semantic_dedup_threshold` (default 0.92).
|
|
87
|
+
4. **Conflict surfacing** — find fact/decision memories with similarity ≥ 0.80. Returned to the caller so the agent can explicitly supersede or reconcile.
|
|
88
|
+
5. **Store** — with version-chain pointers if `supersedes` was supplied.
|
|
89
|
+
|
|
90
|
+
### `recall()`
|
|
91
|
+
|
|
92
|
+
Runs **two retrieval passes in parallel**, then fuses:
|
|
93
|
+
|
|
94
|
+
- **Semantic** — pgvector `<=>` cosine search, HNSW-accelerated.
|
|
95
|
+
- **Keyword** — `ts_rank` against `content_tsv` plus trigram similarity.
|
|
96
|
+
|
|
97
|
+
Ranks are fused via **Reciprocal Rank Fusion** with configurable weights (`rrf_vector_weight`, `rrf_keyword_weight`). Decay-based score boost (range `[0.3, 1.2]`) is applied last.
|
|
98
|
+
|
|
99
|
+
Top-5 results have their decay state updated on access (`update_decay_on_access`).
|
|
100
|
+
|
|
101
|
+
### Knowledge graph
|
|
102
|
+
|
|
103
|
+
`entity_explore()` runs a recursive CTE over `relationships`, configurable depth and direction (`incoming`/`outgoing`/`both`). Returns entities + the memories linked to them.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Decay Model (`decay.py`)
|
|
108
|
+
|
|
109
|
+
FSRS-inspired power-law retrievability:
|
|
110
|
+
|
|
111
|
+
- `R = exp(-t / S)` where `S = base_stability * (1 + storage_strength)`.
|
|
112
|
+
- `update_on_access` boosts `S` with diminishing returns: `S_new = S + growth_factor / (1 + 0.5 * S)`.
|
|
113
|
+
- At query time, `decay_score_boost` returns a ranking multiplier in `[0.3, 1.2]`.
|
|
114
|
+
|
|
115
|
+
Pinned memories, memories in persistent projects, and memories of kind `decision` or `procedure` are exempt from GC.
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Reflection (`reflection.py`, `scheduler.py`)
|
|
120
|
+
|
|
121
|
+
An async background job runs every `ENGRAM_REFLECTION_INTERVAL_HOURS` (default 24). Three phases:
|
|
122
|
+
|
|
123
|
+
1. **GC** — mark obsolete any memory with retrievability < `ENGRAM_REFLECTION_GC_THRESHOLD` (default 0.05), older than `ENGRAM_REFLECTION_GC_MIN_AGE_DAYS` (default 7), and not in an exempt class.
|
|
124
|
+
2. **Consolidation** — cluster high-similarity memories (threshold 0.88), merge into a single summary memory, mark originals obsolete with a pointer.
|
|
125
|
+
3. **Conflict resolution** — find fact/decision pairs with similarity ≥ 0.85 and age gap ≥ 7 days; obsolete the older one.
|
|
126
|
+
|
|
127
|
+
Limits (`reflection_max_consolidations` = 10) prevent churn. The scheduler exposes `/api/admin/reflection/run` for manual triggering.
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Activity & Logging
|
|
132
|
+
|
|
133
|
+
- `activity.py` — in-memory ring buffer (~2000 events). Event types: `WRITE`, `RECALL`, `SESSION`, `ENTITY`, `REFLECT`, `DEDUP`, `FORGET`, `CONFLICT`.
|
|
134
|
+
- `textlog.py` — best-effort persistent log tailed by the dashboard.
|
|
135
|
+
- `logging.py` — optional JSON log formatter (`LOG_FORMAT=json`).
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Backup (`backup.py`)
|
|
140
|
+
|
|
141
|
+
JSON archive format v2 including all tables except `api_keys` and `schema_migrations`. Embeddings serialize as JSON arrays. Backward-compatible v1→v2 upgrade. Restore supports `merge` (upsert) or `clean` (truncate first). Auto-rotation keeps `N` recent plus any younger than `X` days.
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## Rate Limiting (`ratelimit.py`)
|
|
146
|
+
|
|
147
|
+
Starlette middleware, per-IP token bucket.
|
|
148
|
+
|
|
149
|
+
- Defaults: burst 30, refill 120/min.
|
|
150
|
+
- Exempt paths: `/health`, `/sse`, `/messages`.
|
|
151
|
+
- Respects `X-Forwarded-For` when behind a proxy.
|
|
152
|
+
- Returns `429` with `Retry-After`.
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Configuration
|
|
157
|
+
|
|
158
|
+
All via environment variables. Defaults live in [`src/engram/core/config.py`](src/engram/core/config.py).
|
|
159
|
+
|
|
160
|
+
### Required
|
|
161
|
+
|
|
162
|
+
| Variable | Notes |
|
|
163
|
+
|---|---|
|
|
164
|
+
| `ENGRAM_PG_PASSWORD` | Must not be literal `engram`. Server refuses to start otherwise unless `ENGRAM_DEMO_MODE=1`. |
|
|
165
|
+
|
|
166
|
+
### PostgreSQL
|
|
167
|
+
|
|
168
|
+
`ENGRAM_PG_HOST`, `ENGRAM_PG_PORT`, `ENGRAM_PG_USER`, `ENGRAM_PG_DATABASE`, `ENGRAM_PG_POOL_TIMEOUT`.
|
|
169
|
+
|
|
170
|
+
### Embeddings & retrieval
|
|
171
|
+
|
|
172
|
+
`ENGRAM_EMBEDDING_MODEL`, `ENGRAM_EMBEDDING_DIM`, `ENGRAM_RRF_VECTOR_WEIGHT`, `ENGRAM_RRF_KEYWORD_WEIGHT`, `ENGRAM_RRF_OVERFETCH`.
|
|
173
|
+
|
|
174
|
+
### Decay
|
|
175
|
+
|
|
176
|
+
`ENGRAM_DECAY_STABILITY`, `ENGRAM_DECAY_GROWTH`.
|
|
177
|
+
|
|
178
|
+
### Deduplication
|
|
179
|
+
|
|
180
|
+
`ENGRAM_DEDUP_ENABLED`, `ENGRAM_DEDUP_THRESHOLD`, `ENGRAM_SEMANTIC_DEDUP_ENABLED`, `ENGRAM_SEMANTIC_DEDUP_THRESHOLD`.
|
|
181
|
+
|
|
182
|
+
### Reflection
|
|
183
|
+
|
|
184
|
+
`ENGRAM_REFLECTION_ENABLED`, `ENGRAM_REFLECTION_INTERVAL_HOURS`, `ENGRAM_REFLECTION_GC_THRESHOLD`, `ENGRAM_REFLECTION_GC_MIN_AGE_DAYS`, `ENGRAM_REFLECTION_CONSOLIDATION_SIM`, `ENGRAM_REFLECTION_MIN_CLUSTER`, `ENGRAM_REFLECTION_MAX_CONSOLIDATIONS`, `ENGRAM_REFLECTION_CONFLICT_SIM`, `ENGRAM_REFLECTION_CONFLICT_AGE_GAP`.
|
|
185
|
+
|
|
186
|
+
### Backup
|
|
187
|
+
|
|
188
|
+
`ENGRAM_BACKUP_DIR`, `ENGRAM_BACKUP_KEEP_LAST`, `ENGRAM_BACKUP_KEEP_DAYS`.
|
|
189
|
+
|
|
190
|
+
### Network / auth
|
|
191
|
+
|
|
192
|
+
`ENGRAM_ALLOWED_HOSTS`, `ENGRAM_CORS_ORIGINS`, `ENGRAM_DEMO_MODE`, `ENGRAM_RATE_LIMIT_RPM`, `ENGRAM_RATE_LIMIT_BURST`.
|
|
193
|
+
|
|
194
|
+
### Logging
|
|
195
|
+
|
|
196
|
+
`LOG_LEVEL`, `LOG_FORMAT`.
|
|
197
|
+
|
|
198
|
+
### Bulk import (path traversal guard)
|
|
199
|
+
|
|
200
|
+
`ENGRAM_IMPORT_ALLOWED_DIRS` — comma-separated list of base directories under which `/api/bulk/import` is allowed to read. Default `/app`.
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## Known Limitations
|
|
205
|
+
|
|
206
|
+
- Embeddings are currently synchronous per-memory at write time (batched via a thread-pool executor). For very high-throughput workloads, consider pre-batching client-side.
|
|
207
|
+
- HNSW index quality depends on insert order; a periodic `REINDEX` can help after heavy ingestion. See `engram-manage re-embed` for the supported rebuild path.
|
|
208
|
+
- The activity ring buffer is per-process; in a multi-replica deployment, consume the text log instead.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented here. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses [Semantic Versioning](https://semver.org/).
|
|
4
|
+
|
|
5
|
+
## [Unreleased]
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Apache 2.0 license, `NOTICE`, `CONTRIBUTING.md`, `SECURITY.md`, `ARCHITECTURE.md`.
|
|
9
|
+
- Startup guard: the server refuses to start if `ENGRAM_PG_PASSWORD` is unset or equal to the default `engram`, unless `ENGRAM_DEMO_MODE=1`.
|
|
10
|
+
- `.env.example` with documented minimum settings.
|
|
11
|
+
- OCI image labels on the Dockerfile.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- `benchmarks/*.py` defaults now point at `http://localhost:8000` instead of a hard-coded LAN address.
|
|
15
|
+
- README rewritten for a public audience; architectural deep-dive moved to `ARCHITECTURE.md`.
|
|
16
|
+
- CORS middleware now disables credentials when `allow_origins=["*"]` (matches browser spec).
|
|
17
|
+
|
|
18
|
+
### Removed
|
|
19
|
+
- Internal-only `TECHNICAL_REVIEW.md` (content preserved in `ARCHITECTURE.md`).
|
|
20
|
+
- Private domain and hostnames from documentation.
|
|
21
|
+
|
|
22
|
+
## [0.7.0] — 2026-04
|
|
23
|
+
|
|
24
|
+
Historical releases tracked via git history. Highlights:
|
|
25
|
+
|
|
26
|
+
- LongMemEval + LoCoMo benchmark harness, adaptive keyword weight for vague queries.
|
|
27
|
+
- Reciprocal-Rank Fusion hybrid search, preference boosting, entity-aware dedup.
|
|
28
|
+
- Persistent-project flag, pinning, versioning, SimHash dedup, FSRS-inspired decay.
|
|
29
|
+
- Reflection / compaction scheduler (GC, consolidation, conflict resolution).
|
|
30
|
+
- Dashboard (activity stream, graph viz, backup/restore), REST project claiming.
|
|
31
|
+
- Pagination, structured JSON logging, CI pipeline.
|
|
32
|
+
- API key cycling, hard-forget endpoint, 403 hints.
|
|
33
|
+
- Backup rotation, integration tests.
|
|
34
|
+
- Initial PostgreSQL + pgvector rewrite (replacing DuckDB + LanceDB + Kuzu).
|