fix(release): preserve durable recovery capacity
This commit is contained in:
@@ -1006,10 +1006,12 @@
|
||||
renderReleaseRunStoreUnavailable(runsResult.error);
|
||||
} else {
|
||||
renderReleaseRunList(runsResult.data?.runs || []);
|
||||
const pendingRecoveryResult = await recoverPendingReleaseRun();
|
||||
if (pendingRecoveryResult === null && releaseState.currentRun?.run_id) {
|
||||
const pendingCreateResult = await recoverPendingReleaseRun();
|
||||
const pendingCommandResult = await recoverPendingRunCommands();
|
||||
const recoveryAttempted = pendingCreateResult !== null || pendingCommandResult !== null;
|
||||
if (!recoveryAttempted && releaseState.currentRun?.run_id) {
|
||||
await loadReleaseRun(releaseState.currentRun.run_id, { restoreSelection: false });
|
||||
} else if (pendingRecoveryResult === null) {
|
||||
} else if (!recoveryAttempted) {
|
||||
renderIdlePlan();
|
||||
}
|
||||
}
|
||||
@@ -1262,6 +1264,7 @@
|
||||
releaseState.currentRun = resumed;
|
||||
renderReleaseRun(resumed);
|
||||
} catch (error) {
|
||||
if (deterministicRunCommandError(error)) clearInFlightRunCommand(commandKey, commandRequestId);
|
||||
elements.runStatus.textContent = "error";
|
||||
elements.runOutput.insertAdjacentHTML("afterbegin", `<div class="action"><h3>${pill("error", "block")} Resume failed</h3><p>${escapeHtml(error.message)}</p></div>`);
|
||||
} finally {
|
||||
@@ -1280,6 +1283,7 @@
|
||||
releaseState.currentRun = retried;
|
||||
renderReleaseRun(retried);
|
||||
} catch (error) {
|
||||
if (deterministicRunCommandError(error)) clearInFlightRunCommand(commandKey, commandRequestId);
|
||||
elements.runOutput.insertAdjacentHTML("afterbegin", `<div class="action"><h3>${pill("error", "block")} Retry preparation failed</h3><p>${escapeHtml(error.message)}</p></div>`);
|
||||
}
|
||||
}
|
||||
@@ -1300,6 +1304,7 @@
|
||||
releaseState.currentRun = reconciled;
|
||||
renderReleaseRun(reconciled);
|
||||
} catch (error) {
|
||||
if (deterministicRunCommandError(error)) clearInFlightRunCommand(commandKey, commandRequestId);
|
||||
elements.runOutput.insertAdjacentHTML("afterbegin", `<div class="action"><h3>${pill("error", "block")} Reconciliation failed</h3><p>${escapeHtml(error.message)}</p></div>`);
|
||||
button.disabled = false;
|
||||
}
|
||||
@@ -1315,6 +1320,88 @@
|
||||
}
|
||||
}
|
||||
|
||||
function pendingRunCommandRequest(commandKey, requestIdToReplay) {
|
||||
if (typeof commandKey !== "string" || typeof requestIdToReplay !== "string") return null;
|
||||
if (commandKey.startsWith("resume:")) {
|
||||
const runId = commandKey.slice("resume:".length);
|
||||
if (!runId || runId.includes(":")) return null;
|
||||
return {
|
||||
path: `/api/release-runs/${encodeURIComponent(runId)}/resume`,
|
||||
body: { request_id: requestIdToReplay },
|
||||
};
|
||||
}
|
||||
if (commandKey.startsWith("retry:")) {
|
||||
const remainder = commandKey.slice("retry:".length);
|
||||
const separator = remainder.indexOf(":");
|
||||
const runId = remainder.slice(0, separator);
|
||||
const stepId = remainder.slice(separator + 1);
|
||||
if (separator < 1 || !stepId) return null;
|
||||
return {
|
||||
path: `/api/release-runs/${encodeURIComponent(runId)}/steps/${encodeURIComponent(stepId)}/retry`,
|
||||
body: { request_id: requestIdToReplay },
|
||||
};
|
||||
}
|
||||
if (commandKey.startsWith("reconcile:")) {
|
||||
const remainder = commandKey.slice("reconcile:".length);
|
||||
const runSeparator = remainder.indexOf(":");
|
||||
const outcomeSeparator = remainder.lastIndexOf(":");
|
||||
const runId = remainder.slice(0, runSeparator);
|
||||
const stepId = remainder.slice(runSeparator + 1, outcomeSeparator);
|
||||
const outcome = remainder.slice(outcomeSeparator + 1);
|
||||
if (runSeparator < 1 || outcomeSeparator <= runSeparator + 1 || !["effect_absent", "effect_succeeded", "unresolved"].includes(outcome)) return null;
|
||||
return {
|
||||
path: `/api/release-runs/${encodeURIComponent(runId)}/steps/${encodeURIComponent(stepId)}/reconcile`,
|
||||
body: { request_id: requestIdToReplay, outcome, confirm: "RECONCILE" },
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function recoverPendingRunCommands() {
|
||||
const commands = readInFlightRunCommands();
|
||||
const entries = Object.entries(commands);
|
||||
if (!entries.length) return null;
|
||||
let attempted = false;
|
||||
let recoveredRun = null;
|
||||
let pendingError = null;
|
||||
let rejectedError = null;
|
||||
for (const [commandKey, requestIdToReplay] of entries) {
|
||||
const command = pendingRunCommandRequest(commandKey, requestIdToReplay);
|
||||
if (!command) {
|
||||
clearInFlightRunCommand(commandKey, requestIdToReplay);
|
||||
continue;
|
||||
}
|
||||
attempted = true;
|
||||
try {
|
||||
recoveredRun = await postJson(command.path, command.body);
|
||||
clearInFlightRunCommand(commandKey, requestIdToReplay);
|
||||
} catch (error) {
|
||||
if (deterministicRunCommandError(error)) {
|
||||
clearInFlightRunCommand(commandKey, requestIdToReplay);
|
||||
rejectedError = error;
|
||||
} else {
|
||||
pendingError = error;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (recoveredRun) {
|
||||
releaseState.currentRun = recoveredRun;
|
||||
restoreReleaseRunSelection(recoveredRun);
|
||||
renderReleaseRun(recoveredRun);
|
||||
elements.releaseRun.value = recoveredRun.run_id;
|
||||
}
|
||||
if (pendingError) {
|
||||
elements.runOutput.insertAdjacentHTML("afterbegin", `<div class="action"><h3>${pill("pending", "warn")} Saved command recovery is still pending</h3><p>${escapeHtml(pendingError.message)}</p><p class="action-meta">The same private request identifier will be replayed after refresh.</p></div>`);
|
||||
} else if (rejectedError) {
|
||||
elements.runOutput.insertAdjacentHTML("afterbegin", `<div class="action"><h3>${pill("rejected", "block")} Saved command was not accepted</h3><p>${escapeHtml(rejectedError.message)}</p><p class="action-meta">The server returned a deterministic client conflict; the stale request identifier was cleared.</p></div>`);
|
||||
}
|
||||
return attempted ? Boolean(recoveredRun) : null;
|
||||
}
|
||||
|
||||
function deterministicRunCommandError(error) {
|
||||
return error?.status >= 400 && error.status < 500;
|
||||
}
|
||||
|
||||
function inFlightRunCommandId(commandKey, prefix) {
|
||||
const commands = readInFlightRunCommands();
|
||||
if (typeof commands[commandKey] === "string") return commands[commandKey];
|
||||
|
||||
Reference in New Issue
Block a user