返回 DeepSeek-Reasonix
tab_session_boot.go
根目录 / desktop / tab_session_boot.go
1 package main
2
3 import (
4 "context"
5 "errors"
6 "fmt"
7 "os"
8 "strings"
9
10 "reasonix/internal/boot"
11 "reasonix/internal/config"
12 "reasonix/internal/control"
13 "reasonix/internal/plugin"
14 "reasonix/internal/session"
15 )
16
17 type tabControllerBootResult struct {
18 controller control.SessionAPI
19 ctx context.Context
20 registration *sharedHostMCPRegistration
21 model string
22 fallback bool
23 err error
24 }
25
26 func (a *App) sessionOpenBootOptions(
27 tab *WorkspaceTab,
28 snap tabRuntimeSnapshot,
29 cfg *config.Config,
30 service *session.Service,
31 sharedHost *plugin.Host,
32 root, model string,
33 ) boot.Options {
34 return boot.Options{
35 Model: model,
36 RequireKey: false,
37 StatsSource: "desktop",
38 TaskStore: a.taskStore(),
39 OnConfigLoadWarnings: a.configLoadWarningsHandler(),
40 Sink: a.desktopControllerSink(snap.sink, cfg.Notifications),
41 WorkspaceRoot: root,
42 SessionDir: sessionDirForSnapshot(snap),
43 SessionService: service,
44 EffortOverride: cloneStringPtr(snap.effort),
45 EffortModel: snap.model,
46 SharedHost: sharedHost,
47 BrowserExecutor: a.browserExecutorForTab(tab),
48 MCPHostProfile: plugin.HostProfileDesktopApps,
49 CleanupPendingReconciler: reconcileDesktopCleanupPending,
50 SubagentParentLive: a.subagentParentProbeForBuild(tab),
51 SessionRecoveryMeta: a.tabSessionRecoveryMeta(tab),
52 PinnedContextLoader: pinnedContextLoader(root),
53 OnSessionRecovered: a.handleTabSessionRecovered(tab),
54 OnSessionTransition: a.handleTabSessionTransition(tab),
55 BeforeInboxDispatch: a.beforeInboxDispatch,
56 OnSessionTitleChanged: a.onSessionTitleChanged,
57 }
58 }
59
60 func (a *App) bootTabControllerWithModelFallback(
61 baseCtx context.Context,
62 tab *WorkspaceTab,
63 cfg *config.Config,
64 sharedHost *plugin.Host,
65 options boot.Options,
66 extensionGeneration, buildGeneration uint64,
67 sessionID, requestedModel string,
68 ) tabControllerBootResult {
69 buildCtx, registration := beginSharedHostMCPRegistration(baseCtx, sharedHost)
70 controller, err := a.buildTabControllerBootFenced(buildCtx, extensionGeneration, options)
71 result := tabControllerBootResult{controller: controller, ctx: buildCtx, registration: registration, model: options.Model, err: err}
72 a.mu.RLock()
73 draftCreate := tab != nil && strings.TrimSpace(tab.PendingCreateOperationID) != ""
74 a.mu.RUnlock()
75 if !errors.Is(err, boot.ErrUnknownModel) || strings.TrimSpace(sessionID) == "" || draftCreate {
76 return result
77 }
78 fallbackModel, _, ok := cfg.ResolveDesktopNewSessionModel()
79 if !ok || fallbackModel == options.Model {
80 return result
81 }
82 registration.rollback()
83 result.ctx, result.registration = beginSharedHostMCPRegistration(baseCtx, sharedHost)
84 options.Model = fallbackModel
85 result.controller, result.err = a.buildTabControllerBootFenced(result.ctx, extensionGeneration, options)
86 if result.err != nil {
87 return result
88 }
89 result.model, result.fallback = fallbackModel, true
90 a.noticeForTab(tab.ID, fmt.Sprintf("model %q is no longer available; switched to %s", requestedModel, fallbackModel))
91 a.mu.Lock()
92 if !a.tabBuildSupersededLocked(tab, buildGeneration) {
93 tab.model, tab.Label = fallbackModel, fallbackModel
94 }
95 a.mu.Unlock()
96 return result
97 }
98
99 func (a *App) bindTabCanonicalSession(
100 ctx context.Context,
101 identity control.IdentityLifecycle,
102 cfg *config.Config,
103 scope, workspaceRoot, sessionID, legacyPath, model string,
104 modelFallback bool,
105 ) (session.SessionRef, string, error) {
106 var ref session.SessionRef
107 var workspaceID string
108 var err error
109 switch {
110 case strings.TrimSpace(sessionID) != "":
111 service := identity.SessionService()
112 if service == nil {
113 return ref, "", errors.New("v3 session service is unavailable")
114 }
115 ref, err = identity.OpenSession(ctx, session.SessionRef{HostID: service.HostID(), SessionID: strings.TrimSpace(sessionID)})
116 if errors.Is(err, session.ErrSessionNotFound) {
117 operationID := ""
118 if tab := a.tabForSessionBoot(scope, workspaceRoot, sessionID); tab != nil {
119 operationID = tab.PendingCreateOperationID
120 }
121 if operationID != "" {
122 ref, workspaceID, err = a.bindFreshDesktopSessionWithIDs(ctx, scope, workspaceRoot, identity, sessionID, operationID)
123 }
124 }
125 case strings.TrimSpace(legacyPath) != "":
126 ref, err = a.openOrImportDesktopLegacySession(ctx, identity, legacyPath, session.CreateOptions{
127 CWD: desktopWorkspaceRoot(scope, workspaceRoot), Origin: session.SessionOriginLegacyImport,
128 })
129 if errors.Is(err, errUnadoptedLegacySourceMissing) {
130 evidence := a.loadSavedTabReconcileEvidence(ctx, false)
131 if evidence.registryErr != nil || evidence.draftErr != nil {
132 return ref, "", errors.Join(errLegacySourceRecoveryPending, evidence.registryErr, evidence.draftErr)
133 }
134 if savedTabHasRecoveryOwner(desktopTabEntry{SessionPath: legacyPath}, evidence) {
135 return ref, "", errLegacySourceRecoveryPending
136 }
137 ref, workspaceID, err = a.bindFreshDesktopSession(ctx, scope, workspaceRoot, identity)
138 }
139 default:
140 ref, workspaceID, err = a.bindFreshDesktopSession(ctx, scope, workspaceRoot, identity)
141 }
142 if err == nil && workspaceID == "" {
143 workspaceID, err = a.attachDesktopSession(ctx, scope, workspaceRoot, ref)
144 }
145 if err == nil && modelFallback {
146 err = identity.SessionService().SetModel(ctx, ref, model, cfg.ModelSelectionIdentity(model))
147 }
148 return ref, workspaceID, err
149 }
150
151 func (a *App) tabForSessionBoot(scope, workspaceRoot, sessionID string) *WorkspaceTab {
152 a.mu.RLock()
153 defer a.mu.RUnlock()
154 for _, tab := range a.runtimeTabsLocked() {
155 if tab != nil && tab.Scope == scope && tab.SessionID == sessionID &&
156 (scope != "project" || sameProjectRoot(tab.WorkspaceRoot, workspaceRoot)) {
157 return tab
158 }
159 }
160 return nil
161 }
162
163 var errUnadoptedLegacySourceMissing = errors.New("unadopted legacy source is missing")
164 var errLegacySourceRecoveryPending = errors.New("legacy session recovery is pending")
165
166 // The Desktop registry owns adoption. Re-freezing a previously imported source
167 // can generate a different identity after a catalog sidecar refresh, even when
168 // the historical messages have not changed. Resolve adoption before inspecting
169 // the source so a retained canonical session also survives source removal.
170 func (a *App) openOrImportDesktopLegacySession(ctx context.Context, identity control.IdentityLifecycle, path string, options session.CreateOptions) (session.SessionRef, error) {
171 if ref, adopted, err := a.legacyCanonicalRef(ctx, path); adopted || err != nil {
172 if err != nil {
173 return session.SessionRef{}, err
174 }
175 return identity.OpenSession(ctx, ref)
176 }
177 if _, err := os.Stat(path); err != nil {
178 if os.IsNotExist(err) {
179 return session.SessionRef{}, errUnadoptedLegacySourceMissing
180 }
181 return session.SessionRef{}, err
182 }
183 if creator, ok := identity.(control.IdentityCreateLifecycle); ok {
184 return creator.ContinueLegacySessionWithOptions(ctx, path, "", options)
185 }
186 return identity.ContinueLegacySession(ctx, path, "")
187 }
188
189 func (a *App) buildSessionOpenControllerCandidate(
190 ctx context.Context,
191 extensionGeneration uint64,
192 cfg *config.Config,
193 options boot.Options,
194 ) (control.SessionAPI, string, bool, error) {
195 if hook := a.sessionOpenBuildHook; hook != nil {
196 hook(ctx)
197 if err := ctx.Err(); err != nil {
198 return nil, options.Model, false, err
199 }
200 }
201 requestedModel := options.Model
202 candidate, err := a.buildTabControllerBootFenced(ctx, extensionGeneration, options)
203 if !errors.Is(err, boot.ErrUnknownModel) {
204 return candidate, requestedModel, false, err
205 }
206 fallbackModel, _, ok := cfg.ResolveDesktopNewSessionModel()
207 if !ok || fallbackModel == requestedModel {
208 return candidate, requestedModel, false, err
209 }
210 options.Model = fallbackModel
211 candidate, err = a.buildTabControllerBootFenced(ctx, extensionGeneration, options)
212 return candidate, fallbackModel, err == nil, err
213 }
214
214 lines GO