fix: make join approval atomic

This commit is contained in:
User
2026-07-14 15:24:06 +02:00
parent bc02213d3e
commit 333874a755
9 changed files with 314 additions and 34 deletions
+37 -7
View File
@@ -246,9 +246,9 @@ function tht(ctx, args, input) {
// Runs a privileged tht call; converts any failure into an actionable textResult
// (never propagates a raw "Command failed" to the model).
function relayIfThtFails(ctx, args, recovery) {
function relayIfThtFails(ctx, args, recovery, input) {
try {
tht(ctx, args);
tht(ctx, args, input);
return null;
} catch (e) {
const cliMsg = (e.stderr || e.message || String(e)).toString().trim();
@@ -380,6 +380,16 @@ export function resolveConfirmOutcome(resp) {
return { kind: "unknown", choice };
}
export function isJoinReviewApproval(resp, optionIds) {
if (resp?.kind !== "join-review" || !Array.isArray(resp.choices)) return false;
const expected = new Set(optionIds);
const chosen = new Set(resp.choices);
return expected.size === optionIds.length &&
chosen.size === resp.choices.length &&
chosen.size === expected.size &&
[...chosen].every((id) => expected.has(id));
}
// True when a reviewer_decide has no merito options but is allowed to close empty
// and advance (the empty memory phase F2). The gate then shows an info notice and
// auto-advances instead of presenting an empty checklist.
@@ -764,7 +774,15 @@ export default function (pi) {
options: meritOptions,
...(phase === "F2" ? memorySelectionWidgetProps(opts) : {}),
});
const resp = await emitAndWait(ctx, widget);
let resp;
for (;;) {
resp = await emitAndWait(ctx, widget);
if (resp.control === "freetext" || resp.control === "back" || resp.control === "exit")
break;
if (!joinOnly || isJoinReviewApproval(resp, meritOptions.map((option) => option.id)))
break;
await reLoop(ctx);
}
if (resp.control === "freetext") {
return textResult(
`Altro (reviewer): ${resp.text}. Riformula la proposta tenendo conto.`,
@@ -777,11 +795,23 @@ export default function (pi) {
const chosen = joinOnly
? opts.filter((o) => !isReserved(o.label))
: opts.filter((o) => (resp.choices ?? []).includes(o.id));
for (const c of chosen) {
const d = c.decision;
const err = relayIfThtFails(ctx, decisionAddArgs(session, d), "");
if (joinOnly) {
const decisions = chosen.map((choice) => choice.decision);
const err = relayIfThtFails(
ctx,
["decision", "add-join-set", "--session", session, "--doc", "-"],
"Il set di join non è stato registrato; correggi l'errore e riprova.",
JSON.stringify(decisions),
);
if (err) return err;
toAdd.push(d);
toAdd.push(...decisions);
} else {
for (const c of chosen) {
const d = c.decision;
const err = relayIfThtFails(ctx, decisionAddArgs(session, d), "");
if (err) return err;
toAdd.push(d);
}
}
if (advance) advanceIfReady(ctx, session);
return textResult(