返回 DeepSeek-Reasonix
session_composer_state.go
根目录 / desktop / session_composer_state.go
1 package main
2
3 import (
4 "crypto/sha256"
5 "encoding/json"
6 "errors"
7 "fmt"
8 "sort"
9 "strings"
10
11 "reasonix/desktop/internal/sessionui"
12 "reasonix/desktop/internal/workspacestate"
13 "reasonix/internal/session"
14 )
15
16 type SessionComposerState struct {
17 Ref session.SessionRef `json:"ref"`
18 Revision string `json:"revision"`
19 ContentJSON string `json:"contentJson"`
20 ContentVersion int `json:"contentVersion"`
21 Baseline string `json:"baseline"`
22 SubmissionID string `json:"submissionId,omitempty"`
23 SubmissionPhase string `json:"submissionPhase,omitempty"`
24 SubmissionRequest string `json:"submissionRequest,omitempty"`
25 SubmissionFingerprint string `json:"submissionFingerprint,omitempty"`
26 SubmissionRevision string `json:"submissionRevision,omitempty"`
27 HistoryChanged bool `json:"historyChanged"`
28 Conflict bool `json:"conflict"`
29 }
30
31 type SessionComposerSaveRequest struct {
32 Ref session.SessionRef `json:"ref"`
33 ExpectedRevision string `json:"expectedRevision"`
34 ContentJSON string `json:"contentJson"`
35 ContentVersion int `json:"contentVersion"`
36 AcknowledgeHistory bool `json:"acknowledgeHistory,omitempty"`
37 }
38
39 func composerRecordKey(ref session.SessionRef) string { return ref.HostID + ":" + ref.SessionID }
40
41 func (a *App) readSessionComposer(ref session.SessionRef) (SessionComposerState, error) {
42 if err := validateLocalSessionRef(ref); err != nil {
43 return SessionComposerState{}, err
44 }
45 r, err := a.sessionUIStore().Get(a.bootContext(), "composer", composerRecordKey(ref))
46 view := SessionComposerState{Ref: ref, Revision: "0", ContentJSON: "{}", ContentVersion: 1}
47 if err != nil {
48 return view, err
49 }
50 if r.Revision != "0" {
51 if err := json.Unmarshal(r.Payload, &view); err != nil {
52 return view, err
53 }
54 if view.ContentVersion != 1 {
55 return view, errors.New("unsupported composer content version")
56 }
57 }
58 view.Revision = r.Revision
59 return view, nil
60 }
61
62 // Follow user history as well as receipts: imported and older submit paths can
63 // advance history without a SubmissionID. Model settings and streaming assistant
64 // output must not invalidate input while the user composes the next turn.
65 func (a *App) composerBaseline(ref session.SessionRef) (string, error) {
66 snapshot, err := a.desktopSessionService("").Query().Snapshot(a.bootContext(), ref)
67 if err != nil {
68 return "", err
69 }
70 payload, err := json.Marshal(snapshot.Projection.Submissions)
71 if err != nil {
72 return "", err
73 }
74 var receipts map[string]json.RawMessage
75 if err := json.Unmarshal(payload, &receipts); err != nil {
76 return "", err
77 }
78 keys := make([]string, 0, len(receipts))
79 for key := range receipts {
80 keys = append(keys, key)
81 }
82 sort.Strings(keys)
83 users := []json.RawMessage{}
84 for _, message := range snapshot.Projection.Messages {
85 if message.Role == "user" {
86 encoded, err := json.Marshal(message)
87 if err != nil {
88 return "", err
89 }
90 users = append(users, encoded)
91 }
92 }
93 encoded, _ := json.Marshal(struct {
94 Receipts []string
95 Users []json.RawMessage
96 }{keys, users})
97 return fmt.Sprintf("%x", sha256.Sum256(encoded)), nil
98 }
99
100 func (a *App) GetSessionComposerState(ref session.SessionRef) (result SessionComposerState, err error) {
101 defer func() { err = sessionUIError(err, composerRecordKey(ref), "") }()
102 defer a.lockRuntimeMutation("read composer")()
103 return a.getSessionComposerState(ref)
104 }
105
106 func (a *App) getSessionComposerState(ref session.SessionRef) (SessionComposerState, error) {
107 view, err := a.readSessionComposer(ref)
108 if err != nil {
109 return view, err
110 }
111 baseline, err := a.composerBaseline(ref)
112 if err != nil {
113 return view, err
114 }
115 if view.SubmissionID != "" {
116 snapshot, snapshotErr := a.desktopSessionService("").Query().Snapshot(a.bootContext(), ref)
117 if snapshotErr != nil {
118 return view, snapshotErr
119 }
120 _, accepted := snapshot.Projection.Submissions.Lookup(ref.SessionID, view.SubmissionID)
121 if !accepted {
122 accepted, err = a.composerGuidanceAccepted(view)
123 if err != nil {
124 return view, err
125 }
126 }
127 if accepted && snapshot.DurableSequence >= snapshot.EventSequence {
128 submission := view
129 submission.SubmissionPhase = "accepted"
130 view.ContentJSON, view.SubmissionID, view.SubmissionPhase = "{}", "", ""
131 view.SubmissionRequest, view.SubmissionFingerprint = "", ""
132 view.Baseline = baseline
133 return a.saveComposerSettlement(view, submission)
134 }
135 view.SubmissionPhase = "unknown"
136 }
137 view.HistoryChanged = view.Revision != "0" && view.Baseline != "" && view.Baseline != baseline && draftHasContent(view.ContentJSON)
138 if view.Revision == "0" {
139 view.Baseline = baseline
140 }
141 return view, nil
142 }
143
144 func (a *App) saveComposerView(view SessionComposerState) (SessionComposerState, error) {
145 return a.saveComposerSettlement(view, view)
146 }
147
148 func (a *App) saveComposerSettlement(view, submission SessionComposerState) (SessionComposerState, error) {
149 payload, err := json.Marshal(view)
150 if err != nil {
151 return view, err
152 }
153 companions := []sessionui.Record{}
154 if submission.SubmissionID != "" {
155 receipt, marshalErr := json.Marshal(submission)
156 if marshalErr != nil {
157 return view, marshalErr
158 }
159 companions = append(companions, sessionui.Record{Key: composerRecordKey(view.Ref) + "/" + submission.SubmissionID, Payload: receipt})
160 }
161 r, err := a.sessionUIStore().Save(a.bootContext(), "composer", composerRecordKey(view.Ref), view.Revision, payload, companions...)
162 if errors.Is(err, sessionui.ErrConflict) {
163 if decodeErr := json.Unmarshal(r.Payload, &view); decodeErr != nil {
164 return view, decodeErr
165 }
166 view.Revision, view.Conflict = r.Revision, true
167 return view, nil
168 }
169 if err == nil {
170 view.Revision = r.Revision
171 }
172 return view, err
173 }
174
175 func (a *App) SaveSessionComposerState(req SessionComposerSaveRequest) (result SessionComposerState, err error) {
176 defer func() { err = sessionUIError(err, composerRecordKey(req.Ref), "") }()
177 defer a.lockRuntimeMutation("save composer")()
178 if req.ContentVersion != 1 || !json.Valid([]byte(req.ContentJSON)) {
179 return SessionComposerState{}, errors.New("invalid composer content")
180 }
181 view, err := a.readSessionComposer(req.Ref)
182 if err != nil {
183 return view, err
184 }
185 state, err := a.workspaceRegistry().Load(a.bootContext())
186 if err != nil {
187 return view, err
188 }
189 if state.SessionStates[req.Ref.SessionID].Lifecycle == workspacestate.Deleted {
190 return view, session.ErrSessionNotFound
191 }
192 if view.SubmissionID != "" {
193 return view, errors.New("check the pending submission before editing")
194 }
195 baseline, err := a.composerBaseline(req.Ref)
196 if err != nil {
197 return view, err
198 }
199 if view.Revision != "0" && view.Baseline != baseline && draftHasContent(view.ContentJSON) && !req.AcknowledgeHistory {
200 view.HistoryChanged = true
201 return view, nil
202 }
203 view.Revision, view.ContentJSON, view.ContentVersion = req.ExpectedRevision, req.ContentJSON, req.ContentVersion
204 view.Baseline, view.HistoryChanged, view.Conflict = baseline, false, false
205 return a.saveComposerView(view)
206 }
207
208 // BeginSessionComposerSubmission associates an already-saved source with the
209 // ordinary identified submission. It does not admit or execute work.
210 func (a *App) BeginSessionComposerSubmission(ref session.SessionRef, revision, submissionID, requestJSON string) (result SessionComposerState, err error) {
211 defer func() { err = sessionUIError(err, composerRecordKey(ref), submissionID) }()
212 defer a.lockRuntimeMutation("begin composer submission")()
213 view, err := a.getSessionComposerState(ref)
214 if err != nil {
215 return view, err
216 }
217 if view.SubmissionID != "" {
218 if view.SubmissionID == submissionID && view.SubmissionRevision == revision && view.SubmissionRequest == requestJSON {
219 return view, nil
220 }
221 return view, errors.New("another submission owns this input")
222 }
223 if view.Revision != revision || view.HistoryChanged || view.Conflict {
224 return view, sessionui.ErrConflict
225 }
226 if strings.TrimSpace(submissionID) == "" {
227 return view, errors.New("submission identity is required")
228 }
229 if !json.Valid([]byte(requestJSON)) {
230 return view, errors.New("invalid frozen submission request")
231 }
232 previous, err := a.sessionUIStore().Get(a.bootContext(), "submission", composerRecordKey(ref)+"/"+submissionID)
233 if err != nil {
234 return view, err
235 }
236 if previous.Revision != "0" {
237 return view, errors.New("submission identity already belongs to another input version")
238 }
239 view.SubmissionRequest = requestJSON
240 view.SubmissionRevision = revision
241 view.SubmissionFingerprint = fmt.Sprintf("%x", sha256.Sum256([]byte(requestJSON)))
242 view.SubmissionID, view.SubmissionPhase = submissionID, "pending"
243 return a.saveComposerView(view)
244 }
245
246 // CompleteSessionComposerSubmission is called only after the existing send
247 // command returns its admission result. A lost response is reconciled by Get;
248 // callers never infer non-acceptance from an absent transcript row.
249 func (a *App) CompleteSessionComposerSubmission(ref session.SessionRef, submissionID, outcome string) (result SessionComposerState, err error) {
250 defer func() { err = sessionUIError(err, composerRecordKey(ref), submissionID) }()
251 defer a.lockRuntimeMutation("complete composer submission")()
252 view, err := a.readSessionComposer(ref)
253 if err != nil {
254 return view, err
255 }
256 if view.SubmissionID == "" {
257 return view, nil
258 }
259 if view.SubmissionID != submissionID {
260 return view, sessionui.ErrConflict
261 }
262 submission := view
263 submission.SubmissionPhase = outcome
264 switch outcome {
265 case "accepted":
266 view.ContentJSON, view.SubmissionID, view.SubmissionPhase = "{}", "", ""
267 view.SubmissionRequest, view.SubmissionFingerprint = "", ""
268 case "not_accepted":
269 view.SubmissionID, view.SubmissionPhase = "", ""
270 case "unknown":
271 view.SubmissionPhase = "unknown"
272 default:
273 return view, errors.New("invalid submission outcome")
274 }
275 view.Baseline, err = a.composerBaseline(ref)
276 if err != nil {
277 return view, err
278 }
279 return a.saveComposerSettlement(view, submission)
280 }
281
281 lines GO