Merge sends back only a function that no longer passes; clearer commits and progress

- When main changed beneath an approved function and it no longer passes,
  that function goes back to its author and the rest of its batch merges
  without it, instead of the batch waiting forever.
- Commit messages say per function when the code was already on main or only
  an annotation was added to existing code (identical copies, INLINED code),
  so "5 functions" with a small diff reads right.
- Progress counts match_unresolved (identical bytes, some referenced data
  unnamed) as byte-for-byte matches.

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 13:03:22 +02:00
commit b4f1089f7c
3 changed files with 90 additions and 4 deletions

View file

@ -5,7 +5,7 @@ import { now } from './db.js';
import { hasRole } from './accounts.js';
import { STATUS } from '../shared/compare.js';
import { returnBatch, batchAttempts, settleBatch } from './batches.js';
import { withoutFunctions } from './split.js';
import { withoutFunctions, functionRegions } from './split.js';
import { RANK, bestResults, BIG_BATCH } from './verify.js';
// Merges run in two lanes: one big merge (a header batch that must recompile many units) at a
@ -190,6 +190,30 @@ export class Merger {
}
// The approved attempts go back to the batch they came from, which then merges as a whole.
// One function of a merge fails on current main: it goes back to its author, the others wait
// (approved) in their batch and merge on the next round, split from it.
sendBackOne(batch, attempts, failed, feedback, split) {
this.store.tx(() => {
this.store.run(`UPDATE attempts SET state = 'needs_cleanup', result_json = json_set(COALESCE(result_json, '{}'), '$.feedback', json(?)) WHERE id = ?`, JSON.stringify(feedback), failed.id);
this.store.run(
`UPDATE tasks SET state = 'needs_cleanup', preferred_user = ?, preferred_until = ?, updated_at = ? WHERE id = ?`,
failed.user_id, now() + 6 * 3600_000, now(), failed.task_id,
);
const home = split ? split.parent.id : batch.id;
for (const a of attempts) {
if (a.id === failed.id) continue;
this.store.run(`UPDATE attempts SET batch_id = ?, state = 'approved' WHERE id = ?`, home, a.id);
this.store.run(`UPDATE tasks SET state = 'approved', updated_at = ? WHERE id = ?`, now(), a.task_id);
}
if (split) {
this.store.run(`UPDATE attempts SET batch_id = ? WHERE id = ?`, home, failed.id);
this.store.run(`UPDATE batches SET state = 'superseded' WHERE id = ?`, batch.id);
}
this.store.run(`UPDATE batches SET state = 'open', result_json = json_remove(COALESCE(result_json, '{}'), '$.noSplit') WHERE id = ?`, home);
});
this.log.info(`batch ${batch.id}: ${failed.symbol_id} no longer passes on current main (${feedback.summary}); sent back, the rest merges without it`);
}
undoSplit(parent, child, approved, reason) {
this.store.tx(() => {
for (const a of approved) {
@ -262,7 +286,13 @@ export class Merger {
// Byte-identical, or confirmed by peers and approved by a human; anything worse bounces.
const identical = ev.state === 'approved' || ev.state === 'in_review';
const peerConfirmed = ev.state === 'peer_review' && this.peers?.confirmed(a.id);
if (!identical && !peerConfirmed) return bounce(`re-verification on current main failed for ${a.symbol_id}: ${ev.feedback.summary}`, { feedback: ev.feedback });
if (!identical && !peerConfirmed) {
// Code that builds but no longer passes (main changed beneath it): only this function goes
// back to its author; the rest of the batch merges without it on the next round.
const builds = !ev.feedback.compileErrors?.length && ev.feedback.summary !== 'compilation failed';
if (builds && attempts.length > 1) return this.sendBackOne(batch, attempts, a, ev.feedback, split);
return bounce(`re-verification on current main failed for ${a.symbol_id}: ${ev.feedback.summary}`, { feedback: ev.feedback });
}
if (peerConfirmed) confirmedIds.add(a.symbol_id);
results.push({ id: a.symbol_id, name: ev.feedback.target?.demangled ?? null, status: ev.feedback.target?.status, peerConfirmed: !!peerConfirmed });
}
@ -280,7 +310,9 @@ export class Merger {
results.length === 1
? `Decompile ${results[0].id}${results[0].name ? ` (${results[0].name})` : ''}`
: `Decompile ${results.length} functions in ${[...new Set(Object.keys(files))].slice(0, 2).join(', ')}${Object.keys(files).length > 2 ? ', ...' : ''}`;
const lines = results.map((r) => `- ${r.id}${r.name ? ` ${r.name}` : ''}: ${r.peerConfirmed ? 'logic confirmed by peer review' : 'byte-identical'}`);
const head = this.repo.head();
const change = (id) => this.codeChange(id, merged.files, head);
const lines = results.map((r) => `- ${r.id}${r.name ? ` ${r.name}` : ''}: ${r.peerConfirmed ? 'logic confirmed by peer review' : 'byte-identical'}${change(r.id)}`);
const message =
`${title}\n\nBatch #${batch.id} by ${author}: ${identicalCount} byte-identical, ${results.length - identicalCount} confirmed by peer review.\n\n${lines.join('\n')}\n` +
(peers.length ? `\nPeer-reviewed-by: ${peers.join(', ')}` : '') +
@ -323,6 +355,27 @@ export class Merger {
this.log.info(`merged batch ${batch.id} (${attempts.length} functions) as ${commit.slice(0, 10)}`);
}
/**
* What a merge changes about one function's source, for the commit message: an annotation on
* code that was already there (an identical copy, or code marked INLINED) shows up in the diff
* as one line, and a function that was already right as none.
*/
codeChange(id, files, head) {
const strip = (text) => text.split('\n').filter((l) => !/^\s*\/\/\s*(FUNCTION|STUB|INLINED)\b/.test(l)).join('\n');
const body = (t, r) => t.split('\n').slice(r.start, r.end).join('\n');
for (const [file, text] of Object.entries(files)) {
const now = functionRegions(text)?.get(id);
if (!now) continue;
const before = this.repo.show(head, file);
const old = before === null ? null : functionRegions(before)?.get(id);
if (old && body(before, old) === body(text, now)) return ' (already on main)';
// Same body as an existing definition, only annotated now.
if (before !== null && strip(before).includes(strip(body(text, now)))) return ' (annotation added to existing code)';
return '';
}
return '';
}
/**
* After the approved part of a batch merged, the rest of the batch is based on that commit:
* its files become the merged files plus the still-pending functions.

View file

@ -68,7 +68,8 @@ async function loadProgress() {
const body = $('progress-body');
body.replaceChildren();
body.className = '';
const exact = p.byStatus.find((s) => s.status === 'exact') || { n: 0, bytes: 0 };
// Byte for byte: exact, and identical bytes whose referenced data has no name yet.
const exact = p.byStatus.filter((s) => s.status === 'exact' || s.status === 'match_unresolved').reduce((t, s) => ({ n: t.n + s.n, bytes: t.bytes + s.bytes }), { n: 0, bytes: 0 });
const pct = p.bytes ? (exact.bytes / p.bytes) * 100 : 0;
const gauge = el('div', null, 'gauge');

View file

@ -522,6 +522,38 @@ test('renaming an account keeps its tokens working', async () => {
assert.throws(() => ctx.accounts.rename('renamed', 'TakenName', null), /taken/);
});
test('a function that no longer passes at merge goes back alone; the rest merges', async () => {
delete ctx.merger.pump; // an earlier test stubbed it on the instance
const author = await activeUser('sbauthor');
const v = ctx.store.currentVersion();
const x = ctx.store.ensureSymbol({ kind: 'function', versionId: v.id, addr: 0x40e000, size: 8, unit: 'src/sb.cpp' }).symbol.id;
const y = ctx.store.ensureSymbol({ kind: 'function', versionId: v.id, addr: 0x40e040, size: 8, unit: 'src/sb.cpp' }).symbol.id;
ctx.tasks.generate();
let yStatus = STATUS.EXACT;
const real = ctx.verifier.check;
ctx.verifier.check = async (files) => {
const text = files['src/sb.cpp'] ?? '';
const functions = [];
if (text.includes('int fx()')) functions.push({ id: x, status: STATUS.EXACT, score: 1, file: 'src/sb.cpp', codeHash: 'hx', relocations: [], size: { ours: 8, original: 8 } });
if (text.includes('int fy()')) functions.push({ id: y, status: yStatus, score: yStatus === STATUS.EXACT ? 1 : 0.4, file: 'src/sb.cpp', codeHash: 'hy', relocations: [], size: { ours: 8, original: 8 } });
return { check: { annotationErrors: [], units: [{ file: 'src/sb.cpp', compiled: true, lintErrors: [], unannotated: [], bindings: {}, inferred: [], includes: [], errors: [], functions }] }, rec: { conflicts: [], unknown: [], learned: [], learnedAddresses: [] }, changedHeaders: [], units: ['src/sb.cpp'] };
};
try {
const claim = await call('POST', '/api/tasks/claim-batch', { unit: 'src/sb.cpp' }, author);
const files = { 'src/sb.cpp': `// FUNCTION: ${x}\nint fx() { return 1; }\n// FUNCTION: ${y}\nint fy() { return 2; }\n` };
const sub = await call('POST', '/api/batches', { taskIds: claim.body.tasks.map((t) => t.task), baseCommit: ctx.repo.head(), files }, author);
await waitFor(async () => !['queued', 'verifying'].includes(ctx.store.get('SELECT state FROM batches WHERE id = ?', sub.body.batchId).state));
yStatus = STATUS.MISMATCH; // main changed beneath it
ctx.store.run('DELETE FROM batch_checks WHERE batch_id = ?', sub.body.batchId); // force a fresh compile at merge
await ctx.merger.pump();
assert.equal(ctx.store.get('SELECT state FROM tasks WHERE symbol_id = ?', y).state, 'needs_cleanup', 'the failing function goes back');
await ctx.merger.pump();
assert.equal(ctx.store.get('SELECT state FROM tasks WHERE symbol_id = ?', x).state, 'done', 'the other one merges');
} finally {
ctx.verifier.check = real;
}
});
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