From 981bc6933fb1db74e1ace8134741de298d49cfea Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN Date: Sat, 3 Oct 2026 00:34:53 -0400 Subject: [PATCH] runtime: fairly order eligible continuation turns --- docs/TEST_INTEGRITY.md | 4 ++++ .../2026-10-03-rolling-continuation.md | 2 ++ src/runtime/continuation.mjs | 7 +++++-- test/fleet/legacy-runtime-recovery.test.mjs | 14 ++++++++++++++ 4 files changed, 25 insertions(+), 2 deletions(-) diff --git a/docs/TEST_INTEGRITY.md b/docs/TEST_INTEGRITY.md index 8687437..5b7f49b 100644 --- a/docs/TEST_INTEGRITY.md +++ b/docs/TEST_INTEGRITY.md @@ -18,6 +18,10 @@ test_integrity_note: given: "An owner-started executor already occupies the configured automatic capacity" when: "The continuation controller runs" then: "No additional automatic turn starts and no attempt is charged" + - id: SCN-runtime-continuation-fairness + given: "One specialist keeps advancing revisions while another eligible identity has not started" + when: "A slot becomes available" + then: "The least-recently-dispatched peer starts before the returning specialist" observable_outcomes: - "Actual launch ordering, daily charges, coordination messages and explicit held-work reason" determinism_controls: diff --git a/docs/spec_changes/2026-10-03-rolling-continuation.md b/docs/spec_changes/2026-10-03-rolling-continuation.md index 3933ee0..3f48e19 100644 --- a/docs/spec_changes/2026-10-03-rolling-continuation.md +++ b/docs/spec_changes/2026-10-03-rolling-continuation.md @@ -12,4 +12,6 @@ Resume instructions now request eligible implementation/verification/coordinatio Status exposes occupied guard identities and no-progress holds. Installed release and reboot persistence remain unqualified. Existing native pilot service executes this source candidate on its next activation; the daily limit remains 12 automatic attempts, not a token-cost governor. +Eligible identities are ordered least-recently-dispatched within the current UTC day, with the Manager first only on ties. A repeatedly revised specialist task must not consume every freed slot while an eligible peer has never started. + Deterministic acceptance scenarios and anti-cheat notes are in docs/TEST_INTEGRITY.md. Independent live qualification remains necessary; do not close broader event-driven or persistent-operations umbrellas based on this source fix alone. diff --git a/src/runtime/continuation.mjs b/src/runtime/continuation.mjs index 8365e4c..14f945e 100644 --- a/src/runtime/continuation.mjs +++ b/src/runtime/continuation.mjs @@ -77,7 +77,9 @@ export function planContinuation(control, backlog, { at = new Date(), limit = 3 candidates.push({ areaId, fingerprint, signal, workKey: taskKey(active), messageCount: freshMessages.length, taskIds: active.map(t => t.id), continueWork }); } - candidates.sort((a, b) => Number(b.areaId === 'session-manager') - Number(a.areaId === 'session-manager')); + const ordinal = area => state.handled[area]?.dispatchedDay === day ? state.handled[area].dispatchedOrdinal ?? 0 : 0; + candidates.sort((a, b) => ordinal(a.areaId) - ordinal(b.areaId) + || Number(b.areaId === 'session-manager') - Number(a.areaId === 'session-manager')); return { enabled: true, day, attempts, remainingTurns: state.maxTurnsPerDay - attempts, occupiedAreas: occupiedAreas(control), held, candidates: candidates.slice(0, Math.min(limit, state.maxTurnsPerDay - attempts)), mutationPerformed: false }; @@ -106,7 +108,8 @@ export async function runContinuation(control, backlog, { actorId, launch, at, c const state = read(control); if (state.day !== current.day) { state.day = current.day; state.attempts = 0; } state.attempts++; - state.handled[candidate.areaId] = candidate.signal; + state.handled[candidate.areaId] = { ...candidate.signal, + dispatchedDay: current.day, dispatchedOrdinal: state.attempts }; save(control, state); // Reserve before launch; unknown attempts are never refunded. let result; try { result = launch(candidate.areaId); } catch { result = null; } diff --git a/test/fleet/legacy-runtime-recovery.test.mjs b/test/fleet/legacy-runtime-recovery.test.mjs index 16c143c..ede4a1c 100644 --- a/test/fleet/legacy-runtime-recovery.test.mjs +++ b/test/fleet/legacy-runtime-recovery.test.mjs @@ -318,6 +318,20 @@ test('SCN-runtime-continuation-capacity: a manually held executor counts against } finally { f.control.close(); } }); +test('SCN-runtime-continuation-fairness: a fresh task revision does not let one specialist starve an unstarted peer', async () => { + const f = fixture({ extraWorker: true }); + try { + f.control.reportStatus({ areaId: f.areaId, state: 'idle', summary: 'Idle' }); + configureContinuation(f.control, { actorId: 'owner', enabled: true, maxTurnsPerDay: 3 }); + const task = { id: 'TASK-changing', owner: f.areaId, state: 'assigned', revision: 1, dependencies: [] }; + f.control.sendOwnerRequest({ actorId: 'owner', recipient: 'extra-worker', body: 'Peer work' }); + const starts = []; + await runContinuation(f.control, { list: () => [task] }, { actorId: 'owner', limit: 1, + launch: area => { starts.push(area); if (area === f.areaId) task.revision++; return 0; } }); + assert.deepEqual(starts.slice(0, 2), [f.areaId, 'extra-worker']); + } finally { f.control.close(); } +}); + test('SCN-legacy-runtime-recovery-project: evidence cannot be applied to a different checkout with cloned project/session fields', () => { const first = fixture(); const second = fixture();