返回 DeepSeek-Reasonix
session_resume.go
根目录 / desktop / session_resume.go
1 package main
2
3 import (
4 "context"
5 "errors"
6 "fmt"
7 "strings"
8
9 "reasonix/desktop/internal/workspacestate"
10 "reasonix/internal/control"
11 "reasonix/internal/session"
12 )
13
14 var errSessionNavigationSuperseded = errors.New("session navigation was superseded")
15
16 func (a *App) continueLegacySessionForTranscript(tab *WorkspaceTab, ctrl control.SessionAPI, sourcePath string, limit int, includeHistory, readOnly bool) (HistoryPage, error) {
17 identity, ok := ctrl.(control.IdentityLifecycle)
18 if !ok || !identity.UsesExclusiveSession() {
19 return HistoryPage{}, fmt.Errorf("session identity protocol is unavailable")
20 }
21 navigationCtx, finishNavigation := a.beginSessionNavigationContext()
22 defer finishNavigation()
23 if err := context.Cause(navigationCtx); err != nil {
24 return HistoryPage{}, err
25 }
26 if _, adopted, err := a.legacyCanonicalRef(navigationCtx, sourcePath); err != nil {
27 return HistoryPage{}, err
28 } else if !adopted {
29 // Explicit navigation imports before taking any controller swap gate.
30 if _, err := a.ImportHistoricalSession(desktopSourceKey(sourcePath, "")); err != nil {
31 return HistoryPage{}, err
32 }
33 }
34
35 a.runtimeRebuildMu.Lock()
36 defer a.runtimeRebuildMu.Unlock()
37 tab.turnStartMu.Lock()
38 defer tab.turnStartMu.Unlock()
39 if err := context.Cause(navigationCtx); err != nil {
40 return HistoryPage{}, err
41 }
42
43 current := a.controllerForTab(tab)
44 if current != ctrl || current == nil {
45 return HistoryPage{}, fmt.Errorf("tab runtime changed while continuing legacy session")
46 }
47 if current.RuntimeStatus().Running || current.RuntimeStatus().PendingPrompt {
48 return HistoryPage{}, control.ErrTurnRunning
49 }
50 if !historicalPreview(current) {
51 if err := current.Snapshot(); err != nil {
52 return HistoryPage{}, err
53 }
54 }
55 a.mu.RLock()
56 createOptions := desktopLegacyImportOptions(snapshotTabRuntimeLocked(tab).workspaceRoot)
57 a.mu.RUnlock()
58 _, err := a.openOrImportDesktopLegacySession(navigationCtx, identity, sourcePath, createOptions)
59 if err != nil {
60 return HistoryPage{}, err
61 }
62 a.syncTabSessionIdentity(tab, current)
63 a.setTabReadOnly(tab.ID, readOnly)
64 a.invalidatePromptHistoryCache()
65 a.notifyTabRuntimeRebuilt(tab)
66 if !includeHistory {
67 return HistoryPage{Messages: []HistoryMessage{}}, nil
68 }
69 return historyPageFromMessagesForTab(tab, current, current.History(), 0, limit), nil
70 }
71
72 // canonicalOpenIdentity validates the identity protocol of a resolved runtime.
73 // A nil runtime is a dormant tab, not a protocol failure.
74 func canonicalOpenIdentity(ctrl control.SessionAPI) (control.IdentityLifecycle, error) {
75 if ctrl == nil {
76 return nil, nil
77 }
78 // A native historical JSONL runtime is a supported source, but has no
79 // canonical binding to reuse. Let the existing prepare-and-swap path build
80 // the destination runtime without closing or converting the source first.
81 if native, ok := ctrl.(*control.Controller); ok && native.NativeLegacySession() {
82 return nil, nil
83 }
84 identity, ok := ctrl.(control.IdentityLifecycle)
85 if !ok || !identity.UsesExclusiveSession() {
86 return nil, fmt.Errorf("session identity protocol is unavailable")
87 }
88 return identity, nil
89 }
90
91 func (a *App) resumeCanonicalSessionForTranscript(tab *WorkspaceTab, ctrl control.SessionAPI, route string, limit int, includeHistory bool, navigationSequence ...uint64) (HistoryPage, error) {
92 identity, err := canonicalOpenIdentity(ctrl)
93 if err != nil {
94 return HistoryPage{}, err
95 }
96 service := a.desktopSessionService("")
97 ref, ok := sessionRefForRoute(service, route)
98 if !ok {
99 return HistoryPage{}, fmt.Errorf("invalid session identity")
100 }
101 navigationCtx, finishNavigation := a.beginSessionNavigationContext(navigationSequence...)
102 defer finishNavigation()
103 if err := context.Cause(navigationCtx); err != nil {
104 return HistoryPage{}, err
105 }
106 workspace, err := a.canonicalSessionWorkspace(navigationCtx, ref)
107 if err != nil {
108 return HistoryPage{}, err
109 }
110
111 a.runtimeRebuildMu.Lock()
112 defer a.runtimeRebuildMu.Unlock()
113 tab.turnStartMu.Lock()
114 defer tab.turnStartMu.Unlock()
115 wantedNavigation := uint64(0)
116 if len(navigationSequence) > 0 {
117 wantedNavigation = navigationSequence[0]
118 if a.desktopSessions.navigationSeq.Load() != wantedNavigation {
119 return HistoryPage{}, errSessionNavigationSuperseded
120 }
121 }
122 if err := context.Cause(navigationCtx); err != nil {
123 return HistoryPage{}, err
124 }
125
126 current := a.controllerForTab(tab)
127 if ctrl == nil {
128 // The caller resolved a dormant tab before it had a runtime. Adopting a
129 // concurrently built one here, under the rebuild lock, is the
130 // authoritative read; the fences below still reject a later build.
131 ctrl = current
132 if identity, err = canonicalOpenIdentity(ctrl); err != nil {
133 return HistoryPage{}, err
134 }
135 }
136 if current != ctrl {
137 return HistoryPage{}, fmt.Errorf("tab runtime changed while opening session")
138 }
139 var currentRef session.SessionRef
140 if identity != nil {
141 currentRef, _ = identity.SessionRef()
142 }
143 workspaceChanged := canonicalWorkspaceChanged(a.tabRuntimeSnapshot(tab), workspace)
144 if current == nil || currentRef != ref || workspaceChanged {
145 if current != nil && !controllerHasActiveRuntimeWork(current) && !historicalPreview(current) {
146 if err := current.Snapshot(); err != nil {
147 return HistoryPage{}, err
148 }
149 }
150 adopted, err := a.reattachCanonicalSessionRuntime(tab, current, ref, workspace, wantedNavigation)
151 if err != nil {
152 return HistoryPage{}, err
153 }
154 if adopted != nil {
155 current = adopted
156 } else {
157 binding, err := service.EnsureExecution(navigationCtx, ref)
158 if err != nil {
159 return HistoryPage{}, err
160 }
161 defer func() { _ = binding.Release(a.bootContext()) }()
162 targetModel := strings.TrimSpace(binding.Runtime().StateSnapshot().Session.Projection.ModelRef)
163 current, err = a.replaceControllerForSessionOpenLocked(navigationCtx, tab, current, service, ref, targetModel, workspace, wantedNavigation)
164 if err != nil {
165 return HistoryPage{}, err
166 }
167 }
168 }
169 if err := a.commitCanonicalSessionBinding(tab, current, ref, workspace, wantedNavigation); err != nil {
170 return HistoryPage{}, err
171 }
172 a.setTabReadOnly(tab.ID, false)
173 a.invalidatePromptHistoryCache()
174 a.notifyTabRuntimeRebuilt(tab)
175 if !includeHistory {
176 return HistoryPage{Messages: []HistoryMessage{}}, nil
177 }
178 return historyPageFromMessagesForTab(tab, current, current.History(), 0, limit), nil
179 }
180
181 // replaceControllerForSessionOpenLocked prepares an Agent for the target session's
182 // recorded model before publishing it to the tab. The caller holds
183 // runtimeRebuildMu and tab.turnStartMu, so the source remains usable until the
184 // target model, writer, and event projection have all been validated.
185 func (a *App) replaceControllerForSessionOpenLocked(ctx context.Context, tab *WorkspaceTab, current control.SessionAPI, service *session.Service, ref session.SessionRef, targetModel string, workspace workspacestate.Workspace, navigationSequence ...uint64) (control.SessionAPI, error) {
186 if tab == nil || service == nil {
187 return nil, fmt.Errorf("session runtime changed while opening session")
188 }
189 transition, err := a.reserveSessionRuntimePath(tab, sessionRoute(ref.SessionID))
190 if err != nil {
191 return nil, userFacingSessionLeaseError("", err)
192 }
193 committed := false
194 // boot retains its context for MCP and other controller-owned work. Relay
195 // navigation cancellation only until publication, then keep the app lifetime.
196 controllerCtx, cancelController := context.WithCancel(a.bootContext())
197 stopNavigationCancellation := context.AfterFunc(ctx, cancelController)
198 defer func() {
199 stopNavigationCancellation()
200 if !committed {
201 cancelController()
202 a.rollbackSessionRuntimePath(transition)
203 }
204 }()
205 prepared, err := a.prepareSessionOpenEnvironment(tab, workspace)
206 if err != nil {
207 return nil, err
208 }
209 defer func() { a.finishSessionOpenEnvironment(prepared, committed) }()
210 snap, cfg, root, sharedHost := prepared.snapshot, prepared.config, workspace.Root, prepared.host
211 if targetModel == "" {
212 targetModel, _, _ = cfg.ResolveDesktopNewSessionModel()
213 }
214 extensionGeneration := a.currentExtensionGeneration()
215 buildOptions := a.sessionOpenBootOptions(tab, snap, cfg, service, sharedHost, root, targetModel)
216 requestedModel := targetModel
217 candidate, targetModel, fallbackUsed, err := a.buildSessionOpenControllerCandidate(controllerCtx, extensionGeneration, cfg, buildOptions)
218 if err != nil {
219 return nil, err
220 }
221 discard := true
222 defer func() {
223 if discard {
224 candidate.Close()
225 }
226 }()
227 candidateIdentity, ok := candidate.(control.IdentityLifecycle)
228 if !ok || !candidateIdentity.UsesExclusiveSession() {
229 return nil, fmt.Errorf("replacement session identity protocol is unavailable")
230 }
231 if _, err := candidateIdentity.OpenSession(ctx, ref); err != nil {
232 return nil, err
233 }
234 if fallbackUsed {
235 if err := service.SetModel(ctx, ref, targetModel, cfg.ModelSelectionIdentity(targetModel)); err != nil {
236 return nil, err
237 }
238 a.noticeForTab(tab.ID, fmt.Sprintf("model %q is no longer available; switched to %s", requestedModel, targetModel))
239 }
240 a.bindControllerDisplayRecorder(candidate)
241 _, fallbackPreset := desktopNewSessionDefaults(snap.scope, root)
242 runtime := prepareCanonicalControllerRuntime(candidate, snap, a.sessionPresets.restore(ref.SessionID, fallbackPreset))
243
244 confirmed, err := a.canonicalSessionWorkspace(ctx, ref)
245 if err != nil {
246 return nil, err
247 }
248 if confirmed.ID != workspace.ID || !sameDesktopPath(confirmed.Root, root) {
249 return nil, errSessionWorkspaceConflict
250 }
251 var terminalSessions []*terminalSession
252 a.mu.Lock()
253 if err := a.authorizeSessionOpenPublicationLocked(tab, current, candidate, navigationSequence); err != nil {
254 a.mu.Unlock()
255 return nil, err
256 }
257 if !stopNavigationCancellation() || ctx.Err() != nil {
258 a.mu.Unlock()
259 return nil, context.Canceled
260 }
261 oldSink := tab.sink
262 if !a.commitCanonicalRuntimeTransitionLocked(tab, transition, prepared.preserveSource) {
263 a.mu.Unlock()
264 return nil, fmt.Errorf("tab runtime changed while opening session")
265 }
266 if prepared.workspaceChanged && a.terminals != nil {
267 terminalSessions = a.terminals.detachForTab(tab.ID)
268 }
269 applyCanonicalWorkspaceLocked(tab, workspace, prepared.workspaceChanged)
270 tab.SharedHostKey = snap.sharedHostKey
271 tab.Ctrl = candidate
272 tab.sink = snap.sink
273 tab.adoptDisplayState(&tabDisplayState{})
274 tab.ActivityStatus = ""
275 tab.replaceTelemetry(loadTelemetryFor(sessionRoute(ref.SessionID)), sessionRuntimeKey(sessionRoute(ref.SessionID)))
276 setTabSessionIdentity(tab, sessionRoute(ref.SessionID))
277 tab.model = targetModel
278 tab.Label = candidate.Label()
279 applyNormalizedRuntimeToTabLocked(tab, runtime)
280 tab.Ready = true
281 clearTabStartupError(tab)
282 if prepared.preserveSource {
283 a.newSessionRuntimeLocked(tab, transition.targetKey)
284 }
285 tab.sink.setBinding(tab.ID, a, tab.SessionGeneration)
286 tab.sink.setContext(a.ctx)
287 a.bindSessionRuntimeKeyLocked(tab, tab.currentSessionIdentity())
288 a.supersedeTabBuildLocked(tab)
289 a.saveTabsLocked()
290 epoch := a.advanceSessionRuntimeEpochLocked(tab)
291 committed = true
292 a.mu.Unlock()
293
294 if !prepared.preserveSource {
295 fenceCanonicalNavigationSink(oldSink)
296 retireReplacedController(current, candidate)
297 }
298 if prepared.workspaceChanged {
299 a.finishCanonicalWorkspaceMove(tab.ID, terminalSessions)
300 }
301 discard = false
302 a.notifyTabRuntimeRebuiltAtEpoch(tab, epoch)
303 return candidate, nil
304 }
305
306 // The caller holds App.mu so intent, surface identity, and replacement guards
307 // are checked against the same state immediately before controller publication.
308 func (a *App) authorizeSessionOpenPublicationLocked(tab *WorkspaceTab, current, candidate control.SessionAPI, navigation []uint64) error {
309 if len(navigation) > 0 && navigation[0] != 0 && a.desktopSessions.navigationSeq.Load() != navigation[0] {
310 return errSessionNavigationSuperseded
311 }
312 if tab.removed || a.tabs[tab.ID] != tab || tab.Ctrl != current {
313 return fmt.Errorf("tab runtime changed while opening session")
314 }
315 return a.authorizeTabReplacementLocked(tab, candidate, "opening session", "session-open")
316 }
317
317 lines GO