From a3d8d8ab925c63b8b28f144c6754bd213d32b554 Mon Sep 17 00:00:00 2001 From: zucchini <68502517+zanni098@users.noreply.github.com> Date: Sun, 7 Jun 2026 10:25:29 +0500 Subject: [PATCH] fix(observer): auto-scale max_turns by analysis batch size (#2062) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(observer): auto-scale max_turns by analysis batch size (#2035) The hardcoded default of MAX_TURNS=20 is insufficient when MAX_ANALYSIS_LINES=500 (also the default). Claude exhausts its turn budget before it can write all discovered instinct files, producing: Error: Reached max turns (20) Fix: when ECC_OBSERVER_MAX_TURNS is not explicitly set, compute max_turns proportionally to the actual analysis batch size: max_turns = clamp(analysis_count / 10, 20, 100) This gives: - 20–199 lines → 20 turns (existing floor, unchanged) - 500 lines → 50 turns (resolves the reported failure) - 1000 lines → 100 turns (cap) Explicitly setting ECC_OBSERVER_MAX_TURNS still overrides the auto-scaled value, preserving the existing escape hatch. * test(observer): update max_turns test for auto-scaling; document validation The max-turns budget test in tests/hooks/hooks.test.js still asserted the removed literal max_turns="${ECC_OBSERVER_MAX_TURNS:-20}", which would fail against the new auto-scaling logic. Assert the auto-scale formula and the 20/100 clamp bounds instead. Also add the explanatory comment CodeRabbit requested above the max_turns sanitization block, clarifying it guards the explicit ECC_OBSERVER_MAX_TURNS override path. Co-Authored-By: Claude Opus 4.8 --------- Co-authored-by: Claude Opus 4.8 --- .../agents/observer-loop.sh | 15 ++++++++++++++- tests/hooks/hooks.test.js | 4 +++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/skills/continuous-learning-v2/agents/observer-loop.sh b/skills/continuous-learning-v2/agents/observer-loop.sh index a899124a..9bab37c5 100755 --- a/skills/continuous-learning-v2/agents/observer-loop.sh +++ b/skills/continuous-learning-v2/agents/observer-loop.sh @@ -205,9 +205,22 @@ PROMPT fi timeout_seconds="${ECC_OBSERVER_TIMEOUT_SECONDS:-120}" - max_turns="${ECC_OBSERVER_MAX_TURNS:-20}" + # Auto-scale max_turns proportional to analysis batch size when not explicitly set. + # The old hardcoded default of 20 is insufficient for the 500-line MAX_ANALYSIS_LINES + # default: Claude hits --max-turns before it can write all discovered instinct files. + # Formula: 1 turn per 10 analysis lines, floor 20, cap 100. (#2035) + if [ -n "${ECC_OBSERVER_MAX_TURNS:-}" ]; then + max_turns="${ECC_OBSERVER_MAX_TURNS}" + else + max_turns=$(( analysis_count / 10 )) + if [ "$max_turns" -lt 20 ]; then max_turns=20; fi + if [ "$max_turns" -gt 100 ]; then max_turns=100; fi + fi exit_code=0 + # Sanitize max_turns. The auto-scaled path above always yields a valid value >=20, + # but an explicit ECC_OBSERVER_MAX_TURNS override may be non-numeric, empty, or too + # small, so guard here and fall back to the safe default of 20. case "$max_turns" in ''|*[!0-9]*) max_turns=20 diff --git a/tests/hooks/hooks.test.js b/tests/hooks/hooks.test.js index 7ed88422..af0d0d24 100644 --- a/tests/hooks/hooks.test.js +++ b/tests/hooks/hooks.test.js @@ -3137,7 +3137,9 @@ async function runTests() { const observerLoopSource = fs.readFileSync(path.join(__dirname, '..', '..', 'skills', 'continuous-learning-v2', 'agents', 'observer-loop.sh'), 'utf8'); assert.ok(observerLoopSource.includes('ECC_OBSERVER_MAX_TURNS'), 'observer-loop should allow max-turn overrides'); - assert.ok(observerLoopSource.includes('max_turns="${ECC_OBSERVER_MAX_TURNS:-20}"'), 'observer-loop should default to 20 turns'); + assert.ok(observerLoopSource.includes('max_turns=$(( analysis_count / 10 ))'), 'observer-loop should auto-scale max_turns from the analysis batch size when no override is set'); + assert.ok(observerLoopSource.includes('if [ "$max_turns" -lt 20 ]; then max_turns=20; fi'), 'observer-loop should clamp the auto-scaled budget to a floor of 20 turns'); + assert.ok(observerLoopSource.includes('if [ "$max_turns" -gt 100 ]; then max_turns=100; fi'), 'observer-loop should clamp the auto-scaled budget to a cap of 100 turns'); assert.ok(!observerLoopSource.includes('--max-turns 3'), 'observer-loop should not hardcode a 3-turn limit'); assert.ok(observerLoopSource.includes('ECC_SKIP_OBSERVE=1'), 'observer-loop should suppress observe.sh for automated sessions'); assert.ok(observerLoopSource.includes('ECC_HOOK_PROFILE=minimal'), 'observer-loop should run automated analysis with the minimal hook profile');