feat(recovery): one bounded post-failure diagnostic before terminal FAILED (#294)
At the terminal boundary (repair ladder spent, or a non-recoverable gate exhaustion), run exactly one tool-free diagnostic inference per terminal failure fingerprint over recorded facts only. A validated — materially new, confident, recovery-stage-available — RecoveryProposal routes once into the existing recovery stage via the ticket machinery, bypassing the spent route budget but bounded by a one-diagnosis-per-fingerprint dedupe so no loop is possible. Otherwise the run stays terminal FAILED (safe degrade when no diagnoser is wired). - New PostFailureDiagnosedEvent + nested RecoveryProposal (registered in eventModule); every observation/proposal/decision/route recorded for replay. - PostFailureDiagnoser seam (nullable, mirrors SalvageJudge) + DiagnosisInput built from the event log only (no fresh workspace observation). - diagnosisMinConfidence tuning knob. - Hooked at both terminal boundaries: routeToRecovery ladder-exhausted and decideGateExhaustion. Tests: RecoveryRoutingTest (route-once-then-bounded, low-confidence stays terminal), EventsTest serialization round-trip (proposal + null). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -169,6 +169,47 @@ data class RetrySalvageDecidedEvent(
|
||||
val rationale: String,
|
||||
) : EventPayload
|
||||
|
||||
/**
|
||||
* A structured, untrusted recovery proposal produced by the one-shot post-failure diagnostic
|
||||
* (design task #294). The diagnostic inference is tool-free and reads only recorded facts; this is
|
||||
* its proposal. [expectedFingerprint] is the failure fingerprint the proposed [recoveryAction] is
|
||||
* predicted to change the run to — the kernel routes only when it is materially different from the
|
||||
* current terminal fingerprint (i.e. a genuinely new path, not the same dead end). [noRecovery]
|
||||
* lets the model explicitly decline; [confidence] is thresholded by the kernel. LLM-proposed and
|
||||
* therefore untrusted (invariant #7): validated deterministically before it can affect routing.
|
||||
*/
|
||||
@Serializable
|
||||
data class RecoveryProposal(
|
||||
val diagnosis: String,
|
||||
val citedEvidence: String,
|
||||
val recoveryAction: String,
|
||||
val expectedFingerprint: String,
|
||||
val confidence: Double,
|
||||
val noRecovery: Boolean = false,
|
||||
)
|
||||
|
||||
/**
|
||||
* Records the one-shot post-failure diagnostic (design task #294): when a run is about to become
|
||||
* terminal, exactly one tool-free diagnostic inference runs per terminal-failure [fingerprint]. The
|
||||
* nondeterministic [proposal] (LLM-backed, null when none/unparseable), the kernel's deterministic
|
||||
* validation [decision], and whether it [routed] into the existing recovery stage are all recorded
|
||||
* here so replay reproduces the decision without re-invoking the diagnoser (invariants #7/#9). The
|
||||
* per-fingerprint dedupe that bounds this to one attempt keys off this event.
|
||||
*/
|
||||
@Serializable
|
||||
@SerialName("PostFailureDiagnosed")
|
||||
data class PostFailureDiagnosedEvent(
|
||||
val sessionId: SessionId,
|
||||
val stageId: StageId,
|
||||
val gate: String,
|
||||
val fingerprint: String,
|
||||
val proposal: RecoveryProposal?,
|
||||
// ROUTE | TERMINAL_NO_PROPOSAL | TERMINAL_NO_RECOVERY | TERMINAL_LOW_CONFIDENCE |
|
||||
// TERMINAL_NOT_MATERIAL | TERMINAL_NO_ROUTE
|
||||
val decision: String,
|
||||
val routed: Boolean,
|
||||
) : EventPayload
|
||||
|
||||
/**
|
||||
* A stage repeatedly blocked on a missing build prerequisite (see repeatedBuildCriticalReferenceBlock,
|
||||
* design 2026-07-15 §#170) AND the stage both holds file_write and declares the path in-scope, so the
|
||||
|
||||
@@ -65,6 +65,7 @@ import com.correx.core.events.events.RepoKnowledgeRetrievedEvent
|
||||
import com.correx.core.events.events.RepoMapComputedEvent
|
||||
import com.correx.core.events.events.RetryAttemptedEvent
|
||||
import com.correx.core.events.events.RetrySalvageDecidedEvent
|
||||
import com.correx.core.events.events.PostFailureDiagnosedEvent
|
||||
import com.correx.core.events.events.FailureTicketOpenedEvent
|
||||
import com.correx.core.events.events.BuildPrerequisiteBootstrapAttemptedEvent
|
||||
import com.correx.core.events.events.WorkspaceVerificationObservedEvent
|
||||
@@ -161,6 +162,7 @@ val eventModule = SerializersModule {
|
||||
subclass(WorkflowCompletedEvent::class)
|
||||
subclass(RetryAttemptedEvent::class)
|
||||
subclass(RetrySalvageDecidedEvent::class)
|
||||
subclass(PostFailureDiagnosedEvent::class)
|
||||
subclass(FailureTicketOpenedEvent::class)
|
||||
subclass(BuildPrerequisiteBootstrapAttemptedEvent::class)
|
||||
subclass(WorkspaceVerificationObservedEvent::class)
|
||||
|
||||
Reference in New Issue
Block a user