fix: a recovered replay job can no longer hijack the active run

leaseNextJob hands back any pending replay_article job, it has no idea about
runs, and the worker was attributing whatever came back to whichever run was
active. One recovered dead letter from run 1 would have been stamped with run
2's id, given run 2's feedback brief, and dragged run 2's cursor to wherever
that old article sits in the archive. A pinned run would then decide its set was
finished after a couple of articles. There are 139 dead letters and they are
built to recover, so this was not hypothetical.

The idempotency key already says which run enqueued the job. Ask it.

Also: refuse to inherit the parent's model label when starting a run. Inheriting
is exactly how run 1 came to be labelled qwen for predictions deepseek made.

The split moves to the replay container's actual restart time rather than the
commit timestamp five minutes later. Verified the running container really does
have the instrument rules, the de-anchoring and the enum before trusting it as
the boundary. It makes no difference to the partition, there are no replay
predictions at all between 15:57 and midnight that day, but the boundary should
be the thing that actually changed the prompt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WnNxwxfXSbeNtjvtz5gayb
This commit is contained in:
ImBenji
2026-09-08 01:01:36 +01:00
co-authored by Claude Opus 5
parent ec29e64e96
commit b246bd9d4b
5 changed files with 109 additions and 17 deletions
+28 -1
View File
@@ -14,7 +14,7 @@ const { createOrderIntent } = require('../src/autonomy/orderIntents');
const { enqueueCoordinatorEvent, reconcileArchiveBatch, reconcileLiveBatch, isTransientCoordinatorFailure } = require('../workers/autonomyWorker');
const { buildGraphContext } = require('../src/autonomy/graphContext');
const { buildPrompt } = require('../workers/coordinatorWorker');
const { scheduleNext, replayPrompt } = require('../workers/replayWorker');
const { scheduleNext, replayPrompt, runForJob } = require('../workers/replayWorker');
const { refreshHistoricalCalibration, createDecisions, ensureCalibrationColumns } = require('../workers/calibrationWorker');
test('autonomy schema and leased jobs are restart-safe', () => {
@@ -183,6 +183,33 @@ test('the feedback brief reaches the prompt and stays out of it when empty', ()
assert.ok(!replayPrompt(article).includes('CALIBRATION FEEDBACK'));
});
test('a leftover job from an older run cannot hijack the active run', () => {
const db = new Database(':memory:');
initAutonomySchema(db);
const first = db.prepare(`
INSERT INTO autonomy_replay_runs (watermark_at, strategy_version, prompt_version, coordinator_model, status)
VALUES ('2020-01-09T00:00:00Z', 'autonomy-1', 'replay-coordinator-1', 'old-model', 'paused')
`).run().lastInsertRowid;
const second = db.prepare(`
INSERT INTO autonomy_replay_runs (watermark_at, strategy_version, prompt_version, coordinator_model, feedback_brief)
VALUES ('2020-01-09T00:00:00Z', 'autonomy-2', 'replay-coordinator-2', 'new-model', 'you over-call positive')
`).run().lastInsertRowid;
const active = db.prepare('SELECT * FROM autonomy_replay_runs WHERE id=?').get(second);
// a recovered dead letter from run 1, leased while run 2 is the active one
const owner = runForJob(db, { id: 9, idempotency_key: `replay:${first}:article:4242` }, active);
assert.equal(owner.id, first, 'the job belongs to the run that enqueued it');
assert.equal(owner.feedback_brief, null, 'and it must not be handed run 2 brief');
assert.equal(owner.prompt_version, 'replay-coordinator-1');
const own = runForJob(db, { id: 10, idempotency_key: `replay:${second}:article:1` }, active);
assert.equal(own.id, second);
// unattributable jobs fall back rather than being dropped, but loudly
assert.equal(runForJob(db, { id: 11, idempotency_key: null }, active).id, second);
assert.equal(runForJob(db, { id: 12, idempotency_key: 'replay:999:article:1' }, active).id, second);
});
test('calibration and policy abstain on insufficient evidence', () => {
const calibration = calibrateOutcomes([
{ excess_return: 0.02, direction_correct: 1 },