Skip to content

fix(compaction): enforce breaker and per-turn cap on blocking compaction routes - #529

Open
effortprogrammer wants to merge 2 commits into
code-yeongyu:mainfrom
effortprogrammer:fix/issue-527-blocking-compaction-guards
Open

fix(compaction): enforce breaker and per-turn cap on blocking compaction routes#529
effortprogrammer wants to merge 2 commits into
code-yeongyu:mainfrom
effortprogrammer:fix/issue-527-blocking-compaction-guards

Conversation

@effortprogrammer

@effortprogrammer effortprogrammer commented Jul 30, 2026

Copy link
Copy Markdown

What changed

applyBlockingCompaction() now enforces the circuit breaker and the per-turn soft cap at its entry, and the session_compact accepted branch wires the previously dead cap.incrementIneffective counter.

Why

The per-turn cap (soft 3 / hard 10) and circuit breaker (3 failures → 60s cooldown) were only consulted in the session_before_compact hook. Every blocking trigger — before_agent_start hard-limit, agent_end idle, turn_end zero-yield recovery, degradation-monitor recovery — generates a summary via applyBlockingCompaction() and applies it precomputed, which skips session_before_compact in _executeCompaction (if (!compactionResult) branch). Both guards were dead on those routes, so compaction could repeat without bound, each iteration paying a real LLM summarization request. Additionally, incrementIneffective was never called from src/, so ineffective attempts never counted toward the soft cap and the turn_end zero-yield route retriggered on every tool turn.

Reproduced three unbounded loops with deterministic faux-provider tests (details in #527):

  1. hard-limit + transient summarization failure: 8 prompts → 8 summarization calls (breaker ignored);
  2. hard-limit + successful-but-ineffective compaction: 12 prompts → 12 compactions (caps ignored);
  3. zero-yield accepted compaction: 5 sequential tool turns → 5 recovery compactions.

Observed behavior after fix

  • Hard-limit transient failure stops at FAILURE_TRIP_THRESHOLD (3) summarization calls.
  • Accepted blocking compactions stop at the per-turn soft cap (3).
  • Zero-yield recovery runs exactly once per turn, then the soft cap refuses further attempts.

QA / evidence

Full receipts: local-ignore/qa-evidence/20260730-issue-527-blocking-route-guards/.

  • New test/compaction/blocking-compaction-route-guards.test.ts: red before fix (8/12/5 calls), green after (3/3 pass).
  • npx vitest --run test/compaction/ test/compaction*.test.ts test/trigger-compact-extension.test.ts: 47 files, 362 tests pass.
  • Session-level compaction suites + test/suite/regressions/: 336 tests pass after workspace build (the only initial failures were unbuilt-dist module errors in a fresh clone; green after npm run build).
  • npx biome check clean on changed files; root npx tsgo --noEmit exit 0.
  • Live CLI QA compaction-remote-qa.mjs --self-test: 5/6, identical on pristine main (the one FAIL is a pre-existing payload-replay assertion, A/B verified via git stash). compaction-wave1.mjs fails identically on main (fake model never called in this environment) — not caused by this change.

Residual risk

  • Automatic blocking routes now share the soft cap with the hook route, so a pathological same-turn context can refuse a 4th blocking compaction; the turn then proceeds to the normal provider-overflow recovery path (one-shot), which is the existing fail-closed behavior.

closes #527


Summary by cubic

Prevents unbounded blocking compaction loops by enforcing the circuit breaker and per-turn cap on all blocking routes and counting ineffective compactions toward the cap. This bounds retries and avoids wasted LLM summarization calls (addresses #527).

  • Bug Fixes
    • Gate applyBlockingCompaction() on the circuit breaker and per-turn soft cap; reject when tripped or capped.
    • Count ineffective compactions via cap.incrementIneffective in the session_compact accepted handler to stop zero-yield retriggers within the same turn.
    • Add regression tests covering: breaker trips after 3 failures, accepted blocking compactions stop at the soft cap (3), and zero-yield recovery runs once per turn.

Written for commit 047e900. Summary will update on new commits.

Review in cubic

…ion routes

applyBlockingCompaction() bypassed the circuit breaker and per-turn caps
because precomputed results skip session_before_compact in core, letting
the hard-limit, idle, and turn_end zero-yield triggers loop unbounded
(each iteration paying a real summarization request).

- Gate applyBlockingCompaction entry on breaker.isTripped and
  cap.shouldRejectByCap
- Wire cap.incrementIneffective into the session_compact accepted branch
  so ineffective attempts count toward the soft cap (was dead code)
- Add blocking-compaction-route-guards regression tests (red before,
  green after)

QA evidence: local-ignore/qa-evidence/20260730-issue-527-blocking-route-guards/

closes code-yeongyu#527
@effortprogrammer

Copy link
Copy Markdown
Author

Closing: live debugging on a real stuck session (Node inspector attached to the running TUI process) showed the actual user-facing infinite-compaction symptom comes from a different loop — the turn-retry ↔ pre-prompt compaction cycle (~60s breaker-cooldown cadence) plus a compaction lifecycle leak that leaves the UI spinner open permanently. The applyBlockingCompaction guard bypass fixed here is real but was never exercised by the live session (zero hits on that path while the loop was active). I'll open a separate PR for the actual root cause. The cap/breaker bypass documented in #527 remains a valid latent issue; happy to resubmit this fix separately if maintainers want it.

@effortprogrammer

Copy link
Copy Markdown
Author

Reopening: this fix is for issue #527, which turned out to be a different defect from the one that bricked the live session (that one is #531, fixed by #532).

Why this still matters, and more than before: once #532 lands, prompts over the soft threshold proceed during a breaker cooldown instead of dying at admission. That means sessions near the hard limit will now actually reach the before_agent_start hard-limit branch on every prompt — and that branch calls applyBlockingCompaction() without consulting the tripped circuit breaker, paying one doomed summarization request per prompt for the duration of an outage. #527's bypass stops being latent exactly when #532 ships.

Scope recap (unchanged from the original PR):

  • applyBlockingCompaction() entry now enforces the circuit breaker and the per-turn soft cap, covering all four blocking triggers (before_agent_start hard-limit/proactive, agent_end idle, turn_end zero-yield recovery, degradation-monitor recovery).
  • session_compact accepted branch wires the previously dead incrementIneffective counter so zero-yield compactions count toward the soft cap.
  • Regression tests in test/compaction/blocking-compaction-route-guards.test.ts (red before: 8/12/5 executions where guards require 3/10/3; green after).

The earlier close was only because this PR did not address the live incident — not because the defect was invalid.

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.

Compaction caps and circuit breaker bypassed on blocking routes — unbounded compaction loops

1 participant