fix(hooks): stop false loop warnings and repeated identical context warnings (#2121)

* fix(hooks): stop false loop warnings and repeated identical context warnings

Two PostToolUse monitor defects surfaced during a long single-turn session:

1. ecc-metrics-bridge hashToolCall fingerprinted Edit/Write/MultiEdit on
   file_path ONLY, so several distinct edits to the same file produced the
   same hash and tripped the loop detector ("stuck loop") even though every
   edit was different. Now the hash includes the edit content
   (old_string/new_string/content/edits) so distinct edits to one file hash
   differently; identical edits still collide as intended.

2. ecc-context-monitor re-emitted the SAME warning every DEBOUNCE_CALLS (5)
   tool calls even when nothing changed. Because the cost figure only refreshes
   at Stop (turn) boundaries, a single stale value printed the identical
   warning ~20 times within one turn. Dedupe on message content instead: a
   warning surfaces only when its text changes (cost moved, new file count, new
   loop) or on first escalation to critical, and is otherwise suppressed.

Adds regression tests for the same-file/different-content hash case.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(hooks): address CodeRabbit review (#2121)

- ecc-context-monitor: clear dedupe state when warnings resolve, so the same
  warning text recurring in a later turn (context dips/recovers/dips, a loop
  that stops then restarts) is surfaced again instead of suppressed as a
  duplicate. Guarded so the no-warning hot path stays write-free.
- ecc-metrics-bridge: hash the FULL serialized edit payload and truncate the
  digest, not the input. Slicing the serialized string to HASH_INPUT_LIMIT
  first could collapse large edits sharing their first 2048 chars, reviving the
  false-loop collision for big Write/edit payloads.
- Add regression test for >2048-char edit divergence.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
AHNINE Amine
2026-06-07 07:26:30 +02:00
committed by GitHub
parent 5df520658a
commit 4197ea545f
3 changed files with 85 additions and 19 deletions

View File

@@ -78,6 +78,39 @@ function runTests() {
passed++;
else failed++;
if (
test('different edits to the SAME file produce different hashes (no false loop)', () => {
const h1 = hashToolCall('Edit', { file_path: 'a.kt', old_string: 'foo', new_string: 'bar' });
const h2 = hashToolCall('Edit', { file_path: 'a.kt', old_string: 'baz', new_string: 'qux' });
assert.notStrictEqual(h1, h2);
})
)
passed++;
else failed++;
if (
test('identical Edit (same file + same change) still hashes the same', () => {
const args = { file_path: 'a.kt', old_string: 'foo', new_string: 'bar' };
assert.strictEqual(hashToolCall('Edit', args), hashToolCall('Edit', { ...args }));
})
)
passed++;
else failed++;
if (
test('large edits diverging only after 2048 chars still hash differently', () => {
// Shared prefix longer than the old HASH_INPUT_LIMIT (2048) truncation
// point; the payloads differ only afterwards. Hashing the full payload
// (digest truncated, not input) must keep them distinct.
const prefix = 'x'.repeat(4000);
const h1 = hashToolCall('Write', { file_path: 'big.txt', content: prefix + 'AAA' });
const h2 = hashToolCall('Write', { file_path: 'big.txt', content: prefix + 'BBB' });
assert.notStrictEqual(h1, h2);
})
)
passed++;
else failed++;
if (
test('non-file tools hash by stable input to avoid false loop collisions', () => {
const h1 = hashToolCall('Glob', { pattern: '**/*.js', path: '/repo/a' });