Skip to content

fix(core): count a repeat as a loop only when its output repeats too - #47

Merged
TaewoooPark merged 1 commit into
mainfrom
fix/loop-guard-progress
Oct 17, 2026
Merged

TaewoooPark merged 1 commit into
mainfrom
fix/loop-guard-progress

Conversation

@TaewoooPark

Copy link
Copy Markdown
Owner

Problem

The DW-ID micromagnetics benchmark runs a long unattended session: fit four material constants to measured domain-wall data with mumax3, using the spinloop skills, under a 6 h cap. motifcode ended the task twice while the agent was doing exactly what the fitting skill tells it to do.

The skill's fit is resumable. fit.py step SPEC --budget N runs for at most N seconds, and the next call continues the fit. The skill says "call it again until it says done", and the tool ends every call with:

next: `fit.py step SPEC --budget N` again

LoopGuard counted a repeat whenever a turn's action signature matched the turn before, whatever the actions returned. The third identical step call therefore ended the task (session_end: loop_detected). Nobody was at the keyboard, so the session then sat idle until the benchmark's quiet rule closed it.

The three calls before the stop each finished new evaluations:

call evaluations finished best chi2 so far
1 e005, e006 30.296
2 e007, e008 27.17
3 e009, e010, e011 27.071

The two sessions were stopped 4.2 h and 2.3 h into their 6 h budget.

The intent was already the narrower rule. The guard's test is named "stops when the same action repeats with the same result", and packages/eval/REPORT.md describes it as "identical call/output repeated". The code, however, compared the call only.

Change

  • fix(core): count a repeat as a loop only when its output repeats too
    • A repeat now needs the same combined output as the turn before, as well as the same calls.
    • The stall rule is unchanged: five turns in a row with the same output, whatever the calls.
    • LoopGuardSnapshot is unchanged, so checkpoints from earlier versions resume as before.

Evidence

Every recorded motifcode journal was replayed through both versions of LoopGuard, turn by turn as runLoop feeds it. That is 943 sessions and 8,236 tool turns: 936 from the Aider polyglot campaign and its follow-up runs, and 7 from the micromagnetics runs.

recorded stops unpatched guard patched guard
Aider polyglot (936 sessions) 7 the same 7, same turns the same 7, same turns
micromagnetics (7 sessions) 2 the same 2 none
  • The unpatched guard stopped exactly where the journals recorded a stop, and nowhere else. This validates the replay.

  • All 7 polyglot stops were real loops with an unchanged result:

    • 3 wrote the same file with the same content three times (write, or cat > in bash);
    • 2 ran the same command three times with the same output (a g++ failing on the same error, a cargo test matching no tests);
    • 2 were stalls, where different commands all failed with the same error (spawn /bin/sh ENOENT, a sed error).

    None of them depends on comparing the call alone, so the polyglot results do not move.

  • In those 936 sessions, no call was repeated three times with a changing result. The protection this change gives up was never used there.

What the change gives up. A call that repeats with output that changes but carries nothing new, such as a timestamp or a timing, is no longer stopped by this rule. Such a run is still bounded by the turn ceiling (100 by default) and, in interactive use, by the user.

Alternatives considered

  • A higher repeatLimit. Any fixed number still ends a fit that needs more steps; a passing run of this task needed 13 step calls in a row. Real loops would also run longer before being stopped.
  • A setting for the limits. The default would still stop progress for everyone, and a benchmark would have to switch the protection off to avoid it.
  • Warn before stopping. It adds a harness turn every three steps and still ends a long fit.
  • Exempting long-running calls. How long a call takes says nothing about whether it made progress.

Tests

pnpm typecheck, pnpm test (1673 passed), pnpm lint:tools and pnpm build all pass.

The two new tests both fail on main:

  • The loop runs five identical step calls whose output changes, and ends done with no loop_detected.
  • LoopGuard is checked directly:
    • an identical call with an identical result still stops at the third turn;
    • the same call with a changing result never stops;
    • different calls with the same output still stop at the fifth turn.

LoopGuard ended a task after three turns with the same action signature,
whatever the actions returned. A resumable job is driven exactly that way:
one command, run again until it reports done, each call advancing state on
disk. In two long unattended benchmark sessions the guard stopped such a fit
at its third call, though every call had run new evaluations.

A repeat now needs the same output as well as the same call. The stall rule
(five turns in a row with the same output, whatever the calls) is unchanged,
and so is the snapshot, so older checkpoints resume as before.

Replayed over 943 recorded sessions (8,236 tool turns), the unpatched guard
reproduces every recorded stop. The patched guard keeps all 7 stops from the
Aider polyglot runs, each an identical result, and drops the 2 from the long
sessions.
@TaewoooPark

Copy link
Copy Markdown
Owner Author

Reviewed 282190d0ada7cf6488dc268146471d912459fc1b on an Apple Silicon Mac. The change is focused and matches the intended identical-call/identical-output loop rule; no blocking regression found, and no additional patch is needed.

Both new tests fail with the base implementation and pass with this head. I also ran the actual built CLI against a local HTTP fixture and real node step.cjs tool: base stops at three calls with loop_detected, while this head executes five progressing calls and finishes done (exit 0). An unchanged-output control still stops at three. Nine additional probes covered empty output, streak reset, stalls, capture/restore, legacy snapshots, and custom limits. The original long micromagnetics benchmark and the journal-replay counts in the PR were not independently rerun.

Frozen install, typecheck, build and tool-schema lint passed. The full Mac run passed 1,669 tests and hit four existing process-start timeout cases under concurrent suites; all four passed in an isolated 31-test rerun. Loop and resume coverage passed. CI on this exact head is green in all five jobs: https://github.com/TaewoooPark/Motifcode/actions/runs/39023298505

The documented tradeoff remains: changing incidental output can avoid repeat detection, with the turn limit still applying. Thanks for the focused fix and regression coverage. Merging as a squash commit; npm 0.4.3 publication will follow separately after combined release validation.

@TaewoooPark
TaewoooPark merged commit 3e84903 into main Oct 17, 2026
5 checks passed
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.

1 participant