Skip to content

fix(agent): expand repeatSuccessSignature to track all tools (anti-loop)#3478

Closed
gelutjari wants to merge 1 commit into
esengine:main-v2from
gelutjari:fix/anti-loop-all-tools
Closed

fix(agent): expand repeatSuccessSignature to track all tools (anti-loop)#3478
gelutjari wants to merge 1 commit into
esengine:main-v2from
gelutjari:fix/anti-loop-all-tools

Conversation

@gelutjari

@gelutjari gelutjari commented Jun 7, 2026

Copy link
Copy Markdown

Closing this PR. PlannerMaxSteps fix has already been merged upstream via #3532 (more complete implementation with config field + Desktop UI). Will open a new focused PR for the remaining anti-loop guard defense-in-depth fixes (remember/forget tracking, bash regression fix, catch-all default case).

@github-actions github-actions Bot added agent Core agent loop (internal/agent, internal/control) v2 Go rewrite (1.x) — main-v2 branch, active development labels Jun 7, 2026

@SivanCola SivanCola left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the tight patch. I think this needs one correction before merge.

bash is intentionally ReadOnly() == false because arbitrary shell commands may write, but repeatSuccessSignature() previously narrowed the repeat-success guard back down with isShellFileWriteCommand(). Removing that filter makes every repeated foreground bash command count as a write-like success, so non-writing verification commands such as go test ./internal/agent, git status, or pwd are blocked on the third identical successful call in the same user turn, even when rerunning the command is legitimate after intervening state changes.

This already regresses the existing focused test on the PR head:

  • go test ./internal/agent -run TestRepeatGuardAllowsRepeatedNonWritingBashCommand -count=1 fails with bash executed 2 times, want 3 for non-writing commands

Please keep the bash guard limited to known write commands, or replace it with a more precise classification that still allows repeated non-writing verification commands. If the goal is to cover remember/forget and other non-read-only tools, please also add/update focused coverage for those intended loop cases without regressing the existing bash behavior.

Once this is addressed and CI stays green, I will consider merging it into main-v2.

…nerMaxSteps unlimited

- repeatSuccessSignature: track remember/forget tools in write-file case
- bash case: keep isShellFileWriteCommand filter (non-writing bash not blocked)
- default case: catch-all tracking prevents tool loops for any tool
- PlannerMaxSteps: 0 = unlimited, respect user's configured value

All existing tests pass. 3 new tests added:
- TestRepeatGuardBlocksRepeatedRemember
- TestRepeatGuardBlocksRepeatedForget
- TestRepeatGuardBlocksRepeatedUnknownTool
@gelutjari gelutjari force-pushed the fix/anti-loop-all-tools branch from 4ff6446 to 84ce660 Compare June 8, 2026 02:30
@gelutjari

Copy link
Copy Markdown
Author

@SivanCola Thanks for the thorough review. All 3 concerns addressed:

Changes

1. isShellFileWriteCommand filter PRESERVED (bash regression fix)

The bash guard is now split into two explicit checks instead of one compound condition:

// Before (original):
if p.RunInBackground || !isShellFileWriteCommand(p.Command) {
    return "", false
}

// After (this PR):
if p.RunInBackground {
    return "", false
}
if !isShellFileWriteCommand(normalizeShellCommand(p.Command)) {
    return "", false
}

Non-writing bash commands (go test, git status, pwd) still return ("", false) — they are not tracked by the repeat guard and can be called multiple times.

2. remember/forget added to write-file case

case "write_file", "edit_file", "multi_edit", "notebook_edit", "remember", "forget":

These are non-read-only tools that can be called repeatedly with identical args (e.g., forgetting the same memory entry), so they now participate in the repeat-success guard.

3. Default catch-all for remaining tools

default:
    return call.Name + "::" + canonicalToolArgs(call.Arguments), true

Any tool not covered by the explicit cases (e.g., todo_write, complete_step, custom MCP tools) now gets tracked with a tool-name-prefixed signature, preventing unlimited identical calls.

4. PlannerMaxSteps: 0 = unlimited

func PlannerMaxSteps(configured int) int {
    if configured <= 0 {
        return 0 // unlimited
    }
    return configured // respect user's explicit value
}

Previously, max_steps = 0 was silently capped to DefaultPlannerMaxSteps (6). Now 0 means unlimited, and any positive value is respected.

Tests

All 6 repeat guard tests PASS:

PASS: TestRepeatGuardBlocksRepeatedSuccessfulBashFileWrite   (existing — bash file-write blocked after 2)
PASS: TestRepeatGuardAllowsRepeatedNonWritingBashCommand      (existing — non-writing bash allowed 3x)
PASS: TestRepeatGuardAllowsTwoRepeatedWriterSuccesses          (existing — 2x still allowed)
PASS: TestRepeatGuardBlocksRepeatedRemember                    (NEW — remember blocked after 2)
PASS: TestRepeatGuardBlocksRepeatedForget                      (NEW — forget blocked after 2)
PASS: TestRepeatGuardBlocksRepeatedUnknownTool                 (NEW — catch-all tracks unknown tools)

Desktop E2E test PASS:

PASS: TestDesktopE2EBlocksRepeatedSuccessfulBashFileWrite

Full CI suite:

✅ gofmt -l (3 files clean)
✅ go vet ./...
✅ go build ./...
✅ go test ./... (42 packages, 0 failures)
✅ REASONIX_RELEASE_CACHE_GUARD=1 go test ./...

Matrix of guard behavior per tool type:

Tool Tracked? Blocked after 2 identical? Rationale
write_file, edit_file, multi_edit, notebook_edit ✅ Yes ✅ Yes File writes should not repeat
remember, forget ✅ Yes ✅ Yes Memory ops with identical args are wasteful
bash (file-write) ✅ Yes ✅ Yes e.g., python -c "open('f','w').write(...)"
bash (non-writing) ❌ No ❌ No go test, git status may legitimately repeat
bash (background) ❌ No ❌ No Background jobs are fire-and-forget
todo_write, complete_step, etc. ✅ Yes (catch-all) ✅ Yes Prevents stuck planning loops
Read-only tools (read_file, grep, etc.) ❌ No ❌ No t.ReadOnly() → skipped early

@gelutjari gelutjari requested a review from SivanCola June 8, 2026 03:15
@gelutjari gelutjari closed this Jun 9, 2026
@gelutjari gelutjari deleted the fix/anti-loop-all-tools branch June 9, 2026 08:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent Core agent loop (internal/agent, internal/control) v2 Go rewrite (1.x) — main-v2 branch, active development

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants