返回 DeepSeek-Reasonix
goal_lifecycle_binding.go
根目录 / internal / control / goal_lifecycle_binding.go
1 package control
2
3 import (
4 "crypto/sha256"
5 "encoding/hex"
6 "encoding/json"
7 "fmt"
8 "strings"
9 "time"
10
11 goaldomain "reasonix/internal/goal"
12 "reasonix/internal/session"
13 )
14
15 type legacyGoalProjection struct {
16 Goal string `json:"goal"`
17 Status string `json:"status"`
18 TurnsUsed int `json:"turnsUsed"`
19 Block string `json:"block"`
20 }
21
22 // goalLifecycleFromProjection is the only compatibility boundary between
23 // imported legacy goal sidecars and the current versioned goal domain. It does
24 // not write during cold open and never restores todo or process activation.
25 func goalLifecycleFromProjection(raw json.RawMessage, sessionID string, createdAt time.Time) (*goaldomain.Machine, error) {
26 machine := goaldomain.NewMachine(nil, nil)
27 if len(raw) == 0 {
28 return machine, nil
29 }
30 var header struct {
31 Version *int `json:"version"`
32 }
33 if err := json.Unmarshal(raw, &header); err != nil {
34 return machine, fmt.Errorf("decode goal projection: %w", err)
35 }
36 if header.Version != nil {
37 if _, err := machine.Restore(raw); err != nil {
38 return machine, err
39 }
40 return machine, nil
41 }
42 return importLegacyGoalProjection(machine, raw, sessionID, createdAt)
43 }
44
45 func importLegacyGoalProjection(machine *goaldomain.Machine, raw json.RawMessage, sessionID string, createdAt time.Time) (*goaldomain.Machine, error) {
46 var legacy legacyGoalProjection
47 if err := json.Unmarshal(raw, &legacy); err != nil {
48 return machine, fmt.Errorf("decode legacy goal projection: %w", err)
49 }
50 legacy.Goal = strings.TrimSpace(legacy.Goal)
51 if legacy.Goal == "" {
52 return machine, nil
53 }
54 if legacy.TurnsUsed < 0 {
55 return machine, fmt.Errorf("legacy goal has negative admitted rounds")
56 }
57 var phase goaldomain.Phase
58 var blockedReason *goaldomain.BlockReason
59 switch strings.TrimSpace(legacy.Status) {
60 case GoalStatusRunning:
61 phase = goaldomain.PhaseActive
62 case GoalStatusComplete:
63 phase = goaldomain.PhaseComplete
64 case GoalStatusBlocked:
65 phase = goaldomain.PhaseBlocked
66 message := strings.TrimSpace(legacy.Block)
67 if message == "" {
68 message = "legacy goal stopped without a recorded reason"
69 }
70 blockedReason = &goaldomain.BlockReason{Code: "legacy-blocked", Message: message}
71 case "", GoalStatusStopped:
72 phase = goaldomain.PhasePaused
73 default:
74 return machine, fmt.Errorf("legacy goal has unsupported status %q", legacy.Status)
75 }
76 if createdAt.IsZero() {
77 createdAt = time.Unix(0, 0).UTC()
78 } else {
79 createdAt = createdAt.UTC()
80 }
81 hash := sha256.Sum256(append(append([]byte(sessionID), 0), raw...))
82 id := "legacy-" + hex.EncodeToString(hash[:12])
83 document := map[string]any{
84 "version": goaldomain.StateVersion,
85 "current": goaldomain.Snapshot{
86 ID: id, Revision: 1, Objective: legacy.Goal, Phase: phase,
87 MaxGoalRounds: nil, RoundsStarted: uint64(legacy.TurnsUsed),
88 BlockedReason: blockedReason, CreatedAt: createdAt, UpdatedAt: createdAt,
89 },
90 "legacyState": json.RawMessage(append([]byte(nil), raw...)),
91 }
92 encoded, err := json.Marshal(document)
93 if err != nil {
94 return machine, err
95 }
96 if _, err := machine.Restore(encoded); err != nil {
97 return machine, err
98 }
99 return machine, nil
100 }
101
102 func (c *Controller) installGoalLifecycle(runtime *session.Runtime) {
103 machine := goaldomain.NewMachine(nil, nil)
104 var loadErr error
105 if runtime != nil && runtime.Session() != nil {
106 snapshot := runtime.Session().ExecutionSnapshot()
107 createdAt := runtime.Session().Handle().Manifest().CreatedAt
108 machine, loadErr = goalLifecycleFromProjection(snapshot.Projection.GoalState, runtime.Ref().SessionID, createdAt)
109 }
110 c.goalLifecycleMu.Lock()
111 c.goalLifecycle = machine
112 c.goalLifecycleLoadErr = loadErr
113 c.goalLifecycleMu.Unlock()
114 }
115
116 func (c *Controller) goalLifecycleView() (*goaldomain.View, error) {
117 if c == nil {
118 return nil, nil
119 }
120 c.goalLifecycleMu.RLock()
121 machine, loadErr := c.goalLifecycle, c.goalLifecycleLoadErr
122 c.goalLifecycleMu.RUnlock()
123 if loadErr != nil {
124 return nil, loadErr
125 }
126 if machine == nil {
127 return nil, nil
128 }
129 return machine.Get(), nil
130 }
131
131 lines GO