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
1 change: 1 addition & 0 deletions CHANGELOG/v4.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **The LLM consent sentinel survived `aelf uninstall`, so a reinstall never re-prompted ([#1186](https://github.com/robotrocketscience/aelfrice/issues/1186)).** #1173 made disposition artifact-complete for the *store* directory and deliberately left `~/.aelfrice/` alone, because `projects/` there holds every other project's belief corpus and a blanket sweep would destroy corpora the command was not asked to touch. The cost was that every package-written path in that directory outlived the uninstall — decisively the consent sentinel, which records that the user agreed to send belief content to the vendor API (scoped per data class since #1172). Purge the store, `pip uninstall`, reinstall a month later, and the surviving sentinel was still valid for the same model + major version: the gate passed and no prompt appeared. Consent outlived the uninstall that was meant to end the relationship — the #1172 defect class one layer out. `~/.aelfrice/` is now enumerated **by name** rather than swept, in three dispositions. *Install state* goes in every mode, `--keep-db` included, since each file records that a step already happened and a survivor makes a reinstall read a stale decision as current: the consent sentinel, the manifest-version and uv-migration stamps (previously the only two cleared), the temporal-spine backfill sentinel, the auto-install lock, the `claude-memory` reconcile sentinel, and `logs/hook-failures.log` (with `logs/` pruned once empty, never `rmtree`d — a stranger's file in there is left alone). *Captured data* — `telemetry.jsonl` and the legacy `transcripts/` — goes only under `--purge`/`--archive`, matching the store-directory contract, and is reported as kept under `--keep-db`. *Preserved* in every mode: `projects/`, `shared/` (read-only federation peers per #655 — another store's corpus by another name, which the issue's own inventory missed), `config.json`, and `opt-out-hooks.json`. Anything else found there is listed and never deleted, and both destructive gates now disclose the `~/.aelfrice/` paths alongside the store manifest before prompting, because deleting a path the manifest never named is the #1173 defect one directory over. The removal *and* keep lists are single-sourced against their owning modules' constants by agreement tests, the same pattern #1185 used for the store siblings; `auto_install` gained an `AUTO_INSTALL_LOCK_FILENAME` constant so the lock could be named rather than re-hardcoded. `dotdir_plan` (read-only, used for the disclosure) and `dispose_dotdir` take the home directory as a required argument, so no caller can sweep a developer's own `~/.aelfrice/` by omission, and the store artifact set is passed as `skip` so the two dispositions cannot double-report the same path on the non-git fallback, where `~/.aelfrice/` *is* the store directory.
- **`aelf doctor --classify-orphans` sent stored belief content with no consent prompt ([#1172](https://github.com/robotrocketscience/aelfrice/issues/1172)).** The documented boundary is four gates, the fourth being a recorded consent sentinel. `check_gates` covers only the first three and leaves gate 4 to the caller; `aelf onboard` wired it, `--classify-orphans` did not — it hardcoded `enabled=True`, never read the sentinel, and transmitted the content of every orphan belief to the vendor API. With the `[onboard-llm]` extra installed and `ANTHROPIC_API_KEY` exported (both common), a *diagnostic* subcommand shipped the memory store outbound with no prompt, and because no sentinel was ever written, `aelf doctor revoke-llm-consent` could not prevent it. Consent is now **scoped**: a sentinel records which data classes the accepted prompt disclosed. `aelf onboard` grants `onboard_candidates`; `--classify-orphans` requires `stored_beliefs` and prompts separately with a disclosure naming stored belief content — the onboard text never did, since it enumerates document sentences, commit subjects and docstrings and promises "nothing outside the extracted candidate text". Pre-existing sentinels have no `scopes` key and are read as onboard-only, so they do not grandfather this path. Granting either scope preserves the other. `--dry-run` still previews the candidate set with no gate check and no network call.
- **Replay re-derived every transcript belief with the wrong origin and a 5× deflated prior ([#1167](https://github.com/robotrocketscience/aelfrice/issues/1167)).** `replay_full_equality` built its `DerivationInput` with `raw_meta=None`, under a comment that had been false since [#888](https://github.com/robotrocketscience/aelfrice/issues/888): `derive()` reads `raw_meta["role"]` to route `source_kind=transcript` user turns to the undeflated user-source prior and `origin=user_transcript`. Transcript ingest is the dominant production write path, so every belief it produced re-derived as `agent_inferred` at 1/5 the alpha — `aelf doctor --replay` reported drift on any healthy store and its drift signal was pure noise. The log already persists `raw_meta` verbatim, so passing it through makes the round-trip exact. **The probe was also widened.** It compared 3 of ~21 belief columns and dropped the rest silently, which is why the alpha half of this divergence was invisible while its origin half was flagged: a new `mutable_divergence` counter plus a per-field breakdown over `alpha`, `beta`, `lock_level`, `retention_class`, `scope`, `last_retrieved_at` and the edge set is now reported by `aelf doctor --replay` and recorded in the soak history. Those fields stay **informational, never drift** — each is legitimately rewritten after ingest (feedback, `aelf lock`, the snapshot lifecycle, federation, retrieval stamping) and none is reconstructible from the write log as it stands, so counting them as drift would fire on every live store; they can be promoted into the strict contract once the log is total ([#1157](https://github.com/robotrocketscience/aelfrice/issues/1157)). The replay-soak corpus, whose 76 rows all carried `raw_meta: null` and so could not observe any metadata-reading branch, gains four `raw_meta` rows covering the user-transcript path and its two negative cases.
- **Valence propagation attenuated by the wrong belief, double-counted fan-in, and depended on edge insertion order ([#1169](https://github.com/robotrocketscience/aelfrice/issues/1169)).** Four compounding defects in `store.propagate_valence`, the BFS a feedback event walks to reach related beliefs. (1) **Attenuation direction.** The multiplier was the *recipient's* posterior while the docstring called it broker confidence, so the evidence a belief received was proportional to how confident it already was — rich-get-richer on positive signal, and low-confidence junk structurally shielded from negative signal because its small factor pushed the delta under `min_threshold` (a plausible mechanism for the recorded junk-percolation ranking inversion). It now uses the confidence of the belief the signal travels *through*, and `apply_feedback` supplies the source's confidence as of *before* the event so an event cannot amplify its own propagation. (2) **Fan-in and cycles.** Delivery accumulated per in-edge, outside the `visited` guard: a diamond delivered 2×, a 5-way fan-in 5×, so one `aelf confirm` on the root of a convergent subgraph could add α += 20 at the convergence point — and because the source guard covered only the source, any bidirectional edge pair between two non-source beliefs re-delivered to them on every event. Each belief is now credited exactly once per walk, at the shallowest hop that reaches it, by the strongest path within that hop — shortest-path-wins rather than largest-magnitude-wins, since a longer chain of strong edges can carry more than a short weak one and the shallower path is still the one taken. (3) **Determinism.** `edges_from` had no `ORDER BY`, so row order was physical insertion order and the delta delivered to a node could differ 3.3× purely from edge insertion sequence — a determinism leak on a default-on path that `VACUUM` or a migration would move. `edges_from` (and its peer-scope sibling) now order by `(dst, type)`, and within a hop candidates rank by `(-abs(delta), dst)`. (4) **Unbounded mass.** Total injected evidence per event is now capped, defaulting to `abs(valence) * max_hops`, taking the strongest paths first. A new hypothesis property test asserts, for any graph shape, that mass stays within budget, the source is never a recipient, no single delta exceeds the source valence, and the output is invariant to edge insertion order.
- **Codex `$aelf-onboard` classifies on a low-tier model, not the session model ([#1153](https://github.com/robotrocketscience/aelfrice/issues/1153)).** onboard's bulk sentence-classification fan-out defaults to a low-cost model tier (tier-abstract since [#1155](https://github.com/robotrocketscience/aelfrice/issues/1155)). The Codex skill generator carried the slash body verbatim with only a generic "use Codex's own subagent mechanism" note, so on Codex a bare "low-cost tier" default had nothing concrete to resolve to and the work fell through to the session's default model (e.g. `gpt-5.6-luna`): the expensive, slow path for short-label classification. `codex_skill_from_slash` now appends, for the onboard skill, an adapter note resolving that default to Codex's cheapest fast tier (a `-mini`-class model) rather than the session default, without pinning a model id (names drift). Deterministic transform; the note is onboard-specific, so subagent skills with no model directive (reason/wonder) don't carry it. Surfaced during the #1056 live validation.
- **Codex host doctor/setup no longer report the `hooks` feature as off ([#1151](https://github.com/robotrocketscience/aelfrice/issues/1151)).** `host_codex.py` probed a retired Codex feature-flag name (`codex_hooks`) and, because a default-on feature is absent from `config.toml`, reported it "off" — so `aelf doctor --host codex` and `aelf setup --host codex` warned that Codex would run no hooks (and printed `codex features enable codex_hooks`, which errors) even while the hooks demonstrably fire. Surfaced by the live #1056 validation on Codex 0.145.0. The probe now keys on the current `hooks` feature (stable, on by default since the 0.11x–0.12x `codex_hooks` under-development flag was promoted), treats an unmentioned feature as its default (on) rather than off, still honours an explicit legacy `codex_hooks = true`, and warns only when `[features].hooks = false` is set explicitly. Setup guidance names `hooks` and the `--dangerously-bypass-hook-trust` automation path. Doc/message + detection only; hook wiring unchanged.
- **The nightly reproducibility band-check reported success when it had measured nothing ([#1160](https://github.com/robotrocketscience/aelfrice/issues/1160)).** `tolerance.summarize` returned PASS whenever no leaf was FAIL or WARN. Ignoring an *individual* SKIP is correct and ratified (#479) — one uncomputable metric is not a regression — but the rollup ignored *every* SKIP, so a run in which nothing could be computed rolled up to PASS. That is precisely the shape a failed dataset download on the runner takes: every adapter exits because its data dir is absent, every leaf becomes SKIP, counts read `{pass: 0, warn: 0, fail: 0, skip: 12}`, and the verdict was PASS. An empty check list returned PASS as well, and `bench-canonical.yml` exited 0 on anything that was not `fail`, so the cron went green having compared nothing at all. PASS is a claim that something was measured and stayed in band, so it now requires at least one leaf that actually passed; otherwise the rollup is a new `NO_DATA` verdict, and the workflow exits 1 on it. #479's behaviour is preserved exactly — a SKIP beside any real PASS still rolls up to PASS, and FAIL and WARN still dominate, because a leaf that was compared to its band is evidence that measurement happened (so WARN with no PASS stays WARN, not `NO_DATA`). `NO_DATA` is a distinct verdict rather than reusing FAIL because the two demand different responses: a regression means read the diff, no data means fix the runner. Leaf tallying also moved to `.get` so a leaf carrying an unexpected verdict is counted and reported instead of raising `KeyError` inside the gate whose job is to report it. **Not covered:** the same acceptance criterion also asks for one-sided bands so that an improvement is not classified as a failure. That needs a per-metric direction table — quality metrics improve upward, latency metrics downward — and guessing a direction would make the gate blind to regressions in the wrong direction, which is the same class of defect this work exists to remove. Left for a deliberate decision rather than inferred from metric names.
Expand Down
13 changes: 12 additions & 1 deletion src/aelfrice/feedback.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,18 @@ def apply_feedback(
)

if update_posterior and propagate and _propagation_enabled():
deltas = store.propagate_valence(belief_id, valence)
# #1169: the first hop is attenuated by the source's confidence as
# it was *before* this event. Letting propagate_valence read the
# row back would fold this event's own increment into the strength
# of its own propagation.
prior_denom = prior_alpha + prior_beta
deltas = store.propagate_valence(
belief_id,
valence,
src_confidence=(
(prior_alpha / prior_denom) if prior_denom > 0 else 0.0
),
)
# Sorted for a deterministic feedback_history row order
# regardless of edge-iteration order inside the BFS.
for dst_id, delta in sorted(deltas.items()):
Expand Down
Loading
Loading