| 1 | package extension |
| 2 | |
| 3 | // ResumeDecision is the product-facing recovery answer for a generation. |
| 4 | // Irreversible receipts never produce CleanRollback=true. |
| 5 | type ResumeDecision struct { |
| 6 | // AllowResume is always true when the process is healthy enough to continue |
| 7 | // the session; external side effects may already have happened. |
| 8 | AllowResume bool `json:"allowResume"` |
| 9 | // CleanRollback is true only when no irreversible work completed and all |
| 10 | // compensatable work finished compensation successfully. |
| 11 | CleanRollback bool `json:"cleanRollback"` |
| 12 | // HasIrreversible reports completed irreversible effects. |
| 13 | HasIrreversible bool `json:"hasIrreversible"` |
| 14 | // Notes are human-readable doctor/UI lines. |
| 15 | Notes []string `json:"notes,omitempty"` |
| 16 | // Blocking receipt IDs that prevent CleanRollback (not AllowResume). |
| 17 | Blocking []string `json:"blocking,omitempty"` |
| 18 | } |
| 19 | |
| 20 | // DecideResume allows resume but denies clean rollback for irreversible or |
| 21 | // uncompensated effects. Empty or fully compensated generations are clean; |
| 22 | // irreversible receipts are never rewritten to "rolled_back". |
| 23 | func DecideResume(store *ReceiptStore, gen uint64) ResumeDecision { |
| 24 | if store == nil { |
| 25 | return ResumeDecision{AllowResume: true, CleanRollback: true} |
| 26 | } |
| 27 | rec := store.AssessRecoverability(gen) |
| 28 | return ResumeDecision{ |
| 29 | AllowResume: true, |
| 30 | CleanRollback: rec.Clean, |
| 31 | HasIrreversible: rec.HasIrreversible, |
| 32 | Notes: rec.Notes, |
| 33 | Blocking: rec.Blocking, |
| 34 | } |
| 35 | } |
| 36 | |
| 37 | // DecideResumeDefault uses the compatibility owner's ledger. |
| 38 | func DecideResumeDefault(gen uint64) ResumeDecision { |
| 39 | return DecideResume(RuntimeOwnerOrDefault(nil).Receipts, gen) |
| 40 | } |
| 41 | |
| 42 | // RecordProviderSubmit marks a provider request as already submitted |
| 43 | // (irreversible). Call after the sidecar accepts stream open. |
| 44 | func RecordProviderSubmit(generation uint64, streamID, owner string) { |
| 45 | RuntimeOwnerOrDefault(nil).RecordProviderSubmit(generation, streamID, owner) |
| 46 | } |
| 47 | |
| 48 | // RecordMessageSent marks a user-visible outbound message as sent. |
| 49 | func RecordMessageSent(generation uint64, messageID, owner string) { |
| 50 | RuntimeOwnerOrDefault(nil).RecordMessageSent(generation, messageID, owner) |
| 51 | } |
| 52 |