Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ installable release; see the roadmap in [README.md](README.md).

- **PreCompact hook + rebuild logic, augment mode** ([#139](https://github.com/robotrocketscience/aelfrice/issues/139), [docs/context_rebuilder.md](docs/context_rebuilder.md)). v1.4.0 milestone: replaces the v1.2.0a0 alpha's per-token union retrieval workaround with the v1.3 `retrieve()` codepath (L0 + L1 + L2.5 in one call). New `aelfrice.context_rebuilder.rebuild_v14()` pure function packs L0 locked beliefs first (full, never trimmed), then session-scoped beliefs whose `session_id` matches the latest transcript turn's session, then the L2.5/L1 tail from `retrieve()` — within a configurable token budget. Query string is built from entity + triple extraction over the recent-turn window (no LLM). `aelfrice.context_rebuilder.main()` is the new module-level Claude Code PreCompact hook entry point; `aelfrice.hook.pre_compact()` continues to dispatch through the same logic and now wraps output in the harness's `hookSpecificOutput.additionalContext` JSON envelope. New `[rebuilder] turn_window_n` and `[rebuilder] token_budget` keys in `.aelfrice.toml` (defaults 50 and 4000); CLI flags on `aelf rebuild --n` / `--budget` override per-call. `aelf rebuild` now drives the same `rebuild_v14()` codepath the hook uses. `aelf setup --rebuilder` (already shipped at v1.2.0a0) installs the PreCompact hook idempotently. Augment-mode only: both the harness's compaction summary and the rebuild block land in the new context. Suppress mode is parked for v2.x. Empty-transcript / missing-store edge cases exit 0 with no `additionalContext` written. Reproducible: same transcript tail + same store state → byte-identical envelope. Latency budget: median ≤ 200 ms on a 10k-belief store; measured ~2 ms on a workstation. 15 new deterministic tests in `tests/test_context_rebuilder_hook.py` cover ordering, edge cases, reproducibility, latency, the `[rebuilder]` config parser, the JSON envelope shape, and session-scoping invariants.

- **Partial Bayesian-weighted ranking (v1.3.0)** ([#146](https://github.com/robotrocketscience/aelfrice/issues/146), [docs/bayesian_ranking.md](docs/bayesian_ranking.md)). L1 BM25 ranking now consumes the Beta-Bernoulli posterior log-additively per the spec's adopted Path B contract: `score = log(-bm25_raw) + posterior_weight * log(posterior_mean(α, β))`. `posterior_weight` defaults to `0.5` (the synthetic-graph optimum from the v1.3 calibration); `0.0` reproduces v1.0.x BM25-only ordering byte-for-byte (regression-tested). Locked beliefs (L0) bypass scoring entirely; L2.5 entity-index hits and L3 BFS expansions are unaffected — the weight only reranks the L1 candidate set. New `scoring.partial_bayesian_score(bm25_raw, alpha, beta, posterior_weight)` reuses `scoring.posterior_mean` (Jeffreys prior `α / (α+β)`); the Laplace `(α+1) / (α+β+2)` form sketched in #151 is explicitly rejected at this layer per spec rationale. New `MemoryStore.search_beliefs_scored(query, limit) -> list[tuple[Belief, float]]` exposes the FTS5 BM25 score; `MemoryStore.search_beliefs` is unchanged. `retrieve()`, `retrieve_with_tiers()`, and `retrieve_v2()` gain a `posterior_weight: float | None` kwarg. New `aelfrice.retrieval.resolve_posterior_weight()` resolves precedence env > kwarg > TOML > default; `AELFRICE_POSTERIOR_WEIGHT=<float>` env override and `[retrieval] posterior_weight = <float>` in `.aelfrice.toml`. Negative values clamp to `0.0`. `bm25 == 0` (FTS5 non-match) is floored at `PARTIAL_BAYESIAN_BM25_FLOOR = 1e-12` so `log(0)` cannot raise. `RetrievalCache` key tuple gains `posterior_weight` (rounded to four decimals via `POSTERIOR_WEIGHT_KEY_PRECISION`) so two callers passing different weights against the same store do not collide; cache invalidation is unchanged — `apply_feedback`'s `store.update_belief()` already triggers `_fire_invalidation()` and wipes the cache, no new hook in `apply_feedback`. 22 deterministic acceptance tests in `tests/test_bayesian_ranking.py` cover the 14-criterion spec (byte-identical v1.0.x at weight 0.0; equal-BM25 reranked by posterior DESC; high-BM25/low-posterior dethroned by low-BM25/high-posterior; one `apply_feedback(+1)` round promotes a rank-3 belief to ≤ 2 at default weight; lock bypass invariant across weights; cold-belief neutrality at all-prior corpus; cache hit/miss matrix; cache wiped through store callback without direct `cache.invalidate()`; bm25=0 edge case finite). Full feedback-into-ranking eval (10-round MRR uplift, ECE calibration, BM25F + heat-kernel composition, real-feedback retest) lands at v2.0.0.

### Fixed

- **`project-warm`: sentinel debounce keyed off git-common-dir, not worktree path** ([#161](https://github.com/robotrocketscience/aelfrice/issues/161)). Previously `_project_id` was derived from `git rev-parse --show-toplevel`, giving each worktree of the same repo a distinct sentinel under `~/.aelfrice/projects/<id>/.last_warm`. Two worktrees of one repo share a single DB (via `git-common-dir`), so they should share one sentinel. `resolve_project_root` now calls `git rev-parse --path-format=absolute --show-toplevel --git-common-dir` in a single subprocess and keys `ProjectRef.id` off the git-common-dir while keeping `ProjectRef.root` as the worktree working directory (for `os.chdir` in `_warm_store`). New test `test_resolve_project_root_worktrees_share_id` verifies that two worktrees of one repo produce identical `ProjectRef.id` values.
Expand Down
34 changes: 33 additions & 1 deletion docs/CONFIG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ This is the reference for power users whose project has a documentation idiom or
A single optional TOML file at the root of a project (or any ancestor). It exposes two power-user surfaces:

- `[noise]` — onboard-time belief filter. Changes how `aelf onboard` ingests beliefs; nothing else.
- `[retrieval]` (v1.3+) — retrieval-time tier toggles. At v1.3.0 there are two knobs: the entity-index (L2.5) flag and the BFS multi-hop (L3) flag.
- `[retrieval]` (v1.3+) — retrieval-time tier toggles + ranking. At v1.3.0 there are three knobs: the entity-index (L2.5) flag, the BFS multi-hop (L3) flag, and `posterior_weight` for partial Bayesian-weighted L1 ranking.

Locks, hooks, MCP tools, and the Bayesian feedback math are not affected.

Expand Down Expand Up @@ -50,6 +50,14 @@ entity_index_enabled = true
# 0.10 path-score floor; shares the unified token budget.
bfs_enabled = false

# v1.3+. Default 0.5. Posterior-weighted ranking on the L1 BM25
# tier: score = log(-bm25) + posterior_weight * log(posterior_mean).
# Set to 0.0 to reproduce v1.0.x BM25-only ordering byte-for-byte.
# AELFRICE_POSTERIOR_WEIGHT env var overrides; explicit kwargs on
# retrieve() / retrieve_v2() override TOML in turn. Locked beliefs
# (L0) bypass scoring entirely.
posterior_weight = 0.5

[onboard.llm]
# v1.3.0+. Opt in to the LLM-Haiku classifier at onboard time.
# Default: false. Requires the [onboard-llm] extra and the
Expand Down Expand Up @@ -174,6 +182,30 @@ Precedence (first decisive wins): env var `AELFRICE_ENTITY_INDEX=0` > explicit P

The on-write index is always populated regardless of this flag — disabling only affects reads. Re-enabling sees an up-to-date index without a backfill pass.

### `posterior_weight`

Float ≥ 0, default `0.5` at v1.3.0. Combines the L1 BM25 score with the Beta-Bernoulli posterior mean log-additively:

```
score = log(-bm25_raw) + posterior_weight * log(posterior_mean(α, β))
```

`-bm25_raw` flips SQLite FTS5's signed score to positive (smaller-magnitude-negative is better in SQLite; we negate before taking `log`). `posterior_mean(α, β) = α / (α+β)` reuses the existing scoring helper — Jeffreys prior, reads `0.5` for unobserved beliefs.

Behaviour at the boundaries:

- **`0.0`** — score collapses to `log(-bm25_raw)`, byte-identical to v1.0.x `ORDER BY bm25(beliefs_fts)` ordering. Use for diff-tooling and bisection.
- **`0.5`** (default) — synthetic-graph optimum from the v1.3 calibration. Posterior moves rank without overwhelming BM25.
- **`> 1.0`** — posterior dominates; high-confidence beliefs surface even on weak keyword matches. Useful when feedback density is high and BM25 noise is the limiting factor.

Locked beliefs (L0) bypass scoring entirely; the weight only reranks the L1 BM25 candidate set. L2.5 entity-index hits and L3 BFS expansions are unaffected.

Precedence (first decisive wins): env var `AELFRICE_POSTERIOR_WEIGHT=<float>` > explicit Python kwarg `posterior_weight=<float>` on `retrieve()` / `retrieve_v2()` > TOML `[retrieval] posterior_weight` > default `0.5`.

Negative values clamp to `0.0`. Non-numeric env values trace to stderr and fall through. The cache key is extended with the resolved weight (rounded to four decimals), so two callers passing different weights against the same store do not collide on a shared `RetrievalCache`.

The full feedback-into-ranking eval — 10-round MRR uplift, ECE calibration, BM25F + heat-kernel composition — lands at v2.0.0. See [`docs/bayesian_ranking.md`](bayesian_ranking.md) for the v1.3 contract and the rejected-alternatives analysis.

### `bfs_enabled`

Boolean, default `false` at v1.3.0. Toggles the L3 BFS multi-hop graph traversal retrieval tier.
Expand Down
Loading
Loading