返回 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 {"providerDiagnostics", c.providerDiagnosticSnapshot()},
112 {"acceptedThrough", cut},
113 {"runtimeObservedAt", time.Now().UTC()},
114 {"runtimeObservedThrough", state.Session.EventSequence},
115 {"durableThrough", state.Session.DurableSequence},
116 {"persistenceStatus", state.Session.PersistenceStatus},
117 {"persistenceError", state.Session.PersistenceError},
118 }
119 if exportSnapshot.Ref.SessionID != "" {
120 exportSnapshot.DurableThrough = state.Session.DurableSequence
121 extra = cloneDiagnosticExtras(extra)
122 extra["exportSnapshot"] = exportSnapshot
123 }
124 for name, value := range extra {
125 fields = append(fields, struct {
126 name string
127 value any
128 }{name, value})
129 }
130 for _, field := range fields {
131 if err := writeGoalDiagnosticField(dst, field.name, field.value, true); err != nil {
132 return err
133 }
134 }
135 if _, err := io.WriteString(dst, " \"commits\": ["); err != nil {
136 return err
137 }
138 return writeSessionDiagnosticCommits(ctx, dst, runtime.Session(), cut, unavailable)
139 }
140
141 func writeSessionDiagnosticCommits(ctx context.Context, dst io.Writer, store *session.Session, cut uint64, unavailable []string) error {
142 first := true
143 var destinationError error
144 changes := []goalDiagnosticTransition{}
145 activation := goaldomain.ActivationDisarmed
146 err := visitAcceptedGoalDiagnosticCommits(ctx, store, cut, func(commit session.Commit) error {
147 encoded, err := json.MarshalIndent(commit, " ", " ")
148 if err != nil {
149 return err
150 }
151 encoded, err = secrets.RedactJSON(encoded)
152 if err != nil {
153 return err
154 }
155 var formatted bytes.Buffer
156 if err := json.Indent(&formatted, encoded, " ", " "); err != nil {
157 return err
158 }
159 encoded = formatted.Bytes()
160 separator := "\n "
161 if !first {
162 separator = ",\n "
163 }
164 if _, err := io.WriteString(dst, separator); err != nil {
165 destinationError = err
166 return err
167 }
168 if _, err := dst.Write(encoded); err != nil {
169 destinationError = err
170 return err
171 }
172 first = false
173 changes = append(changes, goalActivationChangesForCommit(commit, &activation)...)
174 return nil
175 })
176 if destinationError != nil {
177 return destinationError
178 }
179 if ctx.Err() != nil {
180 return ctx.Err()
181 }
182 if err != nil {
183 unavailable = append(unavailable, "accepted event traversal failed: "+secrets.RedactError(err))
184 }
185 if !first {
186 if _, err := io.WriteString(dst, "\n "); err != nil {
187 return err
188 }
189 }
190 if _, err := io.WriteString(dst, "],\n"); err != nil {
191 return err
192 }
193 if err := writeGoalDiagnosticField(dst, "activationChanges", changes, true); err != nil {
194 return err
195 }
196 if err := writeGoalDiagnosticField(dst, "unavailable", unavailable, false); err != nil {
197 return err
198 }
199 _, err = io.WriteString(dst, "}\n")
200 return err
201 }
202
203 func visitAcceptedGoalDiagnosticCommits(ctx context.Context, store *session.Session, through uint64, visit func(session.Commit) error) error {
204 offset := uint64(0)
205 for {
206 page, err := store.AcceptedPage(ctx, offset, 1000)
207 if err != nil {
208 return err
209 }
210 for _, commit := range page.Commits {
211 if commit.LastSequence() > through {
212 return nil
213 }
214 if err := visit(commit); err != nil {
215 return err
216 }
217 }
218 if !page.Truncated {
219 return nil
220 }
221 if page.Next <= offset {
222 return errors.New("goal diagnostics accepted-page cursor did not advance")
223 }
224 offset = page.Next
225 }
226 }
227
228 func writeGoalDiagnosticField(dst io.Writer, name string, value any, comma bool) error {
229 encoded, err := json.Marshal(value)
230 if err != nil {
231 return err
232 }
233 encoded, err = secrets.RedactJSON(encoded)
234 if err != nil {
235 return err
236 }
237 var formatted bytes.Buffer
238 if err := json.Indent(&formatted, encoded, " ", " "); err != nil {
239 return err
240 }
241 encoded = formatted.Bytes()
242 if _, err := fmt.Fprintf(dst, " %q: ", name); err != nil {
243 return err
244 }
245 if _, err := dst.Write(encoded); err != nil {
246 return err
247 }
248 if comma {
249 _, err = io.WriteString(dst, ",")
250 if err != nil {
251 return err
252 }
253 }
254 _, err = io.WriteString(dst, "\n")
255 return err
256 }
257
258 func fillGoalDiagnosticBuildMetadata(metadata *GoalDiagnosticMetadata) {
259 if metadata == nil {
260 return
261 }
262 info, ok := debug.ReadBuildInfo()
263 if !ok {
264 return
265 }
266 if metadata.ApplicationVersion == "" && info.Main.Version != "" && info.Main.Version != "(devel)" {
267 metadata.ApplicationVersion = info.Main.Version
268 }
269 if metadata.BuildCommit != "" {
270 return
271 }
272 for _, setting := range info.Settings {
273 if setting.Key == "vcs.revision" {
274 metadata.BuildCommit = setting.Value
275 return
276 }
277 }
278 }
279
280 func goalActivationChangesForCommit(commit session.Commit, activation *goaldomain.Activation) []goalDiagnosticTransition {
281 changes := []goalDiagnosticTransition{}
282 hasTurnStart := false
283 for _, item := range commit.Events {
284 hasTurnStart = hasTurnStart || item.Kind == "turn/start"
285 }
286 for _, item := range commit.Events {
287 if item.Kind != "goal/state" {
288 continue
289 }
290 var document struct {
291 Current *goaldomain.Snapshot `json:"current"`
292 }
293 if json.Unmarshal(item.Payload, &document) != nil {
294 continue
295 }
296 transition := goalDiagnosticTransition{Sequence: item.Sequence, OperationID: commit.OperationID, Activation: goaldomain.ActivationDisarmed, Inferred: true}
297 if document.Current != nil {
298 transition.GoalID = document.Current.ID
299 transition.Revision = document.Current.Revision
300 transition.Phase = document.Current.Phase
301 transition.RoundsStarted = document.Current.RoundsStarted
302 if document.Current.Phase == goaldomain.PhaseActive {
303 op := strings.ToLower(commit.OperationID)
304 if hasTurnStart || strings.Contains(op, ":create") || strings.Contains(op, ":resume") || strings.Contains(op, "goal-control:set") {
305 *activation = goaldomain.ActivationArmed
306 }
307 } else {
308 *activation = goaldomain.ActivationDisarmed
309 }
310 transition.Activation = *activation
311 } else {
312 *activation = goaldomain.ActivationDisarmed
313 }
314 changes = append(changes, transition)
315 }
316 return changes
317 }
318
319 func cloneDiagnosticExtras(input map[string]any) map[string]any {
320 out := make(map[string]any, len(input))
321 maps.Copy(out, input)
322 return out
323 }
324
324 lines GO