feat: compaction summary-LLM cooldown + in-loop breaker (Dim 2 d/e)#111
Conversation
…im 2 d/e) ABSORB_MATRIX Dim 2 residual (d)+(e): give the reasoning-loop's compaction summarizer an anti-thrash cooldown and a consecutive-failure circuit breaker, mirroring the console compactor's breaker (corlinman_server...console.compaction.Compactor) at per-turn loop scope. The cheap tool-result elide fast path is NEVER gated — only the heavyweight summarization LLM sub-call — so a turn under context pressure still shrinks while the summarizer is parked. Behaviour (in-loop = across the rounds of one per-turn ReasoningLoop): - A failed summary sub-call (raised OR empty-text return, matching the console compactor's "returned no text") increments a consecutive-failure count and parks the slow path for CORLINMAN_COMPACT_SUMMARY_COOLDOWN_ROUNDS rounds (default 5). - After CORLINMAN_COMPACT_SUMMARY_BREAKER_LIMIT consecutive failures (default 3) the summarizer is disabled for the rest of the turn — no further attempts even once the cooldown elapses — with one warning log (reasoning_loop.summary_breaker_tripped). - A successful summary re-arms the failure count. A run of two low-savings successes (each shrinking the estimate <10%) trips the same cooldown so we stop paying the sub-call cost for marginal gains (hermes anti-thrash). Implementation: - New import-time int knobs via a new _env_positive_int helper (integer sibling of _env_positive_float), floored at 1. - _compact_history gains keyword-only summary_allowed (gates only the slow path) and outcome dict (records summary_attempted / summary_failed / summary_saved_fraction). The overflow-retry site (fast_path_only=True) is untouched. - ReasoningLoop gains per-turn state (_summary_failures / _summary_disabled / _summary_cooldown_until_round / _summary_low_savings_streak), reset in run() alongside _auto_continue_count, advanced by a fully-defensive _update_summary_breaker_state (try/except like the hook call sites). Tests (TDD, corlinman-agent/tests/test_reasoning_loop.py): failure sets cooldown then re-attempts after it elapses; breaker disables for the remainder past the cooldown; success resets the failure count; a low-savings streak trips the cooldown; summary_allowed=False gates only the LLM call (elide still happens); outcome dict recording on success/failure; _env_positive_int parse (default/override/garbage/floor). Existing compaction tests unchanged (two monkeypatch _compact_history stand-ins gained the new kwargs to match the signature).
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: cc0ee2da2b
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| if outcome.get("summary_failed"): | ||
| self._summary_failures += 1 | ||
| self._summary_cooldown_until_round = rounds + _COMPACT_SUMMARY_COOLDOWN_ROUNDS |
There was a problem hiding this comment.
Reset low-savings streak on failed summaries
When a failed summary attempt follows one low-savings success, this branch leaves _summary_low_savings_streak intact. After the failure cooldown expires, a single later low-savings success is then treated as the second consecutive low-savings success and triggers another cooldown, even though the intervening failure broke the success streak. Reset the low-savings streak on failed attempts so the anti-thrash policy only fires for truly consecutive low-savings successes.
Useful? React with 👍 / 👎.
A failure between two low-savings successes broke the 'consecutive' premise of the anti-thrash policy, but the streak counter survived the failure — after the failure cooldown expired, a single later low-savings success read as the second consecutive one and triggered a spurious cooldown. The failure branch now zeroes the streak, so anti-thrash fires only on truly consecutive low-savings successes. Regression test scripts success(low)→failure→success(low) and asserts the third attempt does NOT trip (without the reset, round 4 would be suppressed). 73 passed; ruff+mypy clean.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b164ad027f
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| return | ||
| if outcome.get("summary_failed"): | ||
| self._summary_failures += 1 | ||
| self._summary_cooldown_until_round = rounds + _COMPACT_SUMMARY_COOLDOWN_ROUNDS |
There was a problem hiding this comment.
Skip the configured number of cooldown rounds
When a summary attempt fails (or the low-savings streak trips), this sets the next allowed round to rounds + _COMPACT_SUMMARY_COOLDOWN_ROUNDS, while the caller checks rounds >= _summary_cooldown_until_round at the top of each subsequent round. That means CORLINMAN_COMPACT_SUMMARY_COOLDOWN_ROUNDS=1 immediately retries on the very next round, and the default 5 skips only four intervening rounds, contrary to the knob’s documented meaning of “how many rounds the slow summarization path is skipped after a failure.” In a pressured turn with a broken summary provider, operators using a small cooldown still burn one failing summary RPC every round.
Useful? React with 👍 / 👎.
…odex #111 r2) cooldown_until was rounds + N against a 'rounds >= until' gate, so N=1 retried on the very next round and the default 5 skipped only four — contrary to the knob's documented 'how many rounds the slow path is skipped'. Assignment is now rounds + N + 1 at both trip sites (failure and low-savings streak), making the knob exact: N=1 skips one round. Test expectations shifted accordingly (streak-trip and streak-reset sequences gained one suppressed round); the two provider-counting tests' assertions were already invariant under the shift (2 and 3 sub-calls) — comments updated to the new round arithmetic. 73 passed; ruff+mypy clean.
…ell + #108 backend) Closes claude-code parity Wave 4, merged via #111 (Dim 2), #112 (Dim 4), #113 (#108 items 3/4/5). - Dim 2: summary-LLM compaction cooldown + failure/low-savings breaker. - Dim 4: run_shell run_in_background + shell_task_output/shell_task_kill, with session-ownership isolation, 64KiB paged has_more reads, unified process-group _reap across all 6 terminal paths (incl. daemonized grandchildren + atexit), lifecycle watchdog, evicted-log cleanup, a task-control permission model that tracks the start-grant (allow/log/ask across scoped/wildcard-deny/default-deny/strict/plan) confined by the ownership gate, non-bool/overflow never-raise hardening, and subagent safety (bg refused + controls stripped unless explicitly granted). - #108 items 3/4/5: alias-collision warn, adaptive three-clock overview SSE with pre-scan keepalive, public-URL mtime cache.
- button: solid tint pill (whitelisted glow) / ghost-border outline / matte secondary + quiet ghost / muted err destructive; weight 500; 50px lg tier - new presence-orb primitive (eclipse pearl, sm/md/hero, eclipse-turn) - card/glass-panel/empty-state: resting surfaces carry the moon edge only; primary variant = selected treatment (edge + inset tint glow) - dialog/drawer/command-palette/cmdk: floating tier unified on .sg-glass-overlay (opaque #111 + strong edge + elev-4) - alert: matte charcoal band + 2px status left rule - switch: checked = solid tint w/ tint-ink thumb; unchecked = well - stat-chip/filter-chip: active state = nav-active selected treatment - skeleton shimmer is now an opacity pulse (CSS-level, wave 0)
Summary
ABSORB_MATRIX Dim 2 residual (d)+(e): the in-loop compaction summarizer gains a per-turn cooldown/anti-thrash and a consecutive-failure circuit breaker (the console Compactor already had one; the reasoning-loop path had no failure memory at all — a broken/slow summary provider was re-called every round under pressure, one wasted sub-provider RPC per round).
Orchestrated build: 3-track Opus research (42 load-bearing claims, 0 refuted by adversarial verification) → Opus implementation in an isolated worktree → independently re-validated (ruff/mypy/pytest re-run from scratch) before push.
What shipped
_compact_historygains keyword-onlysummary_allowed: bool = True(gates ONLY the summary-LLM sub-call — the cheap elide path is never gated) andoutcome: dict | None(recordssummary_attempted/summary_failed/summary_saved_fraction). Empty-text summary counts as failure (console-compactor parity).ReasoningLoopper-turn state: failure count, cooldown-until-round, disabled flag, low-savings streak — reset perrun()like_auto_continue_count.CORLINMAN_COMPACT_SUMMARY_COOLDOWN_ROUNDS(default 5);CORLINMAN_COMPACT_SUMMARY_BREAKER_LIMIT(default 3, mirrors console_BREAKER_LIMIT) consecutive failures → summarizer disabled for the turn (one warning log); success resets; two consecutive successes each saving <10% → cooldown (hermes anti-thrash semantics).fast_path_only=True, never summarizes) untouched.Testing
9 new tests (failure→cooldown→re-attempt with real provider counting sub-calls; breaker persists past cooldown; success resets; low-savings streak;
summary_allowed=Falsestill elides; outcome recording; env parse). Full agent package: 899 passed. ruff + mypy clean (independently re-verified).