Long review queue: new claims need a few peer reviews first

While more than peerReviewFirstAt (100) functions wait for peer review, claiming
new work needs reviewsPerClaim (3) verdicts since the agent's last claim;
otherwise 429 with what to do. Work already held, reclaiming one's own
returned functions, paused reviewers and agents with nothing they may review
are not affected. The guidance says so while the queue is long.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RNwtXPYqFu3oKaaXwYwNWW
This commit is contained in:
Saty 2026-10-18 18:34:09 +02:00
commit 3b697d9b11
3 changed files with 72 additions and 4 deletions

View file

@ -300,17 +300,42 @@ export function createApp(ctx) {
});
// Work responses carry the project's current policy for agents (guidance.js).
const guided = (r) => withGuidance(store, r);
const guided = (r) => withGuidance(store, r, { peerReviewFirstAt: config.peerReviewFirstAt ?? 100 });
router.on('GET', '/api/tasks/:id', ({ req, params }) => {
auth(req);
return guided(tasks.context(intParam(params.id)));
});
// While the peer review queue is long, new work costs a few reviews first: verdicts are what
// keeps finished work moving to main. Work already held, and an agent's own returned functions,
// are not affected; neither is anyone with nothing they may review.
const reviewFirst = (user) => {
const at = config.peerReviewFirstAt ?? 100;
const waiting = store.get(`SELECT COUNT(*) AS n FROM attempts WHERE state = 'peer_review'`).n;
if (waiting <= at) return;
if (store.get('SELECT 1 FROM users WHERE id = ? AND peer_review_blocked = 1', user.id)) return;
if (!peers.available(user, 1).length) return;
const need = config.reviewsPerClaim ?? 3;
const lastClaim = store.get(`SELECT MAX(at) AS t FROM events WHERE user_id = ? AND type IN ('claim', 'claim_batch')`, user.id).t ?? 0;
const done = store.get('SELECT COUNT(*) AS n FROM peer_reviews WHERE reviewer_id = ? AND created_at > ?', user.id, lastClaim).n;
if (done >= need) return;
throw new HttpError(
429,
`The peer review queue is long (${waiting} functions waiting). Do ${need - done} more peer review${need - done > 1 ? 's' : ''} first (td_claim_peer_review, then td_submit_peer_review for each function), then claim new work. Keep working on and submitting what you already hold.`,
);
};
router.on('POST', '/api/tasks/claim', async ({ req }) => {
const user = auth(req);
const body = await readJson(req, 4096);
if (body.taskId) return guided(tasks.context(tasks.claim(user, intParam(body.taskId))));
if (body.taskId) {
// Taking back one's own returned function is not new work.
const own = store.get('SELECT 1 FROM tasks WHERE id = ? AND preferred_user = ? AND preferred_until > ?', intParam(body.taskId), user.id, Date.now());
if (!own) reviewFirst(user);
return guided(tasks.context(tasks.claim(user, intParam(body.taskId))));
}
reviewFirst(user);
// "The next task" is the next group: one function at a time across classes wastes the
// agent's context and the compile per submission.
const ids = tasks.claimNext(user, 50);
@ -386,6 +411,7 @@ export function createApp(ctx) {
router.on('POST', '/api/tasks/claim-batch', async ({ req }) => {
const user = auth(req);
const body = await readJson(req, 4096);
reviewFirst(user);
const hexOrNull = (x) => (x == null ? null : typeof x === 'string' ? Number.parseInt(x, 16) : Number(x));
const ids = tasks.claimBatch(user, { unit: body.unit ? String(body.unit) : null, start: hexOrNull(body.start), end: hexOrNull(body.end), limit: body.limit ? Number(body.limit) : 50 });
const v = store.currentVersion();

View file

@ -34,7 +34,12 @@ export function resetAgentGuidance(store) {
}
/** A response with the guidance first, so an agent reads it before the details. */
export function withGuidance(store, response) {
const g = agentGuidance(store);
export function withGuidance(store, response, { peerReviewFirstAt = 100 } = {}) {
let g = agentGuidance(store);
// A long review queue holds finished work back from main: say so while it lasts.
const waiting = store.get(`SELECT COUNT(*) AS n FROM attempts WHERE state = 'peer_review'`).n;
if (g && waiting > peerReviewFirstAt) {
g += ` RIGHT NOW: ${waiting} functions wait for peer review. Before claiming new work, do peer reviews (td_list_peer_reviews / td_claim_peer_review); new claims need a few reviews first while the queue is this long.`;
}
return g ? { guidance: g, ...response } : response;
}

View file

@ -580,6 +580,43 @@ test('the symbol table is only sent when it changed; downloads come compressed w
await plain.arrayBuffer();
});
test('a long review queue makes new claims wait for a few peer reviews', async () => {
await activeUser('rfauthor');
const author = ctx.accounts.byName('rfauthor');
const worker = await activeUser('rfworker');
const v = ctx.store.currentVersion();
const id = ctx.store.ensureSymbol({ kind: 'function', versionId: v.id, addr: 0x40f800, size: 8, unit: 'src/rf.cpp' }).symbol.id;
ctx.tasks.generate();
const task = ctx.store.get('SELECT id FROM tasks WHERE symbol_id = ?', id).id;
ctx.store.run(`UPDATE tasks SET state = 'peer_review' WHERE id = ?`, task);
const attempt = Number(ctx.store.run(`INSERT INTO attempts (task_id, user_id, base_commit, files_json, state, created_at) VALUES (?, ?, ?, '{}', 'peer_review', ?)`, task, author.id, ctx.repo.head(), now()).lastInsertRowid);
const before = ctx.config.peerReviewFirstAt;
ctx.config.peerReviewFirstAt = 0;
try {
const refused = await call('POST', '/api/tasks/claim-batch', {}, worker);
assert.equal(refused.status, 429);
assert.match(refused.body.error, /peer review/);
const g = await call('GET', `/api/tasks/${task}`, null, worker);
assert.match(g.body.guidance, /wait for peer review/, 'the guidance says so too');
// Reviews until new work is allowed: three verdicts, or nothing left this worker may review.
let ok = refused;
for (let i = 0; i < 4 && ok.status === 429; i++) {
const c = await call('POST', '/api/peer-reviews/claim', {}, worker);
for (const f of c.body.functions ?? []) {
await call('POST', `/api/peer-reviews/${f.attemptId}/verdict`, { verdict: 'same', reason: 'the same branches and calls as the original' }, worker);
}
ok = await call('POST', '/api/tasks/claim-batch', {}, worker);
}
assert.notEqual(ok.status, 429, JSON.stringify(ok.body));
assert.ok(ctx.store.get('SELECT COUNT(*) AS n FROM peer_reviews WHERE reviewer_id = ?', ctx.accounts.byName('rfworker').id).n <= 3);
for (const t of ok.body.tasks ?? []) await call('POST', `/api/tasks/${t.task}/release`, {}, worker);
} finally {
ctx.config.peerReviewFirstAt = before;
ctx.store.run(`UPDATE attempts SET state = 'rejected' WHERE id = ?`, attempt);
ctx.store.run(`UPDATE tasks SET state = 'done' WHERE id = ?`, task);
}
});
test('the crew view is public and shows no account details', async () => {
const token = await activeUser('crewview');
await call('GET', '/api/tasks', null, token); // the agent checks in