Skip to content
This repository was archived by the owner on Jun 14, 2026. It is now read-only.

Commit b8f62ef

Browse files
committed
chore: release v0.18.0
Store hardening release combining two changes: - feat(doctor): StoreIntegrityChecks category added to aur doctor with auto-repair under --fix (commit c5dd2ec). - feat(core): tiered access-history compaction behind AURORA_COMPACT_ACCESS_HISTORY=1 flag, default off (commit 0640e5d). See CHANGELOG.md and docs/02-features/memory/STORE_HARDENING.md. Tests: 2709 passed / 3 skipped / 0 failed via make test.
1 parent 0640e5d commit b8f62ef

8 files changed

Lines changed: 44 additions & 50 deletions

File tree

CHANGELOG.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.18.0] - 2026-04-14
11+
12+
### Added
13+
14+
- **`aur doctor` store integrity checks** — new `StoreIntegrityChecks` category runs alongside the existing six health-check categories. Detects silent corruption in the ACT-R store:
15+
- FTS5 desync (missing/stale `chunks_fts` rows vs. `chunks`)
16+
- Orphan code chunks (file_path no longer in `file_index`)
17+
- Activation orphans (rows referencing deleted chunks, FK-bypass insurance)
18+
- Reasoning-chunk growth warning above 10K (Record-phase unbounded caching signal)
19+
- Retrieval roundtrip (end-to-end FTS sanity via highest-access seed chunk)
20+
- Three mechanical failures (FTS desync, orphans, activation orphans) are auto-repaired under the existing `aur doctor --fix` flag.
21+
- **Tiered access-history compaction** behind `AURORA_COMPACT_ACCESS_HISTORY=1` flag (default off). Bounds the unbounded-growth path in `activations.access_history` by collapsing older access records into time buckets for storage only. BLA ranking preserved to <0.001 because the decay formula (ln Σ t_j^(-d)) already treats old records as negligible.
22+
- Four tiers: 0–7d verbatim, 7–30d hourly buckets, 30–180d daily buckets, 180d+ aggregate.
23+
- `AccessHistoryEntry` gains an optional `count: int = 1` field; BLA loop becomes `count * t^(-d)`. Fully backward compatible — pre-change callers unchanged.
24+
- Lazy trigger at length > 200; common-path `record_access` unaffected.
25+
- Design doc: `docs/02-features/memory/STORE_HARDENING.md` covering both changes, rationale, math, and rollout plan.
26+
27+
### Changed
28+
29+
- `get_access_history()` docstring documents the new optional `count` key and the compaction flag.
30+
1031
## [0.17.6] - 2026-02-14
1132

1233
### Added

packages/cli/src/aurora_cli/health_checks.py

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import importlib
1414
import importlib.metadata
1515
import os
16+
import sqlite3
1617
import subprocess
1718
from pathlib import Path
1819
from typing import Any
@@ -1003,8 +1004,6 @@ def run_checks(self) -> list[HealthCheckResult]:
10031004
)
10041005
return results
10051006

1006-
import sqlite3
1007-
10081007
try:
10091008
conn = sqlite3.connect(str(db_path))
10101009
conn.row_factory = sqlite3.Row
@@ -1245,8 +1244,6 @@ def get_fixable_issues(self) -> list[dict[str, Any]]:
12451244
if not db_path.exists():
12461245
return issues
12471246

1248-
import sqlite3
1249-
12501247
try:
12511248
conn = sqlite3.connect(str(db_path))
12521249
except sqlite3.Error:
@@ -1319,8 +1316,6 @@ def _fix_fts_desync(self) -> None:
13191316
Uses the same `(name, body, file_path)` extraction as sqlite._upsert_fts
13201317
so repaired rows are indistinguishable from normally-written ones.
13211318
"""
1322-
import sqlite3
1323-
13241319
db_path = Path(self.config.get_db_path())
13251320
conn = sqlite3.connect(str(db_path))
13261321
try:
@@ -1344,7 +1339,9 @@ def _fix_fts_desync(self) -> None:
13441339

13451340
for chunk_id, chunk_type, content_raw in missing_chunks:
13461341
try:
1347-
content = json.loads(content_raw) if isinstance(content_raw, str) else content_raw
1342+
content = (
1343+
json.loads(content_raw) if isinstance(content_raw, str) else content_raw
1344+
)
13481345
except (json.JSONDecodeError, TypeError):
13491346
continue
13501347
if not isinstance(content, dict):
@@ -1370,8 +1367,6 @@ def _fix_orphan_chunks(self) -> None:
13701367
Also removes matching FTS5 rows so a subsequent FTS consistency check
13711368
doesn't immediately re-flag them as stale.
13721369
"""
1373-
import sqlite3
1374-
13751370
db_path = Path(self.config.get_db_path())
13761371
conn = sqlite3.connect(str(db_path))
13771372
try:
@@ -1400,8 +1395,6 @@ def _fix_orphan_chunks(self) -> None:
14001395

