mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-14 15:16:35 +03:00
Classify failed accepts as errors, and fix parallel lane race/all misuse
Two of the three new findings, plus the bug that chasing them exposed in my own earlier fix. The third is mitigated rather than broken; details below. Failed accepts reported success: live/completion.mjs only classifies a result as `error` when it carries `mode: 'error'`. Everything else unhandled falls through to `agent_done` with an ok ack, which is deliberate for the documented fallback paths (two tests pin it) but wrong for a real failure. So `accept_receipt_conflict` reported success, and reference/live.md's `handled: false` without `mode` bullet told the agent to "read file, find markers, edit" — hand-applying a second accept on top of the one the receipt already recorded. The same hole swallowed `source_locked`, which is mine: the earlier commit made lock contention return clean JSON so the agent could retry, but the classifier turned that failure into agent_done/ok, so the accept was dequeued and silently lost. Mark genuine failures with `mode: 'error'` through one `operationFailure` helper, and give live.md a `mode: "error"` bullet with per-error guidance: retry the same command on `source_locked`, never hand-edit, and on a receipt conflict report what the session actually resolved to. The deliberate fallback and markers-not-found handoffs stay untouched. parallel-compact lane orchestration: `Promise.race` settles on the first *settlement*, so one lane failing fast rejected the whole first-variant step while two lanes were still on their way to succeeding. `Promise.any` now takes the first success and only a total wipeout is fatal, reporting every lane's reason. The tail step's `Promise.all` surfaced a raw lane error non-deterministically; `Promise.allSettled` now reports how many lanes failed and why. Added a `requestImpl` seam so lane orchestration is testable without a provider key. Not a defect: the browser releasing Accept before the source write. That is the intended optimistic design, and it is safe because poll-lanes ranks accept at priority 0 against generate at 2, so a queued accept is always leased before a generate the user queues afterwards, even if the generate arrived first. Its source write lands inside the poll script before the next generate preflights. That invariant is load-bearing and had no tests at all; poll-lanes.mjs now has a suite covering it plus lease and type filtering. Prepared with AI assistance under maintainer direction. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -41,6 +41,20 @@ const ACCEPT_LOCK_WAIT_MS = 1_000;
|
||||
// value arriving over HTTP.
|
||||
const VARIANT_NUM_PATTERN = /^[0-9]{1,3}$/;
|
||||
|
||||
/**
|
||||
* A thrown accept/discard is a real failure, not a manual handoff.
|
||||
*
|
||||
* live/completion.mjs only classifies a result as `error` when it carries
|
||||
* `mode: 'error'`; anything else unhandled falls through to `agent_done` with a
|
||||
* successful ack, and reference/live.md then tells the agent to finish the edit
|
||||
* by hand. That is right for the documented fallback paths and wrong here: a
|
||||
* `source_locked` contention needs a retry (hand-editing races the publisher
|
||||
* holding the lock), and a crash needs surfacing, not a hand-applied guess.
|
||||
*/
|
||||
function operationFailure(err, extra = {}) {
|
||||
return { handled: false, mode: 'error', error: err.message, ...extra };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CLI
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -100,7 +114,13 @@ Output (JSON):
|
||||
console.log(JSON.stringify(sameOperation
|
||||
? { ...priorReceipt.result, handled: true, alreadyApplied: true }
|
||||
: {
|
||||
// mode: 'error' is what marks this a real failure rather than a manual
|
||||
// handoff. Without it, live/completion.mjs classifies the reply as
|
||||
// agent_done and reference/live.md tells the agent to "read file, find
|
||||
// markers, edit" by hand — which would apply a second, conflicting
|
||||
// accept on top of the one the receipt already recorded.
|
||||
handled: false,
|
||||
mode: 'error',
|
||||
error: 'accept_receipt_conflict',
|
||||
priorOperation: priorReceipt.operation,
|
||||
priorVariantId: priorReceipt.variantId ?? null,
|
||||
@@ -154,7 +174,7 @@ Output (JSON):
|
||||
{ waitMs: ACCEPT_LOCK_WAIT_MS },
|
||||
);
|
||||
} catch (err) {
|
||||
result = { handled: false, error: err.message };
|
||||
result = operationFailure(err);
|
||||
}
|
||||
emitResult({
|
||||
...result,
|
||||
@@ -175,7 +195,7 @@ Output (JSON):
|
||||
{ waitMs: ACCEPT_LOCK_WAIT_MS },
|
||||
);
|
||||
} catch (err) {
|
||||
result = { handled: false, error: err.message };
|
||||
result = operationFailure(err);
|
||||
}
|
||||
if (result.handled !== false) {
|
||||
removeSourceArtifactSession(id, process.cwd());
|
||||
@@ -211,7 +231,7 @@ Output (JSON):
|
||||
{ waitMs: ACCEPT_LOCK_WAIT_MS },
|
||||
);
|
||||
} catch (err) {
|
||||
result = { handled: false, error: err.message };
|
||||
result = operationFailure(err);
|
||||
}
|
||||
emitResult({
|
||||
...result,
|
||||
@@ -260,7 +280,7 @@ Output (JSON):
|
||||
{ waitMs: ACCEPT_LOCK_WAIT_MS },
|
||||
);
|
||||
} catch (err) {
|
||||
result = { handled: false, error: err.message };
|
||||
result = operationFailure(err);
|
||||
}
|
||||
emitResult({
|
||||
...result,
|
||||
@@ -336,7 +356,7 @@ Output (JSON):
|
||||
try {
|
||||
result = handleDiscard(id, lines, targetFile);
|
||||
} catch (err) {
|
||||
emitResult({ handled: false, file: relFile, error: err.message });
|
||||
emitResult(operationFailure(err, { file: relFile }));
|
||||
return;
|
||||
}
|
||||
emitResult({ handled: true, file: relFile, carbonize: false, ...result });
|
||||
@@ -345,7 +365,7 @@ Output (JSON):
|
||||
try {
|
||||
result = handleAccept(id, variantNum, lines, targetFile, paramValues);
|
||||
} catch (err) {
|
||||
emitResult({ handled: false, file: relFile, error: err.message });
|
||||
emitResult(operationFailure(err, { file: relFile }));
|
||||
return;
|
||||
}
|
||||
const acceptedOriginalText = result.acceptedOriginalText || '';
|
||||
|
||||
Reference in New Issue
Block a user