| 1 | package control |
| 2 | |
| 3 | import "encoding/json" |
| 4 | |
| 5 | var goalStateKnownFields = map[string]struct{}{ |
| 6 | "goal": {}, "status": {}, "researchMode": {}, "autoResearchTaskID": {}, |
| 7 | "scopeID": {}, "deliveryCheckpoint": {}, "turns": {}, "blocks": {}, |
| 8 | "block": {}, "strict": {}, "todos": {}, "budgetClass": {}, |
| 9 | "turnsUsed": {}, "turnsLimit": {}, "tokensUsed": {}, "requestsUsed": {}, |
| 10 | "workDurationMs": {}, |
| 11 | "tokensLimit": {}, "noProgressTurns": {}, "noProgressLimit": {}, |
| 12 | "lastContinuationReason": {}, "lastEvaluatorReason": {}, "stopCause": {}, |
| 13 | "budgetExtensions": {}, "progressEvidence": {}, |
| 14 | } |
| 15 | |
| 16 | func goalStateUnknownFields(raw []byte) map[string]json.RawMessage { |
| 17 | var fields map[string]json.RawMessage |
| 18 | if json.Unmarshal(raw, &fields) != nil { |
| 19 | return nil |
| 20 | } |
| 21 | for key := range goalStateKnownFields { |
| 22 | delete(fields, key) |
| 23 | } |
| 24 | return cloneGoalStateExtra(fields) |
| 25 | } |
| 26 | |
| 27 | func marshalGoalState(state goalState, extra map[string]json.RawMessage) ([]byte, error) { |
| 28 | known, err := json.Marshal(state) |
| 29 | if err != nil || len(extra) == 0 { |
| 30 | return known, err |
| 31 | } |
| 32 | var merged map[string]json.RawMessage |
| 33 | if err := json.Unmarshal(known, &merged); err != nil { |
| 34 | return nil, err |
| 35 | } |
| 36 | for key, value := range extra { |
| 37 | if _, current := goalStateKnownFields[key]; current { |
| 38 | continue |
| 39 | } |
| 40 | merged[key] = append(json.RawMessage(nil), value...) |
| 41 | } |
| 42 | return json.Marshal(merged) |
| 43 | } |
| 44 | |
| 45 | func cloneGoalStateExtra(in map[string]json.RawMessage) map[string]json.RawMessage { |
| 46 | if len(in) == 0 { |
| 47 | return nil |
| 48 | } |
| 49 | out := make(map[string]json.RawMessage, len(in)) |
| 50 | for key, value := range in { |
| 51 | out[key] = append(json.RawMessage(nil), value...) |
| 52 | } |
| 53 | return out |
| 54 | } |
| 55 | |
| 56 | func (g *goalMachine) grantSpendSliceLocked(fresh bool) { |
| 57 | switch { |
| 58 | case g.tokenBudget <= 0: |
| 59 | g.tokensLimit = 0 |
| 60 | case fresh || g.tokensLimit <= g.tokensUsed: |
| 61 | g.tokensLimit = g.tokensUsed + g.tokenBudget |
| 62 | } |
| 63 | } |
| 64 | |
| 65 | // migrateRemovedGoalPause clears pauses produced by gates no longer enforced. |
| 66 | func (g *goalMachine) migrateRemovedGoalPause() bool { |
| 67 | if g.status != GoalStatusBlocked { |
| 68 | return false |
| 69 | } |
| 70 | switch g.stopCause { |
| 71 | case stopCauseBudgetTurns, stopCauseBudgetTokens, stopCauseGoalRunBudget, stopCauseGoalStuck, stopCauseNoProgress, stopCauseEvaluator, "final_readiness", "quality_readiness": |
| 72 | default: |
| 73 | return false |
| 74 | } |
| 75 | g.status = GoalStatusRunning |
| 76 | g.stopCause = "" |
| 77 | g.block = "" |
| 78 | return true |
| 79 | } |
| 80 | |
| 81 | // normalizeContinuousState runs under g.mu while loading an active sidecar. |
| 82 | func (g *goalMachine) normalizeContinuousState(legacyMode GoalResearchMode, legacyTaskID string) bool { |
| 83 | if g.goal == "" { |
| 84 | return false |
| 85 | } |
| 86 | migrated := false |
| 87 | if g.budgetClass == "" { |
| 88 | g.budgetClass = budgetClassForLegacyMode(g.goal, legacyMode) |
| 89 | } |
| 90 | if legacyTaskID != "" { |
| 91 | g.budgetClass = budgetClassResearch |
| 92 | } |
| 93 | if g.turnsLimit != unlimitedGoalTurns { |
| 94 | g.turnsLimit, migrated = unlimitedGoalTurns, true |
| 95 | } |
| 96 | if g.noProgressLimit != 0 { |
| 97 | g.noProgressLimit, migrated = 0, true |
| 98 | } |
| 99 | if g.budgetExtensions != 0 { |
| 100 | g.budgetExtensions, migrated = 0, true |
| 101 | } |
| 102 | legacyNumericPause := false |
| 103 | switch g.stopCause { |
| 104 | case stopCauseBudgetTurns, stopCauseBudgetTokens, stopCauseGoalRunBudget, stopCauseGoalStuck, stopCauseNoProgress: |
| 105 | legacyNumericPause = true |
| 106 | } |
| 107 | if g.tokenBudget <= 0 { |
| 108 | if g.tokensLimit != 0 { |
| 109 | g.tokensLimit, migrated = 0, true |
| 110 | } |
| 111 | } else if legacyNumericPause || g.tokensLimit <= 0 { |
| 112 | g.tokensLimit, migrated = g.tokensUsed+g.tokenBudget, true |
| 113 | } |
| 114 | return g.migrateRemovedGoalPause() || migrated |
| 115 | } |
| 116 |