Combat T096 correction loop can ping-pong on projected-target rejections (62 attempts, ~1M tokens in one window)
Observed (marsh probe, 2026-09-19, branch fix/242-down-scene @ aa1dcd1; the code involved is unchanged since August and is the same on main):
Round 2 of a Boggard Marsh fight (Eirik down at 0, Thane 23 HP, Elen 31 HP, two will-o'-wisps and a shambling mound) took 64 T096 calls, 275 s, roughly 1M prompt tokens for one automatic window. Records 5-66 of the T096 capture alternate between two rejections:
target cmb-npc-ranger-thane-1 is already down -> legalTargets [cmb-npc-scout-elen-1]
target cmb-npc-scout-elen-1 is already down -> legalTargets [cmb-npc-ranger-thane-1]
What is going on. The validator is right every time. resolve_claimed_window resolves the batch in order against the persisted prerolls; that round the mound held a natural 20 for 34 damage, enough to drop either companion, so whichever companion the enemies focus is at 0 HP by the time the last wisp acts. The model is wrong every time: the correction says "reuse validatedIntents unchanged; change only the rejected actor", but the model answers by moving all enemy attacks onto the named legal target, which the mound's crit then drops instead, and so on. It reasons from the creatures list (start-of-window HP), sees "Elen is already down" with no explanation, and concludes nothing should attack Elen. The loop is unbounded by doctrine and ended only when the model happened to split fire.
Two smaller faults in the same recording compounded it: the mound picked Multiattack (listed on its sheet with 0d0 dice, not executable) in every window, and the companion's Cure Wounds carried requiresPlayerInput for its own 2d8, which pipeline.py honored only for the human actor and otherwise silently committed as a 0 heal while spending the slot.
Fix shipped on fix/242-down-scene (owner-directed: prompt/correction text first, one narrow code reject):
_intent_correction: when the rejection is a projected "already down", the correction now explains that the target fell because of the earlier intents in the model's own batch, that creatures shows start-of-window HP, and that redirecting earlier attacks repeats the rejection.
- T096 system prompt: the same rule in the ordering paragraph; never name
Multiattack/0d0 entries as the ability; an NPC never carries requiresPlayerInput (roll its dice in the ruling, supply final hpDelta).
pipeline.py: an NPC intent carrying requiresPlayerInput is rejected as a correction instead of committing its declared zero.
Evidence (slice replays against the real resolver, reconstructed round dice, real T096 calls; DEV AID, kit in agent-room-fleet-kit/local-data/242-down-scene/slice_replay.py):
| slice |
before |
after |
| D2 corrected call from the recorded rejection resolves in one trip |
0/3 |
3/3 |
| Companion Cure Wounds commits a real heal from the recorded batch |
0 (hpDelta 0, slot spent) |
3/3, one trip (0->13, 0->12, 0->12) |
| Fresh round-2 window: Multiattack named / NPC roll request |
every window |
0/4 and 0/4 |
| Human's own roll request still pauses the turn |
yes |
yes |
Why this issue stays open after the fix. The convergence now rests on the model following the explanation. Code still sends validatedIntents as guidance without enforcing the prefix, so a model that ignores the text can ping-pong again. If it recurs, the deterministic follow-up is to reconcile the validated prefix in code before re-resolving (replace the leading intents by actorId with the validated copies), which makes legalTargets for the rejected actor exact. A per-window attempt cap is not the answer (fail-forward doctrine, #193).
Related: #242 (down scene, where a two-target party makes this trivially reachable), #433 (main-turn correction give-up).
Combat T096 correction loop can ping-pong on projected-target rejections (62 attempts, ~1M tokens in one window)
Observed (marsh probe, 2026-09-19, branch
fix/242-down-scene@ aa1dcd1; the code involved is unchanged since August and is the same onmain):Round 2 of a Boggard Marsh fight (Eirik down at 0, Thane 23 HP, Elen 31 HP, two will-o'-wisps and a shambling mound) took 64 T096 calls, 275 s, roughly 1M prompt tokens for one automatic window. Records 5-66 of the T096 capture alternate between two rejections:
What is going on. The validator is right every time.
resolve_claimed_windowresolves the batch in order against the persisted prerolls; that round the mound held a natural 20 for 34 damage, enough to drop either companion, so whichever companion the enemies focus is at 0 HP by the time the last wisp acts. The model is wrong every time: the correction says "reuse validatedIntents unchanged; change only the rejected actor", but the model answers by moving all enemy attacks onto the named legal target, which the mound's crit then drops instead, and so on. It reasons from thecreatureslist (start-of-window HP), sees "Elen is already down" with no explanation, and concludes nothing should attack Elen. The loop is unbounded by doctrine and ended only when the model happened to split fire.Two smaller faults in the same recording compounded it: the mound picked
Multiattack(listed on its sheet with0d0dice, not executable) in every window, and the companion's Cure Wounds carriedrequiresPlayerInputfor its own 2d8, whichpipeline.pyhonored only for the human actor and otherwise silently committed as a 0 heal while spending the slot.Fix shipped on
fix/242-down-scene(owner-directed: prompt/correction text first, one narrow code reject):_intent_correction: when the rejection is a projected "already down", the correction now explains that the target fell because of the earlier intents in the model's own batch, thatcreaturesshows start-of-window HP, and that redirecting earlier attacks repeats the rejection.Multiattack/0d0entries as the ability; an NPC never carriesrequiresPlayerInput(roll its dice in the ruling, supply final hpDelta).pipeline.py: an NPC intent carryingrequiresPlayerInputis rejected as a correction instead of committing its declared zero.Evidence (slice replays against the real resolver, reconstructed round dice, real T096 calls; DEV AID, kit in
agent-room-fleet-kit/local-data/242-down-scene/slice_replay.py):Why this issue stays open after the fix. The convergence now rests on the model following the explanation. Code still sends
validatedIntentsas guidance without enforcing the prefix, so a model that ignores the text can ping-pong again. If it recurs, the deterministic follow-up is to reconcile the validated prefix in code before re-resolving (replace the leading intents by actorId with the validated copies), which makeslegalTargetsfor the rejected actor exact. A per-window attempt cap is not the answer (fail-forward doctrine, #193).Related: #242 (down scene, where a two-target party makes this trivially reachable), #433 (main-turn correction give-up).