Skip to content

feat: phantom promotion-opportunity detector (#1132) - #1134

Merged
robotrocketscience merged 6 commits into
mainfrom
feat/issue-1132-promotion-opportunity-detector
Jul 21, 2026
Merged

robotrocketscience merged 6 commits into
mainfrom
feat/issue-1132-promotion-opportunity-detector

Conversation

@robotrocketscience

@robotrocketscience robotrocketscience commented Jul 21, 2026

Copy link
Copy Markdown
Owner

What

Builds the phantom promotion-opportunity detector — the promotion-side
mirror of the #980 phantom-generation detector. Closes #1132 (Q2; Q1, the
retention-promotion speculative-exclusion fix, shipped in #1133).

When the opt-in [phantom_promotion] lane is enabled, each UserPromptSubmit
turn checks whether any phantom (origin='speculative') has crossed a
cross-session corroboration threshold and, if so, appends a small
<aelfrice-phantom-promotion-opportunity> note naming the candidate(s) and
their aelf validate <id> / aelf lock surface.

Why

The #1125 census found phantoms are essentially never promoted (0 across
seven real stores) — but not because promotion is broken. Origin promotion is,
by the ratified #229 rule, an explicit user act (aelf validate / lock);
a corroboration count is an explicit non-trigger for that write. The gap is
that nothing ever surfaces a corroborated phantom for the user to act on, so
the explicit act is never initiated.

This detector closes that gap the only way #229 permits — by surfacing, not
writing
. It preserves "the only path to promotion is explicit user typing"
while fixing "nothing prompts the typing." Origin promotion stays exactly where
#229 put it; this lane only decides when to prompt, and never promotes
autonomously (the note says so explicitly).

Design

Modeled 1:1 on the #980 generation detector so the two lanes are consistent:

Named phantom_promotion_opportunity (not _trigger) to avoid colliding with
the #550 "phantom promotion trigger" (the explicit Surface A/B origin-flip),
which is a different concept.

