Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions packages/loopover-engine/src/miner/iterate-policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,14 +71,20 @@ export type IterationState = {
/** 1-indexed count of iterations attempted so far, INCLUDING this one. */
iterationNumber: number;
/** Hard ceiling enforced INSIDE this policy (#2333's own deliverable: not left to an external caller to
* remember to enforce). `iterationNumber >= maxIterations` abandons regardless of self-review outcome. */
* remember to enforce). `iterationNumber >= maxIterations` abandons when the self-review has NOT reached a
* clean pass; a clean pass at or past the ceiling still hands off (subject to `autonomyLevel`), because the
* `pass` branch precedes both ceilings in `decideNextActionWithReason`'s numbered precedence list -- see
* that list for the one canonical statement of the ladder. */
maxIterations: number;
/** True when the loop's own cumulative cost ceiling (e.g. total driver turns spent across every iteration of
* this attempt so far, not just this one) has been reached or exceeded -- the loop mechanics' (#2333) OWN
* "max-cost ceiling enforced inside the loop" deliverable, alongside the iteration ceiling above. This
* policy has no notion of what "cost" means; the caller computes the boolean from whatever cost signal it
* tracks. Optional and defaults to not-reached, so `IterationState` fixtures that predate this field remain
* valid. */
* tracks. Like the iteration ceiling, it abandons only when the self-review has NOT reached a clean pass; a
* clean pass at or past the ceiling still hands off (subject to `autonomyLevel`), because the `pass` branch
* precedes both ceilings in `decideNextActionWithReason`'s numbered precedence list -- see that list for the
* one canonical statement of the ladder. Optional and defaults to not-reached, so `IterationState` fixtures
* that predate this field remain valid. */
costCeilingReached?: boolean | undefined;
selfReview: SelfReviewOutcome;
/** The prior iteration's `fail` blocker codes, for the no-progress detector -- `null` when there is no prior
Expand Down
32 changes: 32 additions & 0 deletions packages/loopover-engine/test/iterate-policy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,3 +249,35 @@ test("autonomy #6560: rejectionSignaled and an ambiguous self-review still win o
);
assert.equal(ambiguous.abandonReason, "self_review_ambiguous");
});

// #9997: the `maxIterations`/`costCeilingReached` field docs claimed the ceilings abandon "regardless of
// self-review outcome", but the precedence list puts the `pass` branch (step 3) AHEAD of both ceilings
// (steps 4-5), so a clean pass at or past a ceiling still hands off. These pin that precedence end to end so
// a future reordering fails the suite instead of silently discarding a passing attempt.
test("#9997: a clean pass AT the iteration ceiling still hands off (pass precedes the ceiling)", () => {
const decision = decideNextActionWithReason(passingState({ iterationNumber: 5, maxIterations: 5 }));
assert.equal(decision.action, "handoff");
assert.equal(decision.abandonReason, undefined);
});

test("#9997: a clean pass with the cost ceiling reached still hands off", () => {
const decision = decideNextActionWithReason(passingState({ costCeilingReached: true }));
assert.equal(decision.action, "handoff");
assert.equal(decision.abandonReason, undefined);
});

test("#9997: a clean pass with BOTH ceilings reached still hands off", () => {
const decision = decideNextActionWithReason(passingState({ iterationNumber: 5, maxIterations: 5, costCeilingReached: true }));
assert.equal(decision.action, "handoff");
assert.equal(decision.abandonReason, undefined);
});

test("#9997: the two branches that DO win over a pass still do, pinning the ladder end to end", () => {
// rejectionSignaled (step 1) beats a pass...
const rejected = decideNextActionWithReason(passingState({ rejectionSignaled: true }));
assert.equal(rejected.abandonReason, "rejection_signaled");
// ...and autonomy "observe" narrows the pass->handoff transition to an abandon, NOT max_iterations_reached,
// even at a reached ceiling (the pass branch is entered first, and observe abandons inside it).
const observed = decideNextActionWithReason(passingState({ autonomyLevel: "observe", iterationNumber: 5, maxIterations: 5 }));
assert.equal(observed.abandonReason, "autonomy_observe_only");
});
14 changes: 14 additions & 0 deletions test/unit/engine-iterate-policy-autonomy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,4 +114,18 @@ describe("autonomy never overrides the higher-precedence ladder steps (#6560)",
expect(noProgress.abandonReason).toBe("no_progress");
},
);

it("#9997: a clean pass at or past a ceiling still hands off — the pass branch precedes both ceilings", () => {
// The field docs used to claim the ceilings abandon "regardless of self-review outcome"; the precedence
// list (step 3 before steps 4-5) is the real contract, so a clean pass wins over a reached ceiling.
for (const state of [
passingState({ iterationNumber: 5, maxIterations: 5 }),
passingState({ costCeilingReached: true }),
passingState({ iterationNumber: 5, maxIterations: 5, costCeilingReached: true }),
]) {
const decision = decideNextActionWithReason(state);
expect(decision.action).toBe("handoff");
expect(decision.abandonReason).toBeUndefined();
}
});
});