missions: retry failed phases + auto-purge on re-launch
ci / gates (push) Successful in 5s
ci / frontend (push) Successful in 26s
ci / rust (push) Successful in 4m30s
ci / e2e (push) Skipped
ci / publish (push) Successful in 2m40s

Every re-attempted phase now starts with a clean slate:

  - phase_runner::launch_phase DELETEs prior status IN ('failed',
    'cancelled') topology_runs for the phase before enqueuing the
    new ones. Completed runs are kept for audit; only the failure
    noise from earlier attempts goes.
  - POST /api/missions/{id}/phases/{phase_id}/retry — resets a
    failed/cancelled phase to 'pending' (auth-scoped to the calling
    workspace + guarded on mission.status='running'). phase_runner
    picks it up on the next 10s tick.
  - MissionCanvas phase card grows a coral 'Retry' button, visible
    only when phase.status='failed' and mission.status='running'.
    Click → resets + refreshes; the prior failed run rows disappear
    from the card as soon as phase_runner enqueues the new attempt.

Design: auto-purge in phase_runner rather than a separate 'clear
failed runs' endpoint. Users don't have to manually clean up before
retrying; the runner does it as part of the natural work of firing
a fresh attempt.

Verified: cargo check + tsc + eslint --quiet all green.
This commit is contained in:
Omar Sobh
2026-07-21 13:14:39 -07:00
parent a1d1097b52
commit 94fecb526c
5 changed files with 90 additions and 0 deletions
@@ -16,6 +16,7 @@ import {
getMission,
listMissionRuns,
refineMission,
retryMissionPhase,
setMissionDescription,
setMissionStatus,
triggerBenchmark,
@@ -759,6 +760,37 @@ export function MissionCanvas({
})()}
{mission.status === "running" || mission.status === "completed" ? (
<div style={{ display: "flex", gap: 6, marginTop: 6 }}>
{p.status === "failed" && mission.status === "running" && (
<button
type="button"
onClick={async () => {
if (phaseBusy === `retry:${p.id}`) return;
setPhaseBusy(`retry:${p.id}`);
setError(null);
try {
await retryMissionPhase(mission.id, p.id);
await load();
} catch (e) {
setError(
e instanceof Error ? e.message : "retry failed",
);
} finally {
setPhaseBusy(null);
}
}}
disabled={phaseBusy === `retry:${p.id}`}
title="Reset this phase to pending; prior failed runs are purged. phase_runner picks it up in ≤10s."
style={{
...secondaryBtn,
borderColor: "rgba(255,138,122,.45)",
color: "#ff8a7a",
background: "rgba(255,138,122,.08)",
opacity: phaseBusy === `retry:${p.id}` ? 0.5 : 1,
}}
>
{phaseBusy === `retry:${p.id}` ? "Retrying…" : "Retry"}
</button>
)}
{p.kind === "security_scan" && (
<button
type="button"