No clock, no network, no LLM (#605-clean).

Threshold

Defaults mirror the retention-promotion rule (belief_retention_class.md §4):
≥3 corroborations across ≥2 distinct sessions. All knobs
(max_fires_per_session, min_corroborations, min_sessions) are TOML-tunable.

Tests

  • test_phantom_promotion_opportunity.py — store predicate (threshold,
    sessions, NULL-session exclusion, non-speculative exclusion, CONTRADICTS
    exclusion, GC'd exclusion, ordering, cap, configurable threshold); config
    resolution (default-off, env, TOML knobs + wrong-type fallback); detect + note
    (incl. an escaping test that a hostile phantom containing the close tag /
    markup cannot break the data boundary); orchestrator budget / dedup / disabled
    / no-session guards.
  • test_hook_phantom_promotion_wiring.py — end-to-end UserPromptSubmit:
    default-off silence, fires when enabled, second-turn dedup, below-threshold
    silence.
  • test_session_ring_promotion.py — normalize round-trip, counter/dedup
    contract, empty-key rejection, lane independence from Phantom belief lifecycle audit + trigger-driven generation in normal turns #980.

103 tests pass across the phantom / session-ring / hook / promotion suites,
including the pre-existing #980 and #550 tests (no regressions).

Config

Default-off; a fresh install is unaffected. docs/user/CONFIG.md documents the
[phantom_promotion] section.

Summary by Sourcery

Add an opt-in phantom promotion-opportunity detector that surfaces highly corroborated speculative beliefs to users during prompt submission, without performing promotion itself.

New Features:

  • Introduce a phantom promotion-opportunity lane that appends a <aelfrice-phantom-promotion-opportunity> note on qualifying UserPromptSubmit turns when enabled via [phantom_promotion] config.
  • Add a store-level find_promotable_phantoms query to identify live speculative beliefs that meet configurable corroboration and session thresholds for promotion consideration.

Enhancements:

  • Extend session ring state with independent promotion fire counting and per-session dedup keys to bound and de-duplicate promotion-opportunity notes.
  • Document the new [phantom_promotion] configuration section, including enablement and tunable thresholds, and record the feature in the v4 changelog.

Tests:

  • Add targeted unit and integration tests covering promotion-opportunity detection logic, configuration resolution, session-ring state handling, and UserPromptSubmit wiring to ensure fail-soft, default-off behaviour.

Read-only detector for phantoms (origin='speculative') that have crossed a
cross-session corroboration threshold: origin='speculative' AND live
(valid_to IS NULL) AND corr>=N AND distinct-sessions>=M AND no inbound
CONTRADICTS, ordered by created_at. The inverse population of
find_promotable_snapshots; NULL sessions excluded from the distinct count
(same rationale). Does not promote -- under #229 a phantom's origin advances
only through explicit user acknowledgment; this feeds the promotion-
opportunity note that surfaces the candidate for that act.
Add promotion_fires (per-session note budget) and promotion_dedup
(belief-ids already surfaced) to the session ring, independent of the #980
generation budget so the two opportunity lanes bound their notes separately.
record_promotion_fire / read_promotion_state mirror the phantom equivalents;
_normalize_for_session seeds + coerces the new fields (default for pre-#1132
rings, no migration). Direct unit tests cover the normalize round-trip, the
counter/dedup contract, and lane independence.
…1132)

The promotion-side mirror of the #980 generation detector. When the opt-in
[phantom_promotion] lane is on, each UserPromptSubmit turn surfaces phantoms
that have crossed the corroboration threshold as an
<aelfrice-phantom-promotion-opportunity> note naming the candidate and its
aelf validate / lock surface.

Note-not-write: origin promotion stays the explicit user act #229 mandates
(a corroboration count is a non-trigger for the write); this lane only
decides WHEN to prompt, never promotes autonomously. Closes the #1125-census
gap -- phantoms were never promoted because nothing surfaced a corroborated
one for the explicit act, not because promotion is broken.

New pure aelfrice.phantom_promotion_opportunity module: config (env >
kwarg > TOML > default-off, mirroring [phantom_generation]), the
store-backed detector, a per-session budget+dedup orchestrator, and an
XML-escaped note. Wired as a fail-soft UserPromptSubmit lane parallel to
#980. Default-off; no clock, network, or LLM (#605-clean).
Document the [phantom_promotion] TOML section (enabled / max_fires_per_session
/ min_corroborations / min_sessions) in CONFIG.md as the promotion-side mirror
of [phantom_generation]. Add [Unreleased] entries: the Q2 detector (Added) and
the Q1 retention-promotion speculative-exclusion fix (Fixed).
@robotrocketscience robotrocketscience added the author-Kulili PR coordination mutex label Jul 21, 2026
@sourcery-ai

sourcery-ai Bot commented Jul 21, 2026

Copy link
Copy Markdown

Reviewer's Guide

Adds an opt-in, fail-soft phantom promotion-opportunity detector and wiring: a store-level query to find corroborated speculative beliefs, a promotion lane in the UserPromptSubmit hook, and per-session budget/dedup state in the session ring, plus documentation and tests.

Sequence diagram for phantom promotion-opportunity lane in UserPromptSubmit

sequenceDiagram
    actor User
    participant Hook as user_prompt_submit
    participant Promo as _maybe_phantom_promotion_block
    participant Config as load_phantom_promotion_config
    participant Store as MemoryStore
    participant Eval as evaluate_promotion_opportunities

    User->>Hook: submit prompt
    Hook->>Promo: _maybe_phantom_promotion_block(session_id, cwd, stderr)
    Promo->>Config: load_phantom_promotion_config(start=cwd)
    Config-->>Promo: PhantomPromotionConfig
    Promo->>Promo: [config.enabled?]
    alt promotion disabled or in memory store
        Promo-->>Hook: ""
    else promotion enabled
        Promo->>Store: MemoryStore(db_path)
        Promo->>Eval: evaluate_promotion_opportunities(store, session_id, config, stderr)
        Eval-->>Promo: opportunities
        Promo->>Store: close()
        Promo-->>Hook: format_promotion_note(opportunities)
        Hook->>User: write promotion note block
    end
Loading

File-Level Changes

Change Details Files
Introduce per-session promotion-opportunity tracking in the session ring and helpers to mutate/read it.
  • Extend normalized session-ring state with promotion_fires counter and promotion_dedup list, with safe defaults and type/range validation.
  • Add record_promotion_fire to increment the promotion counter and maintain a deduplicated key list, failing softly on bad input.
  • Add read_promotion_state to expose promotion_fires and promotion_dedup with defensive normalization, independent from the phantom-generation lane.
src/aelfrice/session_ring.py
tests/test_session_ring_promotion.py
Add a store-level detector for promotable phantoms based on corroboration and session thresholds. src/aelfrice/store.py
tests/test_phantom_promotion_opportunity.py
Wire a phantom promotion-opportunity lane into the UserPromptSubmit hook.
  • Add _maybe_phantom_promotion_block helper that resolves config, opens the store, evaluates promotion opportunities, formats a note, and returns it or an empty string on failure.
  • Invoke _maybe_phantom_promotion_block after the existing phantom-generation lane in user_prompt_submit, writing the resulting block to stdout when present and keeping the hook fail-soft and default-off.
src/aelfrice/hook.py
tests/test_hook_phantom_promotion_wiring.py
Provide a dedicated phantom_promotion_opportunity module for config resolution, detection orchestration, and note formatting.
  • Define PhantomPromotionConfig and config-resolution helpers that honor env > explicit kwarg > TOML > default-off precedence, plus TOML-only numeric knobs with safe fallbacks.
  • Implement detect_promotable_phantoms and evaluate_promotion_opportunities to query candidates, apply per-session budget and dedup using session-ring state, and record fires, never promoting autonomously.
  • Implement XML-escaped note formatting, including hostile-content escaping and bounded topic truncation, framed as data, not instructions.
src/aelfrice/phantom_promotion_opportunity.py
tests/test_phantom_promotion_opportunity.py
Document configuration and changelog entries for the phantom promotion-opportunity detector.
  • Add a new [phantom_promotion] section to CONFIG.md describing the opt-in flag, thresholds, per-session budget, and fail-soft behavior, mirroring phantom_generation.
  • Update v4 changelog to describe the detector, its thresholds, its adherence to the explicit-typing rule, and the new module and wiring.
  • Note the fix to ensure corroboration-driven retention promotion no longer affects phantoms, keeping origin promotion explicit.
docs/user/CONFIG.md
CHANGELOG/v4.md

Assessment against linked issues

Issue Objective Addressed Explanation
#1132 Adjust or explicitly document retention promotion so corroboration-driven snapshot→fact promotion does not unintentionally apply to speculative-origin (phantom) beliefs, and add tests to lock in the chosen behavior. This PR does not modify the retention-promotion path or find_promotable_snapshots; it only mentions in the changelog that the speculative-origin exclusion fix shipped separately in #1133. No code or tests here change or document the retention behavior.
#1132 Implement an explicit, opt-in phantom promotion-opportunity detector that identifies corroborated speculative-origin beliefs and surfaces them as a note, without performing promotion automatically, including store predicate, session-ring state, hook wiring, configuration, documentation, and tests.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@robotrocketscience, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 34 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 712f53b7-166b-410b-82fb-78d3d670b173

📥 Commits

Reviewing files that changed from the base of the PR and between 1e06c11 and 3421cd7.

📒 Files selected for processing (9)
  • CHANGELOG/v4.md
  • docs/user/CONFIG.md
  • src/aelfrice/hook.py
  • src/aelfrice/phantom_promotion_opportunity.py
  • src/aelfrice/session_ring.py
  • src/aelfrice/store.py
  • tests/test_hook_phantom_promotion_wiring.py
  • tests/test_phantom_promotion_opportunity.py
  • tests/test_session_ring_promotion.py
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/issue-1132-promotion-opportunity-detector

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@robotrocketscience robotrocketscience added the attn:review Needs review (PR open, awaiting reviewer) label Jul 21, 2026
@github-actions

github-actions Bot commented Jul 21, 2026

Copy link
Copy Markdown

PR-size soft cap

This PR is over the advisory size threshold:

  • 1169 changed lines (limit: 200)
  • 9 changed files (limit: 3)

Bigger PRs collide with more open work, which under the parallel-session workflow tends to produce repeated attn:merge-conflict cycles (see #602). When practical, split into smaller PRs that each touch a focused surface.

This is advisory only — nothing is blocked. If the size is intentional (large refactor, module removal, generated code), apply the size:override label and this comment will be removed on the next push.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've left some high level feedback:

  • In load_phantom_promotion_config, you call should_trigger_phantom_promotion(start=...) which re-parses .aelfrice.toml via _read_section; consider passing the already-read section into the flag resolver or factoring out the env/TOML parsing so the file is only read once per call.
  • In format_promotion_note, you pass opp.topic through _note_topic, which internally calls _truncate again even though topic was already truncated when building the PromotionOpportunity; you can simplify by assuming topic is already bounded and only escaping here.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `load_phantom_promotion_config`, you call `should_trigger_phantom_promotion(start=...)` which re-parses `.aelfrice.toml` via `_read_section`; consider passing the already-read section into the flag resolver or factoring out the env/TOML parsing so the file is only read once per call.
- In `format_promotion_note`, you pass `opp.topic` through `_note_topic`, which internally calls `_truncate` again even though `topic` was already truncated when building the `PromotionOpportunity`; you can simplify by assuming `topic` is already bounded and only escaping here.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

The typos CI check reads 'opps' as a misspelling of 'oops'. Spell out the
local variable in the detect test.
@robotrocketscience robotrocketscience added the ready-to-merge Trigger merge-train: FF main to this PR's head label Jul 21, 2026
@github-actions

Copy link
Copy Markdown

merge-train: blocked

required check(s) failed: Sourcery review. Fix CI and re-add the label.

The ready-to-merge label has been removed. Address the issue above and re-add the label when you're ready for another attempt.

@github-actions github-actions Bot removed the ready-to-merge Trigger merge-train: FF main to this PR's head label Jul 21, 2026
- load_phantom_promotion_config resolves 'enabled' from the already-read
  TOML section via a shared _resolve_enabled helper, instead of re-reading
  the file through should_trigger_phantom_promotion (one file read per call).
- _note_topic escapes only; the topic is already whitespace-collapsed and
  length-bounded by _truncate when the PromotionOpportunity is built, so the
  double truncation is dropped.
@robotrocketscience

Copy link
Copy Markdown
Owner Author

@sourcery-ai review

@sourcery-ai

sourcery-ai Bot commented Jul 21, 2026

Copy link
Copy Markdown

Sorry @robotrocketscience, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@robotrocketscience
robotrocketscience merged commit 3421cd7 into main Jul 21, 2026
22 of 24 checks passed
@robotrocketscience
robotrocketscience deleted the feat/issue-1132-promotion-opportunity-detector branch July 21, 2026 01:30
@robotrocketscience

Copy link
Copy Markdown
Owner Author

Merged via local FF-push (the sanctioned wedged-bot fallback), not the merge-train. Rationale: the merge-train treats Sourcery review as required, but Sourcery hit its weekly rate limit ("reached your weekly rate limit of 500000 diff characters") and could not re-review after I addressed both of its high-level nits (single TOML read in load_phantom_promotion_config; escape-only _note_topic, commit 3421cd7). The stale red check was a rate-limit artifact, not a code issue. Sourcery review is not in the branch-protection ruleset's required checks (history-scan, pattern-scan, pytest 3.12/3.13, secrets-scan — all green), all six commits are signed, and the branch was FF on main. Closes #1132.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

attn:review Needs review (PR open, awaiting reviewer) author-Kulili PR coordination mutex

Projects

None yet

Development

Successfully merging this pull request may close these issues.

R&D: phantom promotion — origin path is 0-by-design (#229); retention path reaches speculative rows on a count trigger

1 participant