返回 DeepSeek-Reasonix
recovery.go
根目录 / internal / session / recovery.go
1 package session
2
3 import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "sort"
8
9 "reasonix/internal/event"
10 "reasonix/internal/provider"
11 )
12
13 // RecoverInterrupted closes persisted runtime authority that cannot survive a
14 // process restart. It never reruns a tool or restores an approval. Callers must
15 // hold the exclusive write handle returned by Open.
16 //
17 // This is a Session operation because it derives a closure batch from the
18 // projection; the physical handle only writes the resulting commit.
19 func (s *Session) RecoverInterrupted(ctx context.Context) (Commit, bool, error) {
20 if s == nil {
21 return Commit{}, false, fmt.Errorf("session: nil session")
22 }
23 if s.readOnly {
24 return Commit{}, false, ErrReadOnly
25 }
26 // Restart recovery needs only live authority, never historical message
27 // bodies or the provider workset. Using the full compatibility Snapshot here
28 // would replay the entire durable transcript on every cold open.
29 snapshot := s.StateSnapshot()
30 turnID := snapshot.Projection.TurnID
31 if turnID == "" {
32 return Commit{}, false, nil
33 }
34 events := ClosureEvents(snapshot.Projection, "unavailable", "previous runtime exited before recording a result")
35 terminal, _ := json.Marshal(map[string]any{"status": event.TurnInterrupted})
36 events = append(events, Event{Kind: "turn/end", Payload: terminal})
37 commit, err := s.Append(ctx, Batch{OperationID: "turn-finalize:" + turnID, TurnID: turnID, Events: events})
38 if err != nil {
39 return Commit{}, false, err
40 }
41 if _, err := s.Flush(ctx); err != nil {
42 return Commit{}, false, err
43 }
44 return commit, true, nil
45 }
46
47 // ClosureEvents derives deterministic terminal facts without executing tools
48 // or changing the provider workset. Live termination and restart share it.
49 func ClosureEvents(projection Projection, interactionState, detail string) []Event {
50 events := make([]Event, 0, len(projection.ActiveTools)+len(projection.Interactions)+len(projection.ActiveSteps))
51 toolIDs := make([]string, 0, len(projection.ActiveTools))
52 for id := range projection.ActiveTools {
53 toolIDs = append(toolIDs, id)
54 }
55 sort.Strings(toolIDs)
56 for _, id := range toolIDs {
57 state := provider.ToolRunNotStarted
58 legacyState := "not_started"
59 if projection.StartedTools[id] {
60 state = provider.ToolRunUnknown
61 legacyState = "result_unknown"
62 }
63 payload, _ := json.Marshal(map[string]any{"id": id, "name": projection.ActiveTools[id], "state": legacyState, "runState": state, "error": detail})
64 events = append(events, Event{Kind: "tool/result", Payload: payload})
65 }
66 requestIDs := make([]string, 0, len(projection.Interactions))
67 for id := range projection.Interactions {
68 requestIDs = append(requestIDs, id)
69 }
70 sort.Strings(requestIDs)
71 for _, id := range requestIDs {
72 payload, _ := json.Marshal(map[string]any{"id": id, "state": interactionState})
73 events = append(events, Event{Kind: "interaction/resolved", Payload: payload})
74 }
75 stepIDs := make([]string, 0, len(projection.ActiveSteps))
76 for id := range projection.ActiveSteps {
77 stepIDs = append(stepIDs, id)
78 }
79 sort.Strings(stepIDs)
80 for _, id := range stepIDs {
81 payload, _ := json.Marshal(map[string]any{"id": id})
82 events = append(events, Event{Kind: "step/end", Payload: payload})
83 }
84 return events
85 }
86
86 lines GO