返回 DeepSeek-Reasonix
turn_admission.go
根目录 / desktop / turn_admission.go
1 package main
2
3 import (
4 "fmt"
5
6 "reasonix/internal/control"
7 )
8
9 type imageCapabilitySnapshot interface{ ImageCapabilityChanged() bool }
10
11 // Use the existing build/swap/lease boundary before accepting a new turn.
12 // A failed rebuild leaves the previous snapshot visible and rejects this turn.
13 func (a *App) refreshTabImageCapability(tab *WorkspaceTab) error {
14 a.runtimeRebuildMu.Lock()
15 defer a.runtimeRebuildMu.Unlock()
16 tab.turnStartMu.Lock()
17 defer tab.turnStartMu.Unlock()
18 current, ok := a.controllerForTab(tab).(imageCapabilitySnapshot)
19 if !ok || !current.ImageCapabilityChanged() {
20 return nil
21 }
22 if err := a.rebuildSettingTurnLocked("image input", tab, false, false); err != nil {
23 return fmt.Errorf("refresh image input configuration: %w", err)
24 }
25 return nil
26 }
27
28 // tabTurnAdmission owns both locks acquired while a foreground turn starts.
29 type tabTurnAdmission struct {
30 app *App
31 tab *WorkspaceTab
32 released bool
33 }
34
35 type turnFinishingWaiter interface {
36 TurnFinishingDone() (<-chan struct{}, bool)
37 }
38
39 func (admission *tabTurnAdmission) finish(ctrl control.SessionAPI) bool {
40 if admission == nil || admission.released {
41 return false
42 }
43 admission.released = true
44 tab := admission.tab
45 if tab != nil {
46 // Defers preserve lock release if RuntimeStatus panics.
47 defer admission.app.runtimeAdmissionMu.RUnlock()
48 defer tab.turnStartMu.Unlock()
49 }
50 started := ctrl != nil && ctrl.RuntimeStatus().Running
51 if !started && tab != nil && tab.sink != nil {
52 tab.sink.cancelTurnStart()
53 }
54 return started
55 }
56
57 func (admission *tabTurnAdmission) abort() {
58 admission.finish(nil)
59 }
60
61 // beginTabTurn reserves one tab until its TurnDone fan-out completes.
62 func (a *App) beginTabTurn(tabID string, reclaim bool, submissionID ...string) (*tabTurnAdmission, control.SessionAPI, error) {
63 return a.beginRuntimeTurn(tabID, reclaim, false, submissionID...)
64 }
65
66 func (a *App) beginRuntimeTurn(tabID string, reclaim, detached bool, submissionID ...string) (*tabTurnAdmission, control.SessionAPI, error) {
67 for {
68 tab, ctrl := a.tabAndCtrlByID(tabID)
69 if detached {
70 a.mu.RLock()
71 tab = a.tabByEventSinkIDLocked(tabID)
72 ctrl = nil
73 if tab != nil {
74 ctrl = tab.Ctrl
75 }
76 a.mu.RUnlock()
77 }
78 if a.tabIsReadOnly(tab) {
79 return nil, nil, readOnlyChannelErr()
80 }
81 if err := a.workspaceRuntimeAdmissionErr(tab, ctrl); err != nil {
82 return nil, nil, err
83 }
84 // Slow workspace repair stays outside the runtime admission barrier.
85 if err := a.ensureTabControllerWorkspace(tab); err != nil {
86 return nil, nil, err
87 }
88
89 a.runtimeAdmissionMu.RLock()
90 abort := func() {
91 tab.turnStartMu.Unlock()
92 a.runtimeAdmissionMu.RUnlock()
93 }
94 tab.turnStartMu.Lock()
95 if err := a.validateDraftAdmission(tab, firstSubmissionID(submissionID)); err != nil {
96 abort()
97 return nil, nil, err
98 }
99 if a.tabIsReadOnly(tab) {
100 abort()
101 return nil, nil, readOnlyChannelErr()
102 }
103 if reclaim && a.botBridge != nil {
104 a.botBridge.reclaimFromDesktop(tab.ID)
105 }
106 ctrl = a.controllerForTab(tab)
107 if err := a.workspaceRuntimeAdmissionErr(tab, ctrl); err != nil {
108 abort()
109 return nil, nil, err
110 }
111 ctrl = a.controllerForTab(tab)
112 if err := a.workspaceRuntimeAdmissionErr(tab, ctrl); err != nil {
113 abort()
114 return nil, nil, err
115 }
116 if ctrl.RuntimeStatus().Running {
117 if waiter, ok := ctrl.(turnFinishingWaiter); ok {
118 if done, finishing := waiter.TurnFinishingDone(); finishing {
119 // Re-resolve after waiting so close/switch cannot misroute retry.
120 abort()
121 <-done
122 continue
123 }
124 // Fan-out can end between RuntimeStatus and TurnFinishingDone.
125 // Re-check before reporting busy so that completed boundary retries
126 // instead of preserving the original false rejection window.
127 if !ctrl.RuntimeStatus().Running {
128 abort()
129 continue
130 }
131 }
132 abort()
133 return nil, nil, control.ErrTurnRunning
134 }
135 if a.ctx != nil {
136 needed, err := modelSettingsNeedApply(ctrl)
137 if err != nil {
138 abort()
139 return nil, nil, fmt.Errorf("read saved model settings: %w", err)
140 }
141 if needed {
142 a.mu.RLock()
143 draftPending := tab.PendingCreateOperationID != ""
144 a.mu.RUnlock()
145 if draftPending {
146 abort()
147 return nil, nil, fmt.Errorf("model configuration changed before draft admission")
148 }
149 abort()
150 if err := a.refreshTabModelSettings(tab); err != nil {
151 return nil, nil, err
152 }
153 continue
154 }
155 }
156 if snapshot, ok := ctrl.(imageCapabilitySnapshot); a.ctx != nil && ok && snapshot.ImageCapabilityChanged() {
157 a.mu.RLock()
158 draftPending := tab.PendingCreateOperationID != ""
159 a.mu.RUnlock()
160 abort()
161 if draftPending {
162 return nil, nil, fmt.Errorf("image configuration changed before draft admission")
163 }
164 if err := a.refreshTabImageCapability(tab); err != nil {
165 return nil, nil, err
166 }
167 continue
168 }
169 if authentication, ok := ctrl.(interface {
170 AuthenticationState() control.AuthenticationState
171 }); ok {
172 state := authentication.AuthenticationState()
173 if !state.Ready() {
174 abort()
175 return nil, nil, &control.AuthenticationError{State: state}
176 }
177 }
178 if tab.sink != nil && !tab.sink.tryBeginTurn(submissionID...) {
179 abort()
180 return nil, nil, control.ErrTurnRunning
181 }
182 if metadata, ok := ctrl.(interface {
183 SetTurnEventRoutingMetadata(runtimeEpoch, submissionID string)
184 }); ok {
185 epoch := ""
186 if tab.sink != nil {
187 epoch = tab.sink.runtimeEpochSnapshot()
188 }
189 metadata.SetTurnEventRoutingMetadata(epoch, firstSubmissionID(submissionID))
190 }
191 return &tabTurnAdmission{app: a, tab: tab}, ctrl, nil
192 }
193 }
194
195 // Durable follow-ups are new runs even when a controller dispatches them on
196 // its own after TurnDone. Resolve the runtime owner again after detach/attach.
197 func (a *App) beforeInboxDispatch(ctrl *control.Controller) (func(), error) {
198 a.mu.RLock()
199 var owner *WorkspaceTab
200 for _, tab := range a.runtimeTabsLocked() {
201 if tab.Ctrl == ctrl {
202 owner = tab
203 break
204 }
205 }
206 a.mu.RUnlock()
207 if owner == nil {
208 return nil, control.ErrInboxRuntimeUnpublished
209 }
210 admission, current, err := a.beginRuntimeTurn(owner.ID, false, true)
211 if err != nil {
212 return nil, err
213 }
214 if current != ctrl {
215 admission.abort()
216 if replacement, ok := current.(*control.Controller); ok {
217 go replacement.NotifyInboxRuntimeReady()
218 }
219 return nil, control.ErrInboxRuntimeUnpublished
220 }
221 return func() { admission.finish(ctrl) }, nil
222 }
223
223 lines GO