Skip to content

Commit 98a2564

Browse files
authored
fix(engine): correct the iterate-policy ceiling field docs to match the real precedence (#10131)
IterationState.maxIterations and costCeilingReached both documented an unconditional abandon ("regardless of self-review outcome"), but decideNextActionWithReason's own numbered precedence list puts the selfReview.kind === 'pass' branch (step 3) AHEAD of both ceilings (steps 4-5). So a clean predicted-gate pass at or past a ceiling still hands off, which the field docs said was impossible -- and the test suite had no case combining a pass with a reached ceiling, so the precedence between step 3 and steps 4-5 was unpinned. Correct both field docs to state the real contract (the ceiling abandons only when the self-review has not reached a clean pass; a clean pass still hands off, subject to autonomyLevel) and point at the precedence list as the one canonical statement. The precedence list, decideNextAction behaviour, reason strings and autonomyLevel handling are all unchanged -- this is a doc correction plus precedence-pinning tests. Closes #9997
1 parent 4e9cdbe commit 98a2564

3 files changed

Lines changed: 55 additions & 3 deletions

File tree

packages/loopover-engine/src/miner/iterate-policy.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,14 +71,20 @@ export type IterationState = {
7171
/** 1-indexed count of iterations attempted so far, INCLUDING this one. */
7272
iterationNumber: number;
7373
/** Hard ceiling enforced INSIDE this policy (#2333's own deliverable: not left to an external caller to
74-
* remember to enforce). `iterationNumber >= maxIterations` abandons regardless of self-review outcome. */
74+
* remember to enforce). `iterationNumber >= maxIterations` abandons when the self-review has NOT reached a
75+
* clean pass; a clean pass at or past the ceiling still hands off (subject to `autonomyLevel`), because the
76+
* `pass` branch precedes both ceilings in `decideNextActionWithReason`'s numbered precedence list -- see
77+
* that list for the one canonical statement of the ladder. */
7578
maxIterations: number;
7679
/** True when the loop's own cumulative cost ceiling (e.g. total driver turns spent across every iteration of
7780
* this attempt so far, not just this one) has been reached or exceeded -- the loop mechanics' (#2333) OWN
7881
* "max-cost ceiling enforced inside the loop" deliverable, alongside the iteration ceiling above. This
7982
* policy has no notion of what "cost" means; the caller computes the boolean from whatever cost signal it
80-
* tracks. Optional and defaults to not-reached, so `IterationState` fixtures that predate this field remain
81-
* valid. */
83+
* tracks. Like the iteration ceiling, it abandons only when the self-review has NOT reached a clean pass; a
84+
* clean pass at or past the ceiling still hands off (subject to `autonomyLevel`), because the `pass` branch
85+
* precedes both ceilings in `decideNextActionWithReason`'s numbered precedence list -- see that list for the
86+
* one canonical statement of the ladder. Optional and defaults to not-reached, so `IterationState` fixtures
87+
* that predate this field remain valid. */
8288
costCeilingReached?: boolean | undefined;
8389
selfReview: SelfReviewOutcome;
8490
/** The prior iteration's `fail` blocker codes, for the no-progress detector -- `null` when there is no prior

packages/loopover-engine/test/iterate-policy.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,3 +249,35 @@ test("autonomy #6560: rejectionSignaled and an ambiguous self-review still win o
249249
);
250250
assert.equal(ambiguous.abandonReason, "self_review_ambiguous");
251251
});
252+
253+
// #9997: the `maxIterations`/`costCeilingReached` field docs claimed the ceilings abandon "regardless of
254+
// self-review outcome", but the precedence list puts the `pass` branch (step 3) AHEAD of both ceilings
255+
// (steps 4-5), so a clean pass at or past a ceiling still hands off. These pin that precedence end to end so
256+
// a future reordering fails the suite instead of silently discarding a passing attempt.
257+
test("#9997: a clean pass AT the iteration ceiling still hands off (pass precedes the ceiling)", () => {
258+
const decision = decideNextActionWithReason(passingState({ iterationNumber: 5, maxIterations: 5 }));
259+
assert.equal(decision.action, "handoff");
260+
assert.equal(decision.abandonReason, undefined);
261+
});
262+
263+
test("#9997: a clean pass with the cost ceiling reached still hands off", () => {
264+
const decision = decideNextActionWithReason(passingState({ costCeilingReached: true }));
265+
assert.equal(decision.action, "handoff");
266+
assert.equal(decision.abandonReason, undefined);
267+
});
268+
269+
test("#9997: a clean pass with BOTH ceilings reached still hands off", () => {
270+
const decision = decideNextActionWithReason(passingState({ iterationNumber: 5, maxIterations: 5, costCeilingReached: true }));
271+
assert.equal(decision.action, "handoff");
272+
assert.equal(decision.abandonReason, undefined);
273+
});
274+
275+
test("#9997: the two branches that DO win over a pass still do, pinning the ladder end to end", () => {
276+
// rejectionSignaled (step 1) beats a pass...
277+
const rejected = decideNextActionWithReason(passingState({ rejectionSignaled: true }));
278+
assert.equal(rejected.abandonReason, "rejection_signaled");
279+
// ...and autonomy "observe" narrows the pass->handoff transition to an abandon, NOT max_iterations_reached,
280+
// even at a reached ceiling (the pass branch is entered first, and observe abandons inside it).
281+
const observed = decideNextActionWithReason(passingState({ autonomyLevel: "observe", iterationNumber: 5, maxIterations: 5 }));
282+
assert.equal(observed.abandonReason, "autonomy_observe_only");
283+
});

test/unit/engine-iterate-policy-autonomy.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,4 +114,18 @@ describe("autonomy never overrides the higher-precedence ladder steps (#6560)",
114114
expect(noProgress.abandonReason).toBe("no_progress");
115115
},
116116
);
117+
118+
it("#9997: a clean pass at or past a ceiling still hands off — the pass branch precedes both ceilings", () => {
119+
// The field docs used to claim the ceilings abandon "regardless of self-review outcome"; the precedence
120+
// list (step 3 before steps 4-5) is the real contract, so a clean pass wins over a reached ceiling.
121+
for (const state of [
122+
passingState({ iterationNumber: 5, maxIterations: 5 }),
123+
passingState({ costCeilingReached: true }),
124+
passingState({ iterationNumber: 5, maxIterations: 5, costCeilingReached: true }),
125+
]) {
126+
const decision = decideNextActionWithReason(state);
127+
expect(decision.action).toBe("handoff");
128+
expect(decision.abandonReason).toBeUndefined();
129+
}
130+
});
117131
});

0 commit comments

Comments
 (0)