feat(recovery): route failed write-less stages to recovery instead of futile retry
Adds the failure-ticket + recovery-routing mechanism: a deterministic gate->capability table gates whether a stage has agency to fix its own failure, opens a FailureTicketOpenedEvent when it doesn't, and routes to a metadata role=recovery stage (per-stage budget, cap 2, not reset by TransitionExecuted) instead of retrying in place. Extends salvage decisions with a RECOVER option so the review-gate judge can also route to recovery, unifies deterministic-gate and review-gate routing through routeToRecovery(), and has ExecutionPlanCompiler synthesize a write-capable recovery stage + edge for freestyle plans. Read-only tools (file_read, list_dir) are now always available on any tool-granting stage so a recovery stage can inspect the write-less stage's failure without flooding context via shell ls -R.
This commit is contained in:
@@ -221,7 +221,52 @@ func (m *Model) applyServer(msg protocol.ServerMessage) {
|
||||
m.appendAction(msg.SessionID, "✎", "wrote "+path+countSuffix(add, del))
|
||||
m.appendRouter(msg.SessionID, RouterEntry{Role: "tool", Content: *msg.Diff})
|
||||
} else {
|
||||
m.appendAction(msg.SessionID, "✓", actionToolText(msg.ToolName, msg.Summary))
|
||||
label := msg.ToolName
|
||||
if len(msg.AffectedEntities) > 0 {
|
||||
label += " " + strings.Join(msg.AffectedEntities, ", ")
|
||||
}
|
||||
m.appendAction(msg.SessionID, "✓", actionToolText(label, msg.Summary))
|
||||
}
|
||||
case protocol.TypeReviewFindings:
|
||||
if s := m.session(msg.SessionID); s != nil {
|
||||
findings := make([]ReviewFinding, 0, len(msg.ReviewFindings))
|
||||
for _, f := range msg.ReviewFindings {
|
||||
fix := ""
|
||||
if f.SuggestedFix != nil {
|
||||
fix = *f.SuggestedFix
|
||||
}
|
||||
findings = append(findings, ReviewFinding{
|
||||
Severity: f.Severity,
|
||||
Confidence: f.Confidence,
|
||||
Category: f.Category,
|
||||
Target: f.Target,
|
||||
Message: f.Message,
|
||||
SuggestedFix: fix,
|
||||
Correctness: f.Correctness,
|
||||
})
|
||||
}
|
||||
s.LastReview = &ReviewResult{
|
||||
StageID: msg.StageID,
|
||||
Verdict: msg.ReviewVerdict,
|
||||
Findings: findings,
|
||||
Blocked: msg.ReviewBlocked,
|
||||
}
|
||||
detail := msg.ReviewVerdict
|
||||
if len(findings) > 0 {
|
||||
detail += " (" + plural(len(findings), "finding") + ")"
|
||||
}
|
||||
if msg.ReviewBlocked {
|
||||
detail += " · blocked"
|
||||
}
|
||||
s.LastOutput = "review: " + detail
|
||||
s.addEvent(nowMillis(), "ReviewFindings", msg.StageID+": "+detail)
|
||||
icon := "✓"
|
||||
if msg.ReviewVerdict == "FAIL" {
|
||||
icon = "✕"
|
||||
} else if msg.ReviewVerdict == "WARN" {
|
||||
icon = "▲"
|
||||
}
|
||||
m.appendAction(msg.SessionID, icon, "review "+detail)
|
||||
}
|
||||
case protocol.TypeToolAssessed:
|
||||
if s := m.session(msg.SessionID); s != nil {
|
||||
|
||||
Reference in New Issue
Block a user