Rescue a tight tool-call loop sideways instead of killing the turn - #1039
Rescue a tight tool-call loop sideways instead of killing the turn#1039devin-ai-integration[bot] wants to merge 5 commits into
Conversation
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
|
PR author is not in the allowed authors list. |
|
Claude finished @devin-ai-integration[bot]'s task —— View job Posted an advisory comment-length review with 4 suggestionable nits — won't block merge. |
workweave-bot
left a comment
There was a problem hiding this comment.
Advisory only — comment-length nits. Won't block merge.
workweave-bot
left a comment
There was a problem hiding this comment.
Advisory only — comment-length nits. Won't block merge.
|
Claude finished @devin-ai-integration[bot]'s task —— View job Claude finished @devin-ai-integration[bot]'s task —— View job Posted an advisory comment-length review with 2 suggestionable nits — won't block merge. |
workweave-bot
left a comment
There was a problem hiding this comment.
Advisory only — comment-length nits. Won't block merge.
|
Claude finished @devin-ai-integration[bot]'s task —— View job
|
| PinnedUntil: time.Now().Add(pinSessionTTL), | ||
| PolicyGroup: targetCluster, | ||
| LastServedModel: pin.LastServedModel, | ||
| }) |
There was a problem hiding this comment.
Unservable rescue pin skips stop
High Severity
The sideways writer pins Providers[0] after an availableModels check only, with no forcedModelBinding / request-enabled-provider validation. Moved is still true, so the synthetic stop is skipped. runTurnLoop then drops the pin on provider ineligibility and the scorer can re-select the looping model, after which writeNewPin overwrites the rescue.
Additional Locations (1)
Triggered by learned rule: Force-model and user-facing model commands must resolve via catalog, not assume default provider
Reviewed by Cursor Bugbot for commit ef3a5f9. Configure here.
Runtime verification — loop-sideways rescueRan the router locally in docker compose from this branch with a mocked Fireworks upstream and a mocked HMM roster sidecar ( Main regression — 5× identical
Sideways re-pin on a real loop (5× identical
|
|
Claude finished @devin-ai-integration[bot]'s task —— View job
Posted an advisory comment-length review with 1 suggestionable nit — won't block merge. |
workweave-bot
left a comment
There was a problem hiding this comment.
Advisory only — comment-length nit. Won't block merge.
|
Claude finished @devin-ai-integration[bot]'s task —— View job
|
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit ad27221. Configure here.
| return false | ||
| } | ||
| return slices.Contains(effortVariantSuffixes, suffix) || translate.IsValidEffort(suffix) | ||
| } |
There was a problem hiding this comment.
Pro suffix over-excludes distinct models
Medium Severity
sameEngine treats any catalog ID that differs only by a -pro suffix as the same engine, so a tight-loop rescue excludes gpt-5.4-pro and gpt-5.5-pro when the pin is gpt-5.4/gpt-5.5. Those rows are separate models (own prices, no UpstreamID alias), unlike gpt-5.6-luna-pro which maps to the same upstream weights. A looping session can skip a genuine different arm and fall through to a weaker sideways pick or a stop.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit ad27221. Configure here.


Summary
A tight tool-call loop currently ends the turn:
handleToolCallLoopBreakwrites a syntheticend_turnand expires the pin. Prod sessionloom-398a68c6…(2026-08-25 19:31:12) shows why that buys nothing — the pin was cleared, the client resent 37s later, and the HMM re-picked the same arm (gpt-5.6-luna, score 0.88). Net effect: one dead turn on a session that was not actually stuck. Three fixes, all ininternal/proxy:1. Move the session sideways before stopping it. The tight detector now calls
handleToolCallLoopSidewaysfirst, which reuses the struggle machinery (StruggleEscalationRoster.EscalationTarget: cheapest cluster above the pin's own, else a sideways arm within it) and re-pins with a newtranslate.ReasonLoopSidewaysbefore routing, so this same turn dispatches on a different model instead of dying:The synthetic stop survives as the fallback for every case where a move isn't valid, tracked by an explicit action taxonomy:
no_pin,user_forced,already_moved(the pin is already a loop/struggle rescue — looping again there is a task problem, not a misroute),no_target,disabled. The pin write usescontext.Background()like the other durable auto-pins; a failed write degrades to the stop.ReasonLoopSidewaysjoins the immutable-sticky set inturnloop.goso the rescued arm can't be re-scored back onto the looping model.handleToolCallLoopBreakgains apreserveForcedPinargument (same shape ashandleNoProgressBreak): the fallback path is now reachable with a/force-modelpin in place, and automatic eviction must not clear it.2. Attribute the loop to the model that was serving it. The detector runs before routing, so
feats.Modelis the client's inbound baseline — the prod session loggedlooping_model=claude-fable-5 / anthropicwhile every one of its 36 turns was served bygpt-5.6-luna / openai.loopAttributionprefers the pin and falls back to the requested model only when there is no pin to read.3. Exempt poll tools from the identical-args detector.
shell_output(andget_output/BashOutput) drain a buffer that changes underneath a byte-identical request — repetition is the tool's normal usage, and it is what tripped the detector in prod. Poll signatures are skipped when counting; they still occupy window positions, so a genuine repeat interleaved with polls still trips.Not addressed here: struggle escalation needs turns AND wall-clock (30 / 10m), so a fast session like this one (36 turns in 4.5 min) never arms it — that gate is worth revisiting separately.
Link to Devin session: https://app.devin.ai/sessions/e32d7e9712e94512b8ecb03f29c14a37
Requested by: @steventohme