返回 DeepSeek-Reasonix
turn_submission_app.go
根目录 / desktop / turn_submission_app.go
1 package main
2
3 import (
4 "errors"
5 "reasonix/internal/control"
6 "reasonix/internal/event"
7 "reasonix/internal/session"
8 "strings"
9 )
10
11 func (a *App) knownSubmission(tabID string, req control.SubmissionRequest) (bool, error) {
12 _, found, err := a.knownSubmissionReceipt(tabID, req)
13 return found, err
14 }
15
16 func (a *App) knownSubmissionReceipt(tabID string, req control.SubmissionRequest) (session.SubmissionReceipt, bool, error) {
17 tab, ctrl := a.tabAndCtrlByID(tabID)
18 if a.tabIsReadOnly(tab) {
19 return session.SubmissionReceipt{}, false, readOnlyChannelErr()
20 }
21 if identified, ok := ctrl.(*control.Controller); ok && req.ID != "" {
22 return identified.LookupSubmission(req)
23 }
24 return session.SubmissionReceipt{}, false, nil
25 }
26
27 // A competing retry may have waited behind the first caller's tab admission.
28 func (a *App) submissionAdmissionError(tabID string, req control.SubmissionRequest, err error) error {
29 if errors.Is(err, control.ErrTurnRunning) {
30 if found, lookupErr := a.knownSubmission(tabID, req); found || lookupErr != nil {
31 return lookupErr
32 }
33 }
34 return &submissionNotAcceptedError{cause: err}
35 }
36
37 func submitIdentified(ctrl control.SessionAPI, req control.SubmissionRequest, submit func()) error {
38 return submitIdentifiedWithSetup(ctrl, req, nil, submit)
39 }
40
41 func submitIdentifiedWithSetup(ctrl control.SessionAPI, req control.SubmissionRequest, setup func() error, submit func()) error {
42 if identified, ok := ctrl.(*control.Controller); ok && identified.ClassifySubmitRoute(req.Input) != control.SubmitManagementHandled {
43 if setup != nil {
44 _, err := identified.SubmitIdentifiedWithSetup(req, setup)
45 return inboxBridgeError(err)
46 }
47 _, err := identified.SubmitIdentified(req)
48 return inboxBridgeError(err)
49 }
50 if setup != nil {
51 if err := setup(); err != nil {
52 return err
53 }
54 }
55 submit()
56 return nil
57 }
58
59 type turnSubmissionState struct {
60 inFlight bool
61 submissionID string
62 }
63
64 type managementAdmissionError struct {
65 result control.SubmitResult
66 cause error
67 }
68
69 func (e *managementAdmissionError) Error() string { return e.cause.Error() }
70 func (e *managementAdmissionError) Unwrap() error { return e.cause }
71
72 func (t *WorkspaceTab) recordTurnStarted(now int64) int64 {
73 t.telemMu.Lock()
74 defer t.telemMu.Unlock()
75 if t.usageTelemetry.activeTurnStartedAt == 0 {
76 t.usageTelemetry.activeTurnStartedAt = now
77 }
78 return t.usageTelemetry.activeTurnStartedAt
79 }
80
81 func (t *WorkspaceTab) turnStartedAt() int64 {
82 if t == nil {
83 return 0
84 }
85 t.telemMu.Lock()
86 defer t.telemMu.Unlock()
87 return t.usageTelemetry.activeTurnStartedAt
88 }
89
90 // setBinding reroutes the sink while invalidating correlations that were
91 // created for a different frontend tab.
92 func (s *tabEventSink) setBinding(tabID string, app *App, generation ...uint64) {
93 s.mu.Lock()
94 if s.tabID != tabID {
95 s.turn.submissionID = ""
96 }
97 s.tabID = tabID
98 if len(generation) > 0 {
99 if s.sessionGeneration != generation[0] {
100 s.turn.submissionID = ""
101 }
102 s.sessionGeneration = generation[0]
103 }
104 if app != nil {
105 s.app = app
106 }
107 s.mu.Unlock()
108 }
109
110 func (s *tabEventSink) setSessionGeneration(generation uint64) {
111 if s == nil {
112 return
113 }
114 s.mu.Lock()
115 if s.sessionGeneration != generation {
116 s.turn.submissionID = ""
117 }
118 s.sessionGeneration = generation
119 s.mu.Unlock()
120 }
121
122 func (s *tabEventSink) sessionGenerationSnapshot() uint64 {
123 if s == nil {
124 return 0
125 }
126 s.mu.RLock()
127 defer s.mu.RUnlock()
128 return s.sessionGeneration
129 }
130
131 func (s *tabEventSink) setRuntimeEpoch(epoch string) {
132 if s == nil {
133 return
134 }
135 s.mu.Lock()
136 if s.runtimeEpoch != epoch {
137 s.turn.submissionID = ""
138 }
139 s.runtimeEpoch = epoch
140 s.mu.Unlock()
141 }
142
143 func (s *tabEventSink) clearContext() {
144 s.mu.Lock()
145 s.ctx = nil
146 s.turn.submissionID = ""
147 s.mu.Unlock()
148 s.runtimeEvents.Clear()
149 }
150
151 func firstSubmissionID(ids []string) string {
152 if len(ids) == 0 {
153 return ""
154 }
155 return ids[0]
156 }
157
158 func (s *tabEventSink) submissionIDSnapshot() string {
159 if s == nil {
160 return ""
161 }
162 s.mu.RLock()
163 defer s.mu.RUnlock()
164 return s.turn.submissionID
165 }
166
167 type correlatedWireEventTab struct {
168 wireEventTab
169 SubmissionID string `json:"submissionId,omitempty"`
170 }
171
172 func toWireTabWithSubmission(e event.Event, tabID, runtimeEpoch, submissionID string, turnStartedAt int64, sessionGeneration ...uint64) any {
173 wire := toWireTab(e, tabID, runtimeEpoch)
174 if len(sessionGeneration) > 0 {
175 wire.SessionGeneration = sessionGeneration[0]
176 }
177 if e.Kind == event.TurnStarted {
178 wire.TurnStartedAt = turnStartedAt
179 }
180 if submissionID == "" {
181 return wire
182 }
183 return correlatedWireEventTab{wireEventTab: wire, SubmissionID: submissionID}
184 }
185
186 // The WithID entry points correlate one optimistic desktop user item with the
187 // raw TurnDone produced by the turn that this call actually admits.
188 func (a *App) SubmitToTabWithID(tabID, input, submissionID string) error {
189 if err := validateTurnInput(input); err != nil {
190 return err
191 }
192 return a.submitToTab(tabID, input, false, submissionID)
193 }
194
195 func (a *App) SubmitDisplayToTabWithID(tabID, display, input, submissionID string) error {
196 return a.submitDisplayToTab(tabID, display, input, submissionID)
197 }
198
199 func (a *App) submitDisplayToTab(tabID, display, input, submissionID string) error {
200 if err := validateTurnInput(input); err != nil {
201 return err
202 }
203 req := control.SubmissionRequest{ID: submissionID, Input: input, Display: display}
204 if found, err := a.knownSubmission(tabID, req); found || err != nil {
205 return err
206 }
207 admission, ctrl, err := a.beginTabTurn(tabID, true, submissionID)
208 if err != nil {
209 return a.submissionAdmissionError(tabID, req, err)
210 }
211 defer admission.abort()
212 tab := admission.tab
213 if err := a.ensureTabTopicIndexedForUserTurn(tab); err != nil {
214 return err
215 }
216 if err := submitIdentified(ctrl, req, func() { ctrl.SubmitDisplay(display, input) }); err != nil {
217 return err
218 }
219 admission.finish(ctrl)
220 return nil
221 }
222
223 func (a *App) SubmitDeliveryRecoveryToTabWithID(tabID, display, input, submissionID string) error {
224 return a.submitDeliveryRecoveryToTab(tabID, display, input, submissionID)
225 }
226
227 func (a *App) submitDeliveryRecoveryToTab(tabID, display, input, submissionID string) error {
228 if err := validateTurnInput(input); err != nil {
229 return err
230 }
231 req := control.SubmissionRequest{ID: submissionID, Input: input, Display: display, Action: "delivery-recovery"}
232 if found, err := a.knownSubmission(tabID, req); found || err != nil {
233 return err
234 }
235 admission, ctrl, err := a.beginTabTurn(tabID, true, submissionID)
236 if err != nil {
237 return a.submissionAdmissionError(tabID, req, err)
238 }
239 defer admission.abort()
240 tab := admission.tab
241 if err := a.ensureTabTopicIndexedForUserTurn(tab); err != nil {
242 return err
243 }
244 if err := submitIdentified(ctrl, req, func() { ctrl.SubmitDeliveryRecovery(display, input) }); err != nil {
245 return err
246 }
247 admission.finish(ctrl)
248 return nil
249 }
250
251 func (a *App) SubmitInvocationsToTabWithID(tabID, display, input string, invocations []InvocationRequest, submissionID string) error {
252 return a.submitInvocationsToTab(tabID, display, input, invocations, submissionID)
253 }
254
255 func (a *App) submitInvocationsToTab(tabID, display, input string, invocations []InvocationRequest, submissionID string) error {
256 if err := validateInvocationTurnInput(input, invocations); err != nil {
257 return err
258 }
259 req := control.SubmissionRequest{ID: submissionID, Input: input, Display: display, Invocations: controlInvocationRequests(invocations)}
260 if found, err := a.knownSubmission(tabID, req); found || err != nil {
261 return err
262 }
263 admission, ctrl, err := a.beginTabTurn(tabID, true, submissionID)
264 if err != nil {
265 return a.submissionAdmissionError(tabID, req, err)
266 }
267 defer admission.abort()
268 tab := admission.tab
269 if err := a.ensureTabTopicIndexedForUserTurn(tab); err != nil {
270 return err
271 }
272 if err := submitIdentified(ctrl, req, func() { ctrl.SubmitInvocationDisplay(display, input, controlInvocationRequests(invocations)) }); err != nil {
273 return err
274 }
275 admission.finish(ctrl)
276 return nil
277 }
278
279 func (a *App) SubmitInitialGoalToTabWithID(
280 tabID, goal, display, input string,
281 invocations []InvocationRequest,
282 collaborationMode, toolApprovalMode, submissionID string,
283 ) ([]string, error) {
284 if err := validateInvocationTurnInput(input, invocations); err != nil {
285 return []string{}, err
286 }
287 return a.submitInitialGoalToLocalTab(
288 tabID, toolApprovalMode, goal, display, input, invocations, submissionID,
289 )
290 }
291
292 func (a *App) SubmitEditedDisplayToTabWithID(tabID, display, input, original, submissionID string) error {
293 return a.submitEditedDisplayToTab(tabID, display, input, original, submissionID)
294 }
295
296 func (a *App) submitEditedDisplayToTab(tabID, display, input, original, submissionID string) error {
297 if err := validateTurnInput(input); err != nil {
298 return err
299 }
300 req := control.SubmissionRequest{ID: submissionID, Input: input, Display: display, Original: original}
301 if found, err := a.knownSubmission(tabID, req); found || err != nil {
302 return err
303 }
304 admission, ctrl, err := a.beginTabTurn(tabID, true, submissionID)
305 if err != nil {
306 return a.submissionAdmissionError(tabID, req, err)
307 }
308 defer admission.abort()
309 tab := admission.tab
310 if err := a.ensureTabTopicIndexedForUserTurn(tab); err != nil {
311 return err
312 }
313 if err := submitIdentified(ctrl, req, func() { ctrl.SubmitEditedDisplay(display, input, original) }); err != nil {
314 return err
315 }
316 admission.finish(ctrl)
317 return nil
318 }
319
320 func (a *App) submitToTabResult(tabID, input string, fromBridge, classifyManagement bool, submissionID ...string) (control.SubmitResult, error) {
321 if receipt, found, err := a.knownSubmissionReceipt(tabID, control.SubmissionRequest{ID: firstSubmissionID(submissionID), Input: input, Display: input}); found || err != nil {
322 return control.SubmitResult{Disposition: control.SubmitTurnStarted, TurnID: receipt.TurnID}, err
323 }
324 management := control.SubmitResult{Disposition: control.SubmitManagementHandled}
325 trimmed := strings.TrimSpace(input)
326 if trimmed == "/reload" {
327 return a.submitReloadCommand(tabID, fromBridge)
328 }
329 if trimmed == "/effort" || strings.HasPrefix(trimmed, "/effort ") {
330 tab, _ := a.tabAndCtrlByID(tabID)
331 if a.tabIsReadOnly(tab) {
332 return control.SubmitResult{}, readOnlyChannelErr()
333 }
334 if tab == nil {
335 return control.SubmitResult{}, a.workspaceNotReadyErr(tab)
336 }
337 if !fromBridge && a.botBridge != nil {
338 a.botBridge.reclaimFromDesktop(tab.ID)
339 }
340 a.runEffortCommandForTab(tabID, trimmed)
341 return management, nil
342 }
343 if classifyManagement {
344 tab, ctrl := a.tabAndCtrlByID(tabID)
345 if a.tabIsReadOnly(tab) {
346 return control.SubmitResult{}, readOnlyChannelErr()
347 }
348 if err := a.workspaceRuntimeAdmissionErr(tab, ctrl); err != nil {
349 return control.SubmitResult{}, err
350 }
351 if err := a.ensureTabControllerWorkspace(tab); err != nil {
352 return control.SubmitResult{}, err
353 }
354 ctrl = a.controllerForTab(tab)
355 if ctrl == nil {
356 return control.SubmitResult{}, a.workspaceNotReadyErr(tab)
357 }
358 managementRoute := false
359 if classifier, ok := ctrl.(interface {
360 ClassifySubmitRoute(input string) control.SubmitDisposition
361 }); ok {
362 managementRoute = classifier.ClassifySubmitRoute(input) == control.SubmitManagementHandled
363 }
364 if managementRoute {
365 // Management commands still take the tab admission lock so they cannot
366 // race an active turn or a controller replacement.
367 admission, admittedCtrl, err := a.beginRuntimeTurnChecked(tabID, !fromBridge, false, func(owner control.SessionAPI) error {
368 if result, err := control.MaintenanceCommandConflict(owner, trimmed); err != nil {
369 return &managementAdmissionError{result: result, cause: err}
370 }
371 return nil
372 }, submissionID...)
373 if err != nil {
374 var conflict *managementAdmissionError
375 if errors.As(err, &conflict) {
376 return conflict.result, conflict.cause
377 }
378 return control.SubmitResult{}, err
379 }
380 defer admission.abort()
381 tab = admission.tab
382 if err := a.ensureTabTopicIndexedForUserTurn(tab); err != nil {
383 return control.SubmitResult{}, err
384 }
385 if submitter, supported := admittedCtrl.(interface {
386 SubmitDisplayWithResult(display, input string) control.SubmitResult
387 }); supported {
388 result := submitter.SubmitDisplayWithResult(input, input)
389 admission.finish(admittedCtrl)
390 return result, nil
391 }
392 admittedCtrl.SubmitDisplay(input, input)
393 admission.finish(admittedCtrl)
394 return management, nil
395 }
396 }
397 admission, ctrl, err := a.beginTabTurn(tabID, !fromBridge, submissionID...)
398 if err != nil {
399 return control.SubmitResult{Disposition: control.SubmitTurnStarted}, a.submissionAdmissionError(tabID,
400 control.SubmissionRequest{ID: firstSubmissionID(submissionID), Input: input, Display: input}, err)
401 }
402 defer admission.abort()
403 tab := admission.tab
404 if err := a.ensureTabTopicIndexedForUserTurn(tab); err != nil {
405 return control.SubmitResult{}, err
406 }
407 result := control.SubmitResult{Disposition: control.SubmitTurnStarted}
408 if identified, ok := ctrl.(*control.Controller); ok && firstSubmissionID(submissionID) != "" && identified.ClassifySubmitRoute(input) != control.SubmitManagementHandled {
409 receipt, err := identified.SubmitIdentified(control.SubmissionRequest{ID: firstSubmissionID(submissionID), Input: input, Display: input})
410 if err != nil {
411 return control.SubmitResult{}, err
412 }
413 result.TurnID = receipt.TurnID
414 } else if submitter, ok := ctrl.(interface {
415 SubmitDisplayWithResult(display, input string) control.SubmitResult
416 }); ok {
417 result = submitter.SubmitDisplayWithResult(input, input)
418 } else {
419 ctrl.SubmitDisplay(input, input)
420 }
421 admission.finish(ctrl)
422 return result, nil
423 }
424
425 func (a *App) submitReloadCommand(tabID string, fromBridge bool) (control.SubmitResult, error) {
426 management := control.SubmitResult{Disposition: control.SubmitManagementHandled}
427 tab, _ := a.tabAndCtrlByID(tabID)
428 if a.tabIsReadOnly(tab) {
429 return control.SubmitResult{}, readOnlyChannelErr()
430 }
431 if tab == nil {
432 return control.SubmitResult{}, a.workspaceNotReadyErr(tab)
433 }
434 if !fromBridge && a.botBridge != nil {
435 a.botBridge.reclaimFromDesktop(tab.ID)
436 }
437 return management, a.ReloadRuntime(tab.ID)
438 }
439
439 lines GO