| 1 | package recovery |
| 2 | |
| 3 | import ( |
| 4 | "encoding/json" |
| 5 | "time" |
| 6 | ) |
| 7 | |
| 8 | // Phase is a derived view of recovery progress for compatibility snapshots. |
| 9 | // Runtime truth is "has failure" and "has waiter", not a stored phase enum. |
| 10 | type Phase string |
| 11 | |
| 12 | const ( |
| 13 | PhaseIdle Phase = "idle" |
| 14 | PhaseDiagnosing Phase = "diagnosing" |
| 15 | PhaseAwaitingDecision Phase = "awaiting_decision" |
| 16 | ) |
| 17 | |
| 18 | // ChangeKind classifies how the proposed recovery action differs from the |
| 19 | // original approach. |
| 20 | type ChangeKind string |
| 21 | |
| 22 | const ( |
| 23 | ChangeSameStrategy ChangeKind = "same_strategy" |
| 24 | ChangeStrategy ChangeKind = "strategy" |
| 25 | ChangeScope ChangeKind = "scope" |
| 26 | ChangeRisk ChangeKind = "risk" |
| 27 | ChangeUncertain ChangeKind = "uncertain" |
| 28 | ) |
| 29 | |
| 30 | // ReviewOutcome is the independent recovery reviewer's decision. |
| 31 | type ReviewOutcome string |
| 32 | |
| 33 | const ( |
| 34 | ReviewContinue ReviewOutcome = "continue" |
| 35 | ReviewConfirm ReviewOutcome = "confirm" |
| 36 | ) |
| 37 | |
| 38 | // FailureClass separates execution reliability from permission and product |
| 39 | // decisions. Permission/sandbox/user blocks never become FailureEvents. |
| 40 | type FailureClass string |
| 41 | |
| 42 | const ( |
| 43 | FailureClassExecution FailureClass = "execution" |
| 44 | FailureClassMutation FailureClass = "mutation" |
| 45 | FailureClassTransient FailureClass = "transient" |
| 46 | FailureClassVerification FailureClass = "verification" |
| 47 | ) |
| 48 | |
| 49 | // ReviewVerdict is the strict JSON shape the recovery reviewer must produce. |
| 50 | // Host already knows failure/diagnosis/proposed action; only outcome fields are |
| 51 | // required. Extra fields from older models are tolerated on parse. |
| 52 | type ReviewVerdict struct { |
| 53 | Outcome ReviewOutcome `json:"outcome"` |
| 54 | ChangeKind ChangeKind `json:"change_kind"` |
| 55 | Rationale string `json:"rationale"` |
| 56 | |
| 57 | // Legacy optional fields kept for older model outputs and tests. |
| 58 | FailureSummary string `json:"failure_summary,omitempty"` |
| 59 | Diagnosis string `json:"diagnosis,omitempty"` |
| 60 | ProposedAction string `json:"proposed_action,omitempty"` |
| 61 | } |
| 62 | |
| 63 | // FailureEvent records failure evidence for diagnosis and reviewer context. |
| 64 | // SafeRetryLeft/RepeatCount/DiagnosisNotes remain on the wire for old |
| 65 | // snapshots; runtime budgets live on taskRuntime. |
| 66 | type FailureEvent struct { |
| 67 | Class FailureClass `json:"class,omitempty"` |
| 68 | Tool string `json:"tool"` |
| 69 | ArgsSummary string `json:"args_summary,omitempty"` |
| 70 | Subject string `json:"subject,omitempty"` |
| 71 | ErrSummary string `json:"err_summary,omitempty"` |
| 72 | OutputExcerpt string `json:"output_excerpt,omitempty"` |
| 73 | SourceAgent string `json:"source_agent,omitempty"` |
| 74 | TaskID string `json:"task_id,omitempty"` |
| 75 | // TaskScopeID persists only stable goal scopes. Ordinary turn scopes are |
| 76 | // runtime-local and intentionally omitted so a restart cannot revive a stale |
| 77 | // technical latch for a new user turn. It never acts as the Episode budget key. |
| 78 | TaskScopeID string `json:"task_scope_id,omitempty"` |
| 79 | ReadOnly bool `json:"read_only,omitempty"` |
| 80 | Verification bool `json:"verification,omitempty"` |
| 81 | Mutates bool `json:"mutates,omitempty"` |
| 82 | RepeatCount int `json:"repeat_count,omitempty"` |
| 83 | CreatedAt time.Time `json:"created_at,omitempty"` |
| 84 | Args json.RawMessage `json:"args,omitempty"` |
| 85 | Fingerprint string `json:"fingerprint,omitempty"` |
| 86 | SafeRetryLeft int `json:"safe_retry_left,omitempty"` |
| 87 | DiagnosisNotes []string `json:"diagnosis_notes,omitempty"` |
| 88 | } |
| 89 | |
| 90 | // PendingProposal is the mutation paused for user confirmation. |
| 91 | // It is held only in the temporary waiter table, never as durable task state. |
| 92 | type PendingProposal struct { |
| 93 | Tool string `json:"tool"` |
| 94 | Subject string `json:"subject,omitempty"` |
| 95 | Preview string `json:"preview,omitempty"` |
| 96 | Args json.RawMessage `json:"args,omitempty"` |
| 97 | Fingerprint string `json:"fingerprint,omitempty"` |
| 98 | SourceAgent string `json:"source_agent,omitempty"` |
| 99 | ChangeKind ChangeKind `json:"change_kind,omitempty"` |
| 100 | Rationale string `json:"rationale,omitempty"` |
| 101 | Diagnosis string `json:"diagnosis,omitempty"` |
| 102 | Failure string `json:"failure,omitempty"` |
| 103 | Proposed string `json:"proposed,omitempty"` |
| 104 | PlanBefore string `json:"plan_before,omitempty"` |
| 105 | PlanAfter string `json:"plan_after,omitempty"` |
| 106 | // TaskGrant fields are transient host-classified scope. They are deliberately |
| 107 | // omitted from snapshots and never supplied by the model or wire client. |
| 108 | TaskGrantKey string `json:"-"` |
| 109 | TaskGrantTaskScope string `json:"-"` |
| 110 | TaskGrantDisplay string `json:"-"` |
| 111 | } |
| 112 | |
| 113 | // TaskState is the persistable / debug view of one task's recovery state. |
| 114 | // Runtime truth is taskRuntime; Snapshot/Restore project to and from this shape. |
| 115 | // |
| 116 | // Persistence projection writes only LastFailure as historical evidence. |
| 117 | // Failure / ConsecutiveFails / ReviewBlocks may still appear in live Snapshot() |
| 118 | // for debugging, and old on-disk values are migrated to evidence without re-arming. |
| 119 | type TaskState struct { |
| 120 | Phase Phase `json:"phase"` |
| 121 | Failure *FailureEvent `json:"failure,omitempty"` |
| 122 | LastFailure *FailureEvent `json:"last_failure,omitempty"` |
| 123 | Pending *PendingProposal `json:"pending,omitempty"` |
| 124 | ApprovalID string `json:"approval_id,omitempty"` |
| 125 | ConsecutiveFails int `json:"consecutive_fails,omitempty"` |
| 126 | ReviewBlocks int `json:"review_blocks,omitempty"` |
| 127 | TailInjected bool `json:"tail_injected,omitempty"` |
| 128 | // EpisodeID is runtime/debug only and never written by persistence projection. |
| 129 | EpisodeID string `json:"episode_id,omitempty"` |
| 130 | // EpisodeStopped / StopReason are live debug views only. |
| 131 | EpisodeStopped bool `json:"episode_stopped,omitempty"` |
| 132 | StopReason string `json:"stop_reason,omitempty"` |
| 133 | } |
| 134 | |
| 135 | // Snapshot decodes historical Auto Guard sidecars. New runtimes do not write |
| 136 | // or restore this state. |
| 137 | type Snapshot struct { |
| 138 | Tasks map[string]*TaskState `json:"tasks,omitempty"` |
| 139 | } |
| 140 | |
| 141 | // Metrics are content-free counters for release observation. |
| 142 | // They never record parameters, paths, or error bodies. |
| 143 | type Metrics struct { |
| 144 | FailureEvents int64 |
| 145 | RuleContinues int64 |
| 146 | ReviewContinues int64 |
| 147 | HumanPrompts int64 |
| 148 | HumanContinues int64 |
| 149 | TaskGrantContinues int64 |
| 150 | TaskGrantUses int64 |
| 151 | HumanRevises int64 |
| 152 | ReviewErrors int64 |
| 153 | ReviewLatencyMsSum int64 |
| 154 | ReviewLatencyCount int64 |
| 155 | RepeatPrompts int64 |
| 156 | |
| 157 | // Episode / generation counters (content-free). |
| 158 | OperationStops int64 |
| 159 | EpisodeFailureStops int64 |
| 160 | ReviewStops int64 |
| 161 | StoppedOpRetryStops int64 |
| 162 | ModeResets int64 |
| 163 | EpisodeRotations int64 |
| 164 | StaleObservationsIgnored int64 |
| 165 | } |
| 166 | |
| 167 | // ApprovalKindRecovery is the Approval.Kind value for recovery cards. |
| 168 | const ApprovalKindRecovery = "recovery" |
| 169 | |
| 170 | // ApprovalKindTool and ApprovalKindPlan keep ordinary approval kinds explicit. |
| 171 | const ( |
| 172 | ApprovalKindTool = "tool" |
| 173 | ApprovalKindPlan = "plan" |
| 174 | ) |
| 175 |