14011396
def _fix_activation_orphans(self) -> None:
14021397
"""Delete activation rows whose chunk_id no longer exists."""
1403-
import sqlite3
1404-
14051398
db_path = Path(self.config.get_db_path())
14061399
conn = sqlite3.connect(str(db_path))
14071400
try:

packages/cli/src/aurora_cli/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
console = Console()
3131
logger = logging.getLogger(__name__)
3232

33-
AURORA_VERSION = "0.17.6"
33+
AURORA_VERSION = "0.18.0"
3434

3535

3636
def _version_callback(ctx: click.Context, param: click.Parameter, value: bool) -> None:

packages/cli/tests/unit/test_store_integrity_checks.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@
1111
import sqlite3
1212
from pathlib import Path
1313

14-
import pytest
15-
1614
from aurora_cli.config import Config
1715
from aurora_cli.health_checks import StoreIntegrityChecks
1816
from aurora_core.store.schema import get_init_statements

packages/core/src/aurora_core/store/access_history.py

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,9 @@
4343

4444
# Tier boundaries in seconds. Kept as module constants so tests can reference
4545
# them without re-deriving.
46-
_TIER1_MAX_SECONDS = 7 * 24 * 3600 # 7 days
47-
_TIER2_MAX_SECONDS = 30 * 24 * 3600 # 30 days
48-
_TIER3_MAX_SECONDS = 180 * 24 * 3600 # 180 days
46+
_TIER1_MAX_SECONDS = 7 * 24 * 3600 # 7 days
47+
_TIER2_MAX_SECONDS = 30 * 24 * 3600 # 30 days
48+
_TIER3_MAX_SECONDS = 180 * 24 * 3600 # 180 days
4949

5050

