返回 DeepSeek-Reasonix
goal_legacy.go
根目录 / internal / control / goal_legacy.go
1 package control
2
3 import (
4 "strings"
5
6 "reasonix/internal/evidence"
7 )
8
9 type legacyGoalRestore struct {
10 taskID string
11 epoch uint64
12 explicit bool
13 }
14
15 func normalizeBudgetClass(goal, class string, legacyMode GoalResearchMode) string {
16 switch class {
17 case budgetClassSimple, budgetClassWrite, budgetClassResearch:
18 return class
19 default:
20 if strings.TrimSpace(goal) == "" && legacyMode != GoalResearchOn {
21 return ""
22 }
23 return budgetClassForLegacyMode(goal, legacyMode)
24 }
25 }
26
27 // blockLegacyRestore fails closed only while the decoded sidecar still owns the
28 // active Goal epoch. The archive identity is held by Controller's legacy-only
29 // recovery boundary, never by the Goal FSM.
30 func (g *goalMachine) blockLegacyRestore(expectedEpoch uint64, reason string) (uint64, bool) {
31 g.mu.Lock()
32 defer g.mu.Unlock()
33 if g.continuationEpoch != expectedEpoch {
34 return 0, false
35 }
36 g.status = GoalStatusBlocked
37 g.stopCause = stopCauseLegacyArchive
38 g.block = clipGoalReason(reason)
39 g.continuationEpoch++
40 return g.continuationEpoch, true
41 }
42
43 // failLegacyRestorePersistence keeps a recovered archive retryable when the
44 // sidecar replacement fails. The recovered Goal text may remain in memory, but
45 // the Goal stays fail-closed while Controller retains the legacy identity until
46 // a later resume commits the migration durably.
47 func (g *goalMachine) failLegacyRestorePersistence(expectedEpoch uint64, reason string) (uint64, bool) {
48 g.mu.Lock()
49 defer g.mu.Unlock()
50 if g.continuationEpoch != expectedEpoch {
51 return 0, false
52 }
53 g.status = GoalStatusBlocked
54 g.stopCause = stopCauseLegacyArchive
55 g.block = clipGoalReason(reason)
56 g.continuationEpoch++
57 return g.continuationEpoch, true
58 }
59
60 // clearLegacyTaskID completes the sidecar migration after the Goal-only state
61 // has been durably written. The epoch check prevents a late completion from
62 // clearing the identity of a newer migration.
63 func (g *goalMachine) clearLegacyTaskID(expectedEpoch uint64) bool {
64 g.mu.Lock()
65 defer g.mu.Unlock()
66 if g.continuationEpoch != expectedEpoch {
67 return false
68 }
69 g.legacyTaskID = ""
70 return true
71 }
72
73 func (g *goalMachine) legacyArchiveBlockedState() (goal string, epoch uint64, ok bool) {
74 g.mu.Lock()
75 defer g.mu.Unlock()
76 if g.status != GoalStatusBlocked || g.stopCause != stopCauseLegacyArchive {
77 return "", 0, false
78 }
79 return g.goal, g.continuationEpoch, true
80 }
81
82 func (c *Controller) replaceLegacyRestore(legacy legacyGoalRestore) {
83 c.legacyRestoreMu.Lock()
84 c.legacyRestore = legacy
85 c.legacyRestoreMu.Unlock()
86 }
87
88 func (c *Controller) legacyRestoreSnapshot() (legacyGoalRestore, bool) {
89 c.legacyRestoreMu.Lock()
90 defer c.legacyRestoreMu.Unlock()
91 legacy := c.legacyRestore
92 return legacy, legacy.explicit || strings.TrimSpace(legacy.taskID) != ""
93 }
94
95 func (c *Controller) advanceLegacyRestoreEpoch(taskID string, from, to uint64) {
96 c.legacyRestoreMu.Lock()
97 defer c.legacyRestoreMu.Unlock()
98 if c.legacyRestore.taskID == taskID && c.legacyRestore.epoch == from {
99 c.legacyRestore.epoch = to
100 }
101 }
102
103 func (c *Controller) clearLegacyRestore(taskID string, epoch uint64) {
104 c.legacyRestoreMu.Lock()
105 defer c.legacyRestoreMu.Unlock()
106 if c.legacyRestore.taskID == taskID && c.legacyRestore.epoch == epoch {
107 c.legacyRestore = legacyGoalRestore{}
108 }
109 }
110
111 // fillGoalTextIfEmpty installs archive-recovered goal text without resetting counters.
112 func (g *goalMachine) fillGoalTextIfEmpty(expectedEpoch uint64, goal string) (uint64, bool) {
113 goal = strings.TrimSpace(goal)
114 if goal == "" {
115 return 0, false
116 }
117 g.mu.Lock()
118 defer g.mu.Unlock()
119 if g.continuationEpoch != expectedEpoch || strings.TrimSpace(g.goal) != "" {
120 return 0, false
121 }
122 g.goal = goal
123 if g.status == "" || g.stopCause == stopCauseLegacyArchive {
124 g.status = GoalStatusRunning
125 }
126 if g.stopCause == stopCauseLegacyArchive {
127 g.stopCause, g.block = "", ""
128 }
129 g.budgetClass = budgetClassResearch
130 g.turnsLimit = unlimitedGoalTurns
131 g.noProgressLimit = 0
132 if g.tokenBudget > 0 && g.tokensLimit <= g.tokensUsed {
133 g.tokensLimit = g.tokensUsed + g.tokenBudget
134 }
135 if g.scopeID == "" {
136 g.scopeID = newGoalScopeID()
137 g.deliveryCheckpoint = evidence.DeliveryCheckpoint{ScopeID: g.scopeID}
138 }
139 g.continuationEpoch++
140 return g.continuationEpoch, true
141 }
142
143 // resumeLegacyArchive applies an archive recovery only while the same blocked
144 // Goal lifecycle is still current. Archive reads happen off-lock, so the epoch
145 // check prevents a stale recovery from replacing a concurrently installed Goal.
146 func (g *goalMachine) resumeLegacyArchive(expectedEpoch uint64, goal string) (uint64, bool) {
147 goal = strings.TrimSpace(goal)
148 if goal == "" {
149 return 0, false
150 }
151 g.mu.Lock()
152 defer g.mu.Unlock()
153 if g.continuationEpoch != expectedEpoch || g.status != GoalStatusBlocked || g.stopCause != stopCauseLegacyArchive {
154 return 0, false
155 }
156 g.goal = goal
157 g.status = GoalStatusRunning
158 g.disarmed = false
159 g.stopCause, g.block = "", ""
160 g.budgetClass = budgetClassResearch
161 g.turnsLimit = unlimitedGoalTurns
162 g.noProgressLimit = 0
163 if g.tokenBudget > 0 && g.tokensLimit <= g.tokensUsed {
164 g.tokensLimit = g.tokensUsed + g.tokenBudget
165 }
166 if g.scopeID == "" {
167 g.scopeID = newGoalScopeID()
168 g.deliveryCheckpoint = evidence.DeliveryCheckpoint{ScopeID: g.scopeID}
169 }
170 g.continuationEpoch++
171 return g.continuationEpoch, true
172 }
173
173 lines GO