返回 DeepSeek-Reasonix
goal_diagnostics.go
根目录 / internal / control / goal_diagnostics.go
1 package control
2
3 import (
4 "bytes"
5 "context"
6 "encoding/json"
7 "errors"
8 "fmt"
9 "io"
10 "maps"
11 "runtime/debug"
12 "strings"
13 "time"
14
15 goaldomain "reasonix/internal/goal"
16 "reasonix/internal/secrets"
17 "reasonix/internal/session"
18 )
19
20 // GoalDiagnosticMetadata is supplied by the host so a user-exported artifact
21 // identifies the exact build and negotiated feature surface that produced it.
22 type GoalDiagnosticMetadata struct {
23 ApplicationVersion string `json:"applicationVersion,omitempty"`
24 BuildCommit string `json:"buildCommit,omitempty"`
25 ProtocolVersion int `json:"protocolVersion,omitempty"`
26 Capabilities []string `json:"capabilities"`
27 }
28
29 type goalDiagnosticTransition struct {
30 Sequence uint64 `json:"sequence"`
31 OperationID string `json:"operationId"`
32 GoalID string `json:"goalId,omitempty"`
33 Revision uint64 `json:"revision,omitempty"`
34 Phase goaldomain.Phase `json:"phase,omitempty"`
35 Activation goaldomain.Activation `json:"activation"`
36 RoundsStarted uint64 `json:"roundsStarted,omitempty"`
37 Inferred bool `json:"inferred"`
38 }
39
40 // ExportGoalDiagnostics is the compatibility in-memory form. Production hosts
41 // use WriteGoalDiagnostics so diagnostic size is not a memory or RPC limit.
42 func (c *Controller) ExportGoalDiagnostics(ctx context.Context, metadata GoalDiagnosticMetadata) ([]byte, error) {
43 var output bytes.Buffer
44 if err := c.WriteGoalDiagnostics(ctx, &output, metadata); err != nil {
45 return nil, err
46 }
47 return output.Bytes(), nil
48 }
49
50 // WriteGoalDiagnostics streams the authoritative accepted event prefix after
51 // attempting a Flush checkpoint. A failed Flush is exported as evidence, and
52 // credential-like material is redacted one commit at a time.
53 func (c *Controller) WriteGoalDiagnostics(ctx context.Context, dst io.Writer, metadata GoalDiagnosticMetadata) error {
54 return c.WriteSessionDiagnostics(ctx, dst, metadata, nil)
55 }
56
57 // WriteSessionDiagnostics preserves the goal schema while adding host observations.
58 func (c *Controller) WriteSessionDiagnostics(ctx context.Context, dst io.Writer, metadata GoalDiagnosticMetadata, extra map[string]any) error {
59 if c == nil {
60 return session.ErrSessionNotRunning
61 }
62 _, runtime, exclusive := c.v3Binding()
63 if !exclusive || runtime == nil {
64 return errors.New("goal diagnostics require a canonical session")
65 }
66 cut := runtime.Session().EventSequence()
67 var exportSnapshot session.ExportSnapshot
68 if value, ok := extra["exportSnapshot"]; ok {
69 raw, err := json.Marshal(value)
70 if err != nil {
71 return err
72 }
73 if err = json.Unmarshal(raw, &exportSnapshot); err != nil {
74 return err
75 }
76 if exportSnapshot.Ref != runtime.Ref() || exportSnapshot.SnapshotSequence > cut {
77 return errors.New("diagnostic export source changed")
78 }
79 cut = exportSnapshot.SnapshotSequence
80 }
81 _, flushErr := runtime.Session().FlushThrough(ctx, cut)
82 if metadata.Capabilities == nil {
83 metadata.Capabilities = []string{}
84 }
85 fillGoalDiagnosticBuildMetadata(&metadata)
86 state := runtime.StateSnapshot()
87 state.Session.PersistenceError = secrets.RedactCredentials(state.Session.PersistenceError)
88 observation := c.RuntimeStateSnapshot()
89 observation.PersistenceErr = secrets.RedactCredentials(observation.PersistenceErr)
90 unavailable := []string{
91 "activation transitions are inferred from recorded Goal events; process-local activation history before export is unavailable",
92 }
93 if flushErr != nil {
94 unavailable = append(unavailable, "durability checkpoint failed: "+secrets.RedactError(flushErr))
95 }
96 if _, err := io.WriteString(dst, "{\n"); err != nil {
97 return err
98 }
99 fields := []struct {
100 name string
101 value any
102 }{
103 {"schemaVersion", 1},
104 {"exportedAt", time.Now().UTC()},
105 {"metadata", metadata},
106 {"runtime", state},
107 {"observation", observation},
108 {"submissionDiagnostics", map[string]uint64{"reused": c.submissions.reused.Load(), "conflicts": c.submissions.conflicts.Load(), "unknown": c.submissions.unknown.Load()}},
109 {"shellDiagnostics", c.persistentShell.Diagnostics()},
110 {"lifecycleDiagnostics", c.lifecycleDiagnosticSnapshot()},
111 {"acceptedThrough", cut},
112 {"runtimeObservedAt", time.Now().UTC()},
113 {"runtimeObservedThrough", state.Session.EventSequence},
114 {"durableThrough", state.Session.DurableSequence},
115 {"persistenceStatus", state.Session.PersistenceStatus},
116 {"persistenceError", state.Session.PersistenceError},
117 }
118 if exportSnapshot.Ref.SessionID != "" {
119 exportSnapshot.DurableThrough = state.Session.DurableSequence
120 extra = cloneDiagnosticExtras(extra)
121 extra["exportSnapshot"] = exportSnapshot
122 }
123 for name, value := range extra {
124 encoded, err := json.Marshal(value)
125 if err != nil {
126 return err
127 }
128 redacted := json.RawMessage(secrets.Redact(string(encoded)))
129 if !json.Valid(redacted) {
130 return errors.New("invalid redacted diagnostic field")
131 }
132 fields = append(fields, struct {
133 name string
134 value any
135 }{name, redacted})
136 }
137 for _, field := range fields {
138 if err := writeGoalDiagnosticField(dst, field.name, field.value, true); err != nil {
139 return err
140 }
141 }
142 if _, err := io.WriteString(dst, " \"commits\": ["); err != nil {
143 return err
144 }
145 return writeSessionDiagnosticCommits(ctx, dst, runtime.Session(), cut, unavailable)
146 }
147
148 func writeSessionDiagnosticCommits(ctx context.Context, dst io.Writer, store *session.Session, cut uint64, unavailable []string) error {
149 first := true
150 var destinationError error
151 changes := []goalDiagnosticTransition{}
152 activation := goaldomain.ActivationDisarmed
153 err := visitAcceptedGoalDiagnosticCommits(ctx, store, cut, func(commit session.Commit) error {
154 encoded, err := json.MarshalIndent(commit, " ", " ")
155 if err != nil {
156 return err
157 }
158 encoded = []byte(secrets.Redact(string(encoded)))
159 if !json.Valid(encoded) {
160 return errors.New("redacted goal diagnostic commit is not valid JSON")
161 }
162 separator := "\n "
163 if !first {
164 separator = ",\n "
165 }
166 if _, err := io.WriteString(dst, separator); err != nil {
167 destinationError = err
168 return err
169 }
170 if _, err := dst.Write(encoded); err != nil {
171 destinationError = err
172 return err
173 }
174 first = false
175 changes = append(changes, goalActivationChangesForCommit(commit, &activation)...)
176 return nil
177 })
178 if destinationError != nil {
179 return destinationError
180 }
181 if ctx.Err() != nil {
182 return ctx.Err()
183 }
184 if err != nil {
185 unavailable = append(unavailable, "accepted event traversal failed: "+secrets.RedactError(err))
186 }
187 if !first {
188 if _, err := io.WriteString(dst, "\n "); err != nil {
189 return err
190 }
191 }
192 if _, err := io.WriteString(dst, "],\n"); err != nil {
193 return err
194 }
195 if err := writeGoalDiagnosticField(dst, "activationChanges", changes, true); err != nil {
196 return err
197 }
198 if err := writeGoalDiagnosticField(dst, "unavailable", unavailable, false); err != nil {
199 return err
200 }
201 _, err = io.WriteString(dst, "}\n")
202 return err
203 }
204
205 func visitAcceptedGoalDiagnosticCommits(ctx context.Context, store *session.Session, through uint64, visit func(session.Commit) error) error {
206 offset := uint64(0)
207 for {
208 page, err := store.AcceptedPage(ctx, offset, 1000)
209 if err != nil {
210 return err
211 }
212 for _, commit := range page.Commits {
213 if commit.LastSequence() > through {
214 return nil
215 }
216 if err := visit(commit); err != nil {
217 return err
218 }
219 }
220 if !page.Truncated {
221 return nil
222 }
223 if page.Next <= offset {
224 return errors.New("goal diagnostics accepted-page cursor did not advance")
225 }
226 offset = page.Next
227 }
228 }
229
230 func writeGoalDiagnosticField(dst io.Writer, name string, value any, comma bool) error {
231 encoded, err := json.MarshalIndent(value, " ", " ")
232 if err != nil {
233 return err
234 }
235 if _, err := fmt.Fprintf(dst, " %q: ", name); err != nil {
236 return err
237 }
238 if _, err := dst.Write(encoded); err != nil {
239 return err
240 }
241 if comma {
242 _, err = io.WriteString(dst, ",")
243 if err != nil {
244 return err
245 }
246 }
247 _, err = io.WriteString(dst, "\n")
248 return err
249 }
250
251 func fillGoalDiagnosticBuildMetadata(metadata *GoalDiagnosticMetadata) {
252 if metadata == nil {
253 return
254 }
255 info, ok := debug.ReadBuildInfo()
256 if !ok {
257 return
258 }
259 if metadata.ApplicationVersion == "" && info.Main.Version != "" && info.Main.Version != "(devel)" {
260 metadata.ApplicationVersion = info.Main.Version
261 }
262 if metadata.BuildCommit != "" {
263 return
264 }
265 for _, setting := range info.Settings {
266 if setting.Key == "vcs.revision" {
267 metadata.BuildCommit = setting.Value
268 return
269 }
270 }
271 }
272
273 func goalActivationChangesForCommit(commit session.Commit, activation *goaldomain.Activation) []goalDiagnosticTransition {
274 changes := []goalDiagnosticTransition{}
275 hasTurnStart := false
276 for _, item := range commit.Events {
277 hasTurnStart = hasTurnStart || item.Kind == "turn/start"
278 }
279 for _, item := range commit.Events {
280 if item.Kind != "goal/state" {
281 continue
282 }
283 var document struct {
284 Current *goaldomain.Snapshot `json:"current"`
285 }
286 if json.Unmarshal(item.Payload, &document) != nil {
287 continue
288 }
289 transition := goalDiagnosticTransition{Sequence: item.Sequence, OperationID: commit.OperationID, Activation: goaldomain.ActivationDisarmed, Inferred: true}
290 if document.Current != nil {
291 transition.GoalID = document.Current.ID
292 transition.Revision = document.Current.Revision
293 transition.Phase = document.Current.Phase
294 transition.RoundsStarted = document.Current.RoundsStarted
295 if document.Current.Phase == goaldomain.PhaseActive {
296 op := strings.ToLower(commit.OperationID)
297 if hasTurnStart || strings.Contains(op, ":create") || strings.Contains(op, ":resume") || strings.Contains(op, "goal-control:set") {
298 *activation = goaldomain.ActivationArmed
299 }
300 } else {
301 *activation = goaldomain.ActivationDisarmed
302 }
303 transition.Activation = *activation
304 } else {
305 *activation = goaldomain.ActivationDisarmed
306 }
307 changes = append(changes, transition)
308 }
309 return changes
310 }
311
312 func cloneDiagnosticExtras(input map[string]any) map[string]any {
313 out := make(map[string]any, len(input))
314 maps.Copy(out, input)
315 return out
316 }
317
317 lines GO