5151
def _parse_timestamp(raw: Any) -> datetime | None:
@@ -113,9 +113,9 @@ def compact_access_history(
113113
now = now.replace(tzinfo=timezone.utc)
114114

115115
# Split entries into tiers by age.
116-
tier1: list[dict[str, Any]] = [] # kept verbatim
117-
tier2_by_hour: dict[int, int] = {} # hour_epoch -> total count
118-
tier3_by_day: dict[int, int] = {} # day_epoch -> total count
116+
tier1: list[dict[str, Any]] = [] # kept verbatim
117+
tier2_by_hour: dict[int, int] = {} # hour_epoch -> total count
118+
tier3_by_day: dict[int, int] = {} # day_epoch -> total count
119119
tier4_count = 0
120120
tier4_earliest: datetime | None = None
121121

@@ -187,9 +187,7 @@ def compact_access_history(
187187
# access and the tier 3/4 boundary, which is where tier 4 begins.
188188
if tier4_count > 0 and tier4_earliest is not None:
189189
tier4_boundary = now - timedelta(seconds=_TIER3_MAX_SECONDS)
190-
midpoint_epoch = (
191-
int(tier4_earliest.timestamp()) + int(tier4_boundary.timestamp())
192-
) // 2
190+
midpoint_epoch = (int(tier4_earliest.timestamp()) + int(tier4_boundary.timestamp())) // 2
193191
midpoint = datetime.fromtimestamp(midpoint_epoch, tz=timezone.utc)
194192
output.append(
195193
{

packages/core/tests/integration/test_access_history_compaction_sqlite.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,9 @@
1515
from __future__ import annotations
1616

1717
import json
18-
import os
1918
import sqlite3
2019
from datetime import datetime, timedelta, timezone
2120

22-
import pytest
23-
2421
from aurora_core.chunks.code_chunk import CodeChunk
2522
from aurora_core.store.access_history import COMPACTION_TRIGGER_LENGTH
2623
from aurora_core.store.sqlite import SQLiteStore

packages/core/tests/unit/test_access_history_compaction.py

Lines changed: 10 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,7 @@ def test_empty_input_returns_empty(self):
6464
def test_output_length_bounded(self):
6565
"""10K entries spanning 2 years must compact to a small bounded size."""
6666
random.seed(42)
67-
entries = [
68-
_entry(random.uniform(0, 720 * 86400)) for _ in range(10_000)
69-
]
67+
entries = [_entry(random.uniform(0, 720 * 86400)) for _ in range(10_000)]
7068
out = compact_access_history(entries, now=NOW)
7169
# Tier 2 can have at most 23 * (30-7) ≈ 552 hourly buckets if every
7270
# hour in tiers 2 was hit. Tier 3 has at most (180-30) = 150 daily
@@ -78,9 +76,7 @@ def test_output_length_bounded(self):
7876
def test_total_count_preserved(self):
7977
"""Compaction must not lose any access count — it's a reshape only."""
8078
random.seed(7)
81-
entries = [
82-
_entry(random.uniform(0, 365 * 86400)) for _ in range(500)
83-
]
79+
entries = [_entry(random.uniform(0, 365 * 86400)) for _ in range(500)]
8480
expected = _total_count(entries)
8581
out = compact_access_history(entries, now=NOW)
8682
assert _total_count(out) == expected
@@ -161,10 +157,10 @@ def test_entry_400_days_old_is_tier4_aggregate(self):
161157

162158
def test_mixed_tiers_all_represented(self):
163159
entries = [
164-
_entry(3600), # tier 1
165-
_entry(10 * 86400), # tier 2
166-
_entry(100 * 86400), # tier 3
167-
_entry(200 * 86400), # tier 4
160+
_entry(3600), # tier 1
161+
_entry(10 * 86400), # tier 2
162+
_entry(100 * 86400), # tier 3
163+
_entry(200 * 86400), # tier 4
168164
]
169165
out = compact_access_history(entries, now=NOW)
170166
assert len(out) == 4
@@ -187,9 +183,7 @@ def test_single_recent_access_unchanged(self):
187183
def test_hot_chunk_365_day_history(self):
188184
"""A chunk accessed 1000 times over a year: BLA must barely change."""
189185
random.seed(1)
190-
entries = [
191-
_entry(random.uniform(0, 365 * 86400)) for _ in range(1000)
192-
]
186+
entries = [_entry(random.uniform(0, 365 * 86400)) for _ in range(1000)]
193187
bla_before = _bla_from_raw(entries)
194188
out = compact_access_history(entries, now=NOW)
195189
bla_after = _bla_from_raw(out)
@@ -201,9 +195,7 @@ def test_error_bound_holds_across_random_patterns(self):
201195
for seed in range(50):
202196
random.seed(seed)
203197
size = random.randint(50, 1500)
204-
entries = [
205-
_entry(random.uniform(0, 720 * 86400)) for _ in range(size)
206-
]
198+
entries = [_entry(random.uniform(0, 720 * 86400)) for _ in range(size)]
207199
bla_before = _bla_from_raw(entries)
208200
out = compact_access_history(entries, now=NOW)
209201
bla_after = _bla_from_raw(out)
@@ -219,20 +211,15 @@ def test_ranking_preserved_top_k(self):
219211
chunks: list[tuple[str, list[dict]]] = []
220212
for i in range(20):
221213
size = random.randint(10, 500)
222-
entries = [
223-
_entry(random.uniform(0, 300 * 86400)) for _ in range(size)
224-
]
214+
entries = [_entry(random.uniform(0, 300 * 86400)) for _ in range(size)]
225215
chunks.append((f"chunk-{i}", entries))
226216

227217
# Rank before compaction.
228218
before = sorted(chunks, key=lambda c: _bla_from_raw(c[1]), reverse=True)
229219
before_ids = [c[0] for c in before]
230220

231221
# Compact each chunk, re-rank.
232-
after = [
233-
(cid, compact_access_history(entries, now=NOW))
234-
for cid, entries in chunks
235-
]
222+
after = [(cid, compact_access_history(entries, now=NOW)) for cid, entries in chunks]
236223
after.sort(key=lambda c: _bla_from_raw(c[1]), reverse=True)
237224
after_ids = [c[0] for c in after]
238225

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "aurora-actr"
7-
version = "0.17.6"
7+
version = "0.18.0"
88
description = "AURORA: Adaptive Unified Reasoning and Orchestration Architecture with MCP Integration"
99
readme = "README.md"
1010
requires-python = ">=3.12"

0 commit comments

Comments
 (0)