Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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/v3.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- **Cadence shadow-evaluation mode — direct P1 vs P2 comparison on identical workload** ([#875](https://github.com/robotrocketscience/aelfrice/issues/875), unblocks the [#749](https://github.com/robotrocketscience/aelfrice/issues/749) campaign comparison). The shipped policy module exposes `policy` as a single-value enum: exactly one cadence runs per project at a time. That mutex blocks the campaign's stated goal of scoring P1 vs P2 vs P3 — scoring today requires longitudinal flip-and-rebake with workload-drift confound. This surface lifts that constraint without changing live firing behavior. New `would_fire_p1` and `would_fire_p2` policy-agnostic predicates (return `(bool, reason)`, do not check `config.policy`); existing `should_fire` / `should_fire_p2` are re-expressed as policy-check + delegate, preserving behavior. New `[cadence] shadow_mode_enabled` TOML opt-in (default OFF) + `AELFRICE_CADENCE_SHADOW_MODE_ENABLED` env override; when on, the Stop hook evaluates every implemented policy's `would_fire_*` per tick and appends one JSON row to `<project>/.git/aelfrice/cadence_shadow/<session_id>.jsonl` carrying `{ts, session_id, selected, fired, shadow: {p1_every_k_turns: {would_fire, reason}, p2_ctx_threshold: {would_fire, reason}}}`. Selected policy still drives the live rebuilder fire; shadow rows are diagnostic only. Failure-isolated: any exception in the shadow path traces stderr and returns; live dispatch is unaffected. Gated behind `[cadence] enabled = true` so a stray shadow flag on disabled cadence doesn't write surprise disk content. New `aelf cadence-score` hidden CLI subcommand wraps `src/aelfrice/cadence_score.py` and emits a per-policy fire-rate + 2x2 P1/P2 agreement matrix over the on-disk shadow log; `--project PATH` (default cwd), `--session SESSION_ID` filter, `--json` for machine-readable output. Tests: 8 hook integration cases (default-off no-write, P1-no-fire, P1-fire, P2-selected logs both, policy-off logs fired=false even when P1 would have fired, two-tick append, cadence-disabled no-write, env override), 14 scoring module unit cases (empty, totals, timestamp range, per-policy counts, selected fire-rate, agreement matrix, session filter, malformed-decision skip, missing-dir tolerance, JSONL edge cases, JSON roundtrip), 7 resolver cases, 7 shadow-log helper cases, 16 `would_fire_*` purity cases, 5 CLI surface cases. Total 229 cadence + hook-cadence + scoring + cli tests pass; zero regressions on the 188 pre-existing cadence tests.

- **`use_intentional_clustering` × `use_type_aware_compression` compose-reconciliation** ([#878](https://github.com/robotrocketscience/aelfrice/issues/878), unblocks [#769](https://github.com/robotrocketscience/aelfrice/issues/769) flip-default). `retrieve_v2` used to raise `ValueError` when both flags resolved True — the cluster pack accounted in raw `_belief_tokens` while the surrounding pack loop accounted in the compressed render's `rendered_tokens`, and composing two different cost models would have drifted the byte budget. `pack_with_clusters` (`src/aelfrice/clustering.py`) now accepts a `cost_fn: Callable[[Belief], int] | None = None` parameter; `retrieve_v2` passes its existing `_cost` closure through that seam so both arms account in the same currency (raw token estimate or compressed `rendered_tokens` depending on `compress_on`). The `cluster_on and compress_on` mutex raise is dropped. File-level comment and `retrieve_v2` docstring updated to reflect compose-compatibility. Test surface: `test_mutual_exclusion_with_compression_raises` replaced with `test_composes_with_compression` (composed path produces non-empty `beliefs` and parallel `compressed_beliefs`); `test_compressed_beliefs_populated_when_flag_on` drops its `use_intentional_clustering=False` workaround so the default-on clustering path is the one under test. Determinism ([#605](https://github.com/robotrocketscience/aelfrice/issues/605)) preserved: `cost_fn` defaults to the raw estimator when unset, so callers that don't opt into compression see byte-identical pack behaviour from this commit. Named "v2.x follow-up" by [#436](https://github.com/robotrocketscience/aelfrice/pull/436)'s default-on flip body — landing it here unblocks [#769](https://github.com/robotrocketscience/aelfrice/issues/769) (compression default-on flip) which was gated empirically (bench evidence A2 +0.3267 / A4 +0.0085) but structurally blocked on this reconciliation.

- **UPS-side cadence — in-session `<cadence-checkpoint>` injection** ([#870](https://github.com/robotrocketscience/aelfrice/issues/870), follow-up to [#869](https://github.com/robotrocketscience/aelfrice/pull/869) and [#871](https://github.com/robotrocketscience/aelfrice/issues/871)). Closes the loop the [#749](https://github.com/robotrocketscience/aelfrice/issues/749) body framed for in-session cadence: where [#869](https://github.com/robotrocketscience/aelfrice/pull/869) fires P1 at end of turn but Stop has no `additionalContext` channel (so the rebuilder body only reaches the next session via [#871](https://github.com/robotrocketscience/aelfrice/issues/871)'s resume cache after a `/clear`), this surface adds a sibling fire at start of UserPromptSubmit. New helper `_maybe_run_ups_cadence_checkpoint(payload, session_id, serr)` mirrors the Stop-side dispatch: resolves `[cadence] enabled` / `policy` / `k` / `ctx_threshold` / `ctx_byte_window` via the same env > kwarg > TOML > default resolvers; reads `next_fire_idx` from `session_ring.read_ring_state` *before* this turn's `_ring_append_ids` runs, so UPS sees the same fire_idx Stop saw at end of the prior turn (the two consumers fire on the same monotonic-counter boundary by construction); runs `should_fire` (P1) or `should_fire_p2`; on True calls the shared `_run_cadence_rebuild(payload, cwd)` to get the formatted rebuilder body; returns the body to the caller. UPS wraps the returned body in `<cadence-checkpoint>...</cadence-checkpoint>` (distinct tag from [#871](https://github.com/robotrocketscience/aelfrice/issues/871)'s `<cadence-resume>` first-prompt mechanism) and writes it to stdout *ahead* of any per-prompt retrieval body — augment mode, not replacement: both blocks emit when both apply, with the broader cadence synthesis priming the model before the query-specific BM25 hits land. Stop-side keeps its existing role (writes the resume cache for `/clear`-and-resume); UPS does **not** write the cache, so the cache stays single-sourced. Stderr observability: `aelfrice: ups cadence checkpoint fired @ fire_idx=N (policy=..., k=...)`. Default-OFF inherited from [#869](https://github.com/robotrocketscience/aelfrice/pull/869); an unset `[cadence] enabled` returns None before any ring read. Fail-soft: any error inside the dispatch returns None and the wider UPS flow continues unaffected. Determinism ([#605](https://github.com/robotrocketscience/aelfrice/issues/605)): P1 firing predicate is pure over `(fire_idx, k)`; P2 firing predicate is pure over `(transcript_bytes, last_user_prompt, config)`; no wall-clock, no random sampling — replay with the same inputs produces the same fire decisions. The two firing sites can fire on the same boundary (Stop end-of-turn-N, UPS start-of-turn-(N+1), same `next_fire_idx`); compute duplicates by one rebuilder pass per K turns — acceptable cost for the surface split. 11 unit tests in `tests/test_hook_ups_cadence.py` target the dispatch in isolation (empty session_id short-circuit, disabled-by-default, policy-off, P1 multiples-of-K cross-product, fire_idx=0 cold-start guard, missing ring state, empty recent-turns no-rebuild, missing DB, P2 boundary+threshold fire, P2 without-boundary no-fire, env override). Five integration tests in `tests/test_hook_ups_cadence_wiring.py` exercise `user_prompt_submit` end-to-end: disabled emits no block; P1 boundary emits `<cadence-checkpoint>` on stdout; P1 non-boundary emits no block; augment-mode ordering check (cadence block precedes retrieval hit on stdout); missing session_id skips cleanly. Full hook + cadence test surface: 590 passed, 3 skipped.
Expand Down
Loading
Loading