Skip to content

fix(hook): inject rebuild block at SessionStart(source=compact), not PreCompact (#1031) - #1032

Merged
github-actions[bot] merged 2 commits into
mainfrom
fix/issue-1031-precompact-sessionstart-rebuild
Jun 30, 2026
Merged

github-actions[bot] merged 2 commits into
mainfrom
fix/issue-1031-precompact-sessionstart-rebuild

Conversation

@robotrocketscience

@robotrocketscience robotrocketscience commented Jun 30, 2026

Copy link
Copy Markdown
Owner

Closes #1031.

Problem

On every /compact, the aelf-pre-compact-hook failed the host harness's hook-output validation and its entire <aelfrice-rebuild> block was discarded — the context-rebuilder was a silent no-op on compaction. Observed live:

PreCompact [.../aelf-pre-compact-hook] failed: Hook JSON output validation failed — (root): Invalid input
output: {"hookSpecificOutput": {"hookEventName": "PreCompact", "additionalContext": "<aelfrice-rebuild>…"}}

Root cause: the host harness does not accept additionalContext under hookSpecificOutput for PreCompact. PreCompact is absent from the canonical list of context-injecting events (SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, Stop, …). The wire shape was copied from the PreToolUse search-tool hook, where additionalContext is valid. The PreCompact hook exists only for this injection (transcript rotation is a separate hook), so it was 100% dead weight producing a rejected output.

Fix

Move the rebuild-block injection to the SessionStart hook gated on source == "compact":

  • SessionStart does inject context (the locked <aelfrice-baseline> block already ships via its raw stdout), and source == "compact" fires after compaction completes — verified live (SessionStart:compact succeeded in the same session the PreCompact hook was rejected).
  • The canonical turns.jsonl log is append-only and survives compaction, so the recent-turn tail reads identically.
  • Trigger-mode gating is preserved in the shared helper: manual → no rebuild; threshold → emit. The rebuild stays gated on source == "compact" (a normal startup SessionStart never emits it).
  • The PreCompact hook is now silent on stdout (kept for trigger-mode parity / the dynamic-mode parked trace).
  • Explicit aelf rebuild / /aelf:rebuild is unchanged (drives rebuild_v14 directly).

Tests

  • pre_compact exit-contract tests retained; new test asserts it stays silent even with turns + hits + threshold mode.
  • The rebuild-emission behavior tests (log preference, host-transcript fallback, tier ordering, determinism, threshold-fires) moved from pre_compact to session_start(source="compact") across test_hook_pre_compact.py, test_rebuilder_triggers.py, test_context_rebuilder_hook.py.
  • New gating tests: manual-mode skip and non-compact-source skip at SessionStart.

Verification

  • Full suite: 5422 passed, 66 skipped, 75 xfailed.
  • uvx vulture src --min-confidence 80: no new findings.
  • uvx typos: clean.

🤖 Generated with Claude Code

Summary by Sourcery

Move the context-rebuilder injection from the PreCompact hook to SessionStart for compaction flows and make PreCompact stdout a no-op while preserving trigger-mode behavior.

Bug Fixes:

  • Ensure the <aelfrice-rebuild> block is accepted by the host harness after compaction by emitting it via SessionStart(source="compact") instead of PreCompact.

Enhancements:

  • Centralize rebuild-block construction in a shared helper used by SessionStart, maintaining trigger-mode gating and store/transcript resolution.
  • Extend SessionStart payload handling to respect the source field and gate rebuild emission on source == "compact" while leaving normal startups unchanged.

Documentation:

  • Document the compaction rebuild-path fix and PreCompact behavior change in the v3 changelog.

Tests:

  • Retarget context-rebuilder, trigger-mode, and ordering tests from PreCompact to SessionStart(source="compact"), and add coverage that PreCompact remains silent and non-compact SessionStart emits no rebuild block.

Summary by CodeRabbit

  • Bug Fixes
    • Fixed compaction so the rebuild content now appears during /compact as expected.
    • Ensured the rebuild output is preserved across compaction and reflects the most recent turns consistently.
    • Kept the manual rebuild command behavior unchanged.
    • Improved handling so rebuild output is no longer lost when compaction runs.

…PreCompact (#1031)

The PreCompact hook emitted the <aelfrice-rebuild> block as
hookSpecificOutput.additionalContext, but the host harness does not
accept additionalContext from a PreCompact hook (PreCompact is absent
from the canonical list of context-injecting events). The harness
rejected the output with a validation error on every /compact and
discarded the entire block, making the context-rebuilder a silent no-op.

Move the injection to the SessionStart hook on source==compact, which
fires after compaction and which the harness honors (same raw-stdout
channel the locked baseline already uses). The turns.jsonl log survives
compaction, so the recent-turn tail is read identically. Neuter the
PreCompact hook's stdout emission (retained only for trigger-mode parity
and the dynamic-mode parked trace). trigger_mode=manual still suppresses
the automatic path; explicit aelf rebuild is unchanged. Tests for the
rebuild emission move from pre_compact to session_start(source=compact).
@sourcery-ai

sourcery-ai Bot commented Jun 30, 2026

Copy link
Copy Markdown

Reviewer's Guide

Moves the context-rebuilder’s <aelfrice-rebuild> injection from the PreCompact hook (whose additionalContext output is rejected by the harness) to the SessionStart hook when source == "compact", neuters PreCompact’s stdout behavior, and refactors tests and helpers to assert the new behavior and trigger-mode gating.

Sequence diagram for context-rebuilder injection moving from PreCompact to SessionStart

sequenceDiagram
    participant Harness
    participant pre_compact
    participant session_start
    participant _build_rebuild_block_from_payload
    participant _rebuild_and_format

    Harness->>pre_compact: pre_compact(stdin, stdout, stderr)
    pre_compact-->>Harness: return 0 (no stdout)

    Harness->>session_start: session_start(stdin, stdout, stderr)
    session_start->>session_start: _extract_session_id
    session_start->>session_start: _parse_pre_compact_payload
    alt [source == compact]
        session_start->>_build_rebuild_block_from_payload: _build_rebuild_block_from_payload(payload)
        _build_rebuild_block_from_payload->>_build_rebuild_block_from_payload: load_rebuilder_config
        alt [trigger_mode == threshold]
            _build_rebuild_block_from_payload->>_build_rebuild_block_from_payload: _read_recent_for_pre_compact
            _build_rebuild_block_from_payload->>_build_rebuild_block_from_payload: db_path
            _build_rebuild_block_from_payload->>_rebuild_and_format: _rebuild_and_format(recent, token_budget,...)
            _rebuild_and_format-->>_build_rebuild_block_from_payload: rebuild_block
            _build_rebuild_block_from_payload-->>session_start: rebuild_block
            session_start->>Harness: write rebuild_block to stdout
        else [trigger_mode != threshold or skip]
            _build_rebuild_block_from_payload-->>session_start: ""
        end
    else [source != compact]
        session_start-->>Harness: baseline-only stdout
    end
    session_start-->>Harness: return 0
Loading

File-Level Changes

Change Details Files
Neuter PreCompact hook and route rebuild emission through SessionStart(source="compact").
  • Remove use of emit_pre_compact_envelope and any rebuild emission from the PreCompact hook, making it always return 0 and never write to stdout.
  • Add payload parsing and source handling to session_start, including a new _build_rebuild_block_from_payload helper that encapsulates rebuilder config, trigger-mode gating, recent-turn resolution, and store checks.
  • Teach session_start to append the rebuild block to its stdout only when source == "compact", preserving baseline emission and ensuring non-blocking behavior on errors.
src/aelfrice/hook.py
Update PreCompact and SessionStart hook tests to cover the new injection path and PreCompact’s silence.
  • Rename and repurpose helpers in test_hook_pre_compact to work with SessionStart stdout, including _rebuild_block and _start_compact, and remove JSON-envelope parsing logic.
  • Add/modify tests to assert PreCompact stays silent even when a rebuild would otherwise fire, while SessionStart(source="compact") emits the rebuild block, prefers aelfrice logs over Claude transcripts, falls back when needed, and skips emission when there are no turns or non-compact sources.
  • Adjust test payload factories to include source and hook_event_name/event fields to simulate different hook invocations.
tests/test_hook_pre_compact.py
Align rebuilder trigger-mode tests with the new SessionStart:compact injection behavior.
  • Extend test_rebuilder_triggers to construct payloads with source and event, add helpers to extract rebuild blocks from SessionStart, and update threshold/manual-mode tests to assert behavior for both PreCompact and SessionStart:compact.
  • Ensure manual trigger mode suppresses automatic rebuild both at PreCompact and SessionStart:compact, and threshold mode emits only for source == "compact" while non-compact SessionStart remains silent.
  • Keep explicit aelf rebuild behavior intact by not changing those tests.
tests/test_rebuilder_triggers.py
Move context-rebuilder hook acceptance criteria tests from PreCompact to SessionStart:compact.
  • Update context_rebuilder_hook tests to generate SessionStart:compact payloads, use _rebuild_block and _start_compact helpers, and assert tier ordering, determinism, and latency on the new injection path.
  • Replace previous JSON additionalContext-envelope parsing with direct slicing of the rebuild block from SessionStart stdout, matching the new wire format.
  • Keep existing AC acceptance criteria (tiers, ordering, determinism) but apply them to the SessionStart:compact channel.
tests/test_context_rebuilder_hook.py
Document the behavioral change in the v3 changelog.
  • Add a changelog entry explaining that PreCompact outputs were rejected by the harness and that the rebuild block now rides the SessionStart(source="compact") hook, while PreCompact is silent and trigger-mode semantics are preserved.
CHANGELOG/v3.md

Assessment against linked issues

Issue Objective Addressed Explanation
#1031 Inject the block via the SessionStart hook when source == "compact", preserving prior rebuild behavior (log/transcript preference, trigger_mode gating, thresholds).
#1031 Neuter the PreCompact hook so it no longer emits harness-invalid JSON on stdout (while still returning 0 and honoring trigger_mode semantics, with explicit aelf rebuild / /aelf:rebuild behavior unchanged).
#1031 Ensure tests cover SessionStart(source="compact") rebuild emission, gating (including manual mode and non-compact sources), and the neutered PreCompact path.

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

@github-actions

github-actions Bot commented Jun 30, 2026

Copy link
Copy Markdown

PR-size soft cap

This PR is over the advisory size threshold:

  • 624 changed lines (limit: 200)
  • 5 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.

@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

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

Next review available in: 52 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: 8e7b529d-4bf9-4955-a96d-585c9ad2b7ae

📥 Commits

Reviewing files that changed from the base of the PR and between 6e65d36 and 28533af.

📒 Files selected for processing (1)
  • src/aelfrice/hook.py
📝 Walkthrough

Walkthrough

Fixes #1031 by making pre_compact() silent on stdout and moving context-rebuilder rebuild-block injection to session_start() gated on source == "compact". A new _build_rebuild_block_from_payload() helper centralizes rebuild block construction with trigger-mode gating and turn resolution. Tests across three modules are updated to assert the new behavior.

Changes

Rebuild injection migration: PreCompact → SessionStart:compact

Layer / File(s) Summary
Constants, imports, and PreCompact silencing
src/aelfrice/hook.py
Removes emit_pre_compact_envelope import, adds PAYLOAD_SOURCE_KEY / "compact" constants, and rewrites pre_compact() to accept but discard stdout, emitting nothing.
_build_rebuild_block_from_payload helper and SessionStart injection
src/aelfrice/hook.py
Introduces shared helper applying trigger-mode gating, turn resolution, and store checks; wires it into session_start() which now fully parses stdin payload and conditionally appends the rebuild block when source == "compact".
Test infrastructure updates
tests/test_hook_pre_compact.py, tests/test_context_rebuilder_hook.py, tests/test_rebuilder_triggers.py
Updates imports and adds _payload, _rebuild_block, and _start_compact helpers across three test modules to support SessionStart:compact-driven rebuild-block assertions.
Test assertions: PreCompact silent + SessionStart:compact emits rebuild block
tests/test_hook_pre_compact.py, tests/test_context_rebuilder_hook.py, tests/test_rebuilder_triggers.py
Rewrites AC1, AC3, TM1, TM2 test cases to assert pre_compact produces empty stdout and SessionStart:compact emits or omits the rebuild block per trigger mode, source, and store/transcript availability.
Changelog
CHANGELOG/v3.md
Adds [Unreleased] → Fixed entry for #1031.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • robotrocketscience/aelfrice#352: Modifies the same SessionStart hook implementation in src/aelfrice/hook.py, adding audit logging with tokens/latency/beliefs at the same integration point.
  • robotrocketscience/aelfrice#591: Extends the v1.4 rebuild block renderer (rebuild_v14/_format_block) that this PR's _build_rebuild_block_from_payload delegates to for formatting.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 65.63% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and accurately summarizes the main change: moving rebuild injection from PreCompact to SessionStart(source=compact).
Description check ✅ Passed The description covers the problem, fix, tests, and verification, though it doesn't exactly follow the repository template sections.
Linked Issues check ✅ Passed The changes satisfy #1031 by moving rebuild emission to SessionStart(source=compact), silencing PreCompact, preserving manual gating, and updating tests.
Out of Scope Changes check ✅ Passed The modified files stay aligned with the issue scope; the changelog and test updates support the hook fix rather than adding unrelated behavior.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/issue-1031-precompact-sessionstart-rebuild

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.

@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 found 1 issue, and left some high level feedback:

  • There are now three separate _rebuild_block helpers (and multiple _payload / _start_compact variants) in different test modules that are nearly identical; consider centralizing these in a shared test utility to reduce duplication and keep future behavior changes in one place.
  • In pre_compact, the stdout argument is only referenced to appease unused-argument linting; it may be clearer to either rename it to _stdout or add a brief note near the function signature that the parameter is intentionally unused to avoid confusion for future readers.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- There are now three separate `_rebuild_block` helpers (and multiple `_payload` / `_start_compact` variants) in different test modules that are nearly identical; consider centralizing these in a shared test utility to reduce duplication and keep future behavior changes in one place.
- In `pre_compact`, the `stdout` argument is only referenced to appease unused-argument linting; it may be clearer to either rename it to `_stdout` or add a brief note near the function signature that the parameter is intentionally unused to avoid confusion for future readers.

## Individual Comments

### Comment 1
<location path="src/aelfrice/hook.py" line_range="2640-2649" />
<code_context>
+def _build_rebuild_block_from_payload(payload: dict[str, object]) -> str:
</code_context>
<issue_to_address>
**issue (bug_risk):** Align token-budget overrides between baseline rebuild and compact-session rebuild block

In `session_start`, a provided `token_budget` override is honored for the primary baseline rebuild, but `_build_rebuild_block_from_payload` always uses `config.token_budget`. This means the same hook entrypoint yields different behavior before vs. after compaction. If you intend parity, please pass the effective budget into `_build_rebuild_block_from_payload` (or add an optional override parameter) so both paths respect the same override semantics.
</issue_to_address>

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.

Comment thread src/aelfrice/hook.py

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
tests/test_context_rebuilder_hook.py (1)

111-160: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Consider centralizing the shared SessionStart test helpers.

_payload, _rebuild_block, and _start_compact are duplicated near-verbatim across tests/test_context_rebuilder_hook.py, tests/test_hook_pre_compact.py, and tests/test_rebuilder_triggers.py (note the event vs hook_event_name keyword drift between copies). A shared conftest.py/helper module would prevent divergence as the rebuild-block contract evolves.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_context_rebuilder_hook.py` around lines 111 - 160, The
SessionStart test helper logic is duplicated across multiple test modules, and
the copies are already drifting in keyword usage between event and
hook_event_name. Centralize `_payload`, `_rebuild_block`, and `_start_compact`
into a shared test helper (for example a common helper module or conftest
fixture) and update `session_start`-based tests to import and reuse it so the
rebuild-block contract stays consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/aelfrice/hook.py`:
- Around line 2718-2721: The stdin read in the compact parsing path is
swallowing exceptions in `raw = sin.read()` without any diagnostic, which hides
failures that affect `session_id` and `source=="compact"` handling. Update the
`try/except` around the read in this hook to log the exception with enough
context before continuing or re-raising as appropriate, using the surrounding
compact/session parsing logic to locate the failure. Keep the behavior in
`hook.py` consistent with the rest of the error handling in this code path and
avoid a bare silent `except`.

---

Nitpick comments:
In `@tests/test_context_rebuilder_hook.py`:
- Around line 111-160: The SessionStart test helper logic is duplicated across
multiple test modules, and the copies are already drifting in keyword usage
between event and hook_event_name. Centralize `_payload`, `_rebuild_block`, and
`_start_compact` into a shared test helper (for example a common helper module
or conftest fixture) and update `session_start`-based tests to import and reuse
it so the rebuild-block contract stays consistent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 4b9c9cd3-aa10-4d20-b540-527fb09349d1

📥 Commits

Reviewing files that changed from the base of the PR and between eba5bf4 and 6e65d36.

📒 Files selected for processing (5)
  • CHANGELOG/v3.md
  • src/aelfrice/hook.py
  • tests/test_context_rebuilder_hook.py
  • tests/test_hook_pre_compact.py
  • tests/test_rebuilder_triggers.py

Comment thread src/aelfrice/hook.py Outdated
A read failure dropped session_id and the source/cwd parsing the
compact-rebuild path needs; surface it on stderr instead of swallowing
silently. Addresses CodeRabbit review.
@robotrocketscience robotrocketscience added the ready-to-merge Trigger merge-train: FF main to this PR's head label Jun 30, 2026
@github-actions github-actions Bot removed the ready-to-merge Trigger merge-train: FF main to this PR's head label Jun 30, 2026
@github-actions
github-actions Bot merged commit 28533af into main Jun 30, 2026
28 checks passed
@github-actions

Copy link
Copy Markdown

merge-train: merged 28533afmain via FF push.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(hook): PreCompact rebuild block rejected by harness — move injection to SessionStart(source=compact)

1 participant