Recovery primitive: given a run's suspended RunSummary, heals every
CURRENTLY suspended step whose exact (suspendedAt, resumeCount)
fingerprint has no approval record of ANY status, and that has no record
of a later suspension, then files it fresh.
Safe to call on every status() read of a suspended run, rather than only
after a confirmed bridge failure, because per step:
any record (any status) already bound to the CURRENT fingerprint means
nothing to do — a matching pending/claimed/escalated record makes
queueApprovalForSuspension's own idempotent create() a no-op anyway
(this skips the round-trip), and a matching DECIDED record means the
decision landed but its resume has not (yet, or ever, on the wedge path
this exists for) re-queued the next gate — re-filing would double-file
a gate correctly waiting on its own resume, the decide -> resume
in-flight window;
any record bound to a LATER suspension of the step (a greater
resumeCount) means the summary predates it — a status read outside the
run's lock can return such a summary — so the step is left alone:
nothing is superseded and nothing is filed;
otherwise, every STALE OPEN record for that step (pending/claimed/
escalated, bound to a suspension the step has since moved past —
produced e.g. by the raw grant-free resume route re-suspending the step
while an earlier request still sits open) is SUPERSEDED first
(ApprovalService.supersedeStaleAsPrincipal: a CAS transition straight to
'rejected', attributed to principalActor(principal), audited as
approval.supersede, never through decide() — so it never touches the run)
before the fresh file, closing the loop where a stale-but-open record
otherwise never heals (every poll re-lists, finds the same open record via
the open-step uniqueness index, and re-files nothing). A stale DECIDED
record needs no supersede — it is already terminal and excluded from
grant derivation by its own (non-'approved', or fingerprint-mismatched)
status.
if a supersede loses its CAS (a real decision won the race between the
list() above and the supersede), this step is left alone for this
round entirely — no fresh file — rather than clobbering or
double-filing over a decision that just landed; the next status() read
re-evaluates against a fresh summary and a fresh list().
Delegates the actual filing to queueApprovalForSuspension against a copy of
summary narrowed to only the healed paths, so a step with a live or
in-flight record is never touched, and a timer step's stale records are
superseded with nothing filed in their place. Generic runner summaries carry durable
execution provenance. A suspended summary without it cannot be filed: using
the system principal as a fallback would erase an unknown human initiator and
weaken separation of duties. Agent hosts may pass an explicit trusted value.
Recovery primitive: given a run's suspended RunSummary, heals every CURRENTLY suspended step whose exact (suspendedAt, resumeCount) fingerprint has no approval record of ANY status, and that has no record of a later suspension, then files it fresh. Safe to call on every status() read of a suspended run, rather than only after a confirmed bridge failure, because per step:
Delegates the actual filing to queueApprovalForSuspension against a copy of
summarynarrowed to only the healed paths, so a step with a live or in-flight record is never touched, and a timer step's stale records are superseded with nothing filed in their place. Generic runner summaries carry durable execution provenance. A suspended summary without it cannot be filed: using the system principal as a fallback would erase an unknown human initiator and weaken separation of duties. Agent hosts may pass an explicit trusted value.