返回 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.browserExecutorForRuntime(tab.ID, snap.sink),
48 SharedSkillWatchService: a.sharedSkillWatchService(),
49 MCPHostProfile: plugin.HostProfileDesktopApps,
50 CleanupPendingReconciler: reconcileDesktopCleanupPending,
51 SubagentParentLive: a.subagentParentProbeForBuild(tab),
52 SessionRecoveryMeta: a.tabSessionRecoveryMeta(tab),
53 PinnedContextLoader: pinnedContextLoader(root),
54 OnSessionRecovered: a.handleTabSessionRecovered(tab),
55 OnSessionTransition: a.handleTabSessionTransition(tab),
56 BeforeInboxDispatch: a.beforeInboxDispatch,
57 OnSessionTitleChanged: a.onSessionTitleChanged,
58 }
59 }
60
61 func (a *App) bootTabControllerWithModelFallback(
62 baseCtx context.Context,
63 tab *WorkspaceTab,
64 cfg *config.Config,
65 sharedHost *plugin.Host,
66 options boot.Options,
67 extensionGeneration, buildGeneration uint64,
68 sessionID, requestedModel string,
69 ) tabControllerBootResult {
70 buildCtx, registration := beginSharedHostMCPRegistration(baseCtx, sharedHost)
71 controller, err := a.buildTabControllerBootFenced(buildCtx, extensionGeneration, options)
72 result := tabControllerBootResult{controller: controller, ctx: buildCtx, registration: registration, model: options.Model, err: err}
73 a.mu.RLock()
74 draftCreate := tab != nil && strings.TrimSpace(tab.PendingCreateOperationID) != ""
75 a.mu.RUnlock()
76 if !errors.Is(err, boot.ErrUnknownModel) || strings.TrimSpace(sessionID) == "" || draftCreate {
77 return result
78 }
79 fallbackModel, _, ok := cfg.ResolveDesktopNewSessionModel()
80 if !ok || fallbackModel == options.Model {
81 return result
82 }
83 registration.rollback()
84 result.ctx, result.registration = beginSharedHostMCPRegistration(baseCtx, sharedHost)
85 options.Model = fallbackModel
86 result.controller, result.err = a.buildTabControllerBootFenced(result.ctx, extensionGeneration, options)
87 if result.err != nil {
88 return result
89 }
90 result.model, result.fallback = fallbackModel, true
91 a.noticeForTab(tab.ID, fmt.Sprintf("model %q is no longer available; switched to %s", requestedModel, fallbackModel))
92 a.mu.Lock()
93 if !a.tabBuildSupersededLocked(tab, buildGeneration) {
94 tab.model, tab.Label = fallbackModel, fallbackModel
95 }
96 a.mu.Unlock()
97 return result
98 }
99
100 func (a *App) bindTabCanonicalSession(
101 ctx context.Context,
102 identity control.IdentityLifecycle,
103 cfg *config.Config,
104 scope, workspaceRoot, sessionID, legacyPath, model string,
105 modelFallback bool,
106 ) (session.SessionRef, string, error) {
107 var ref session.SessionRef
108 var workspaceID string
109 var err error
110 switch {
111 case strings.TrimSpace(sessionID) != "":
112 service := identity.SessionService()
113 if service == nil {
114 return ref, "", errors.New("v3 session service is unavailable")
115 }
116 if tab := a.tabForSessionBoot(scope, workspaceRoot, sessionID); tab != nil && strings.HasPrefix(sessionID, "desktop-manual-") {
117 a.mu.RLock()
118 reserved := tab.PendingCreateOperationID != ""
119 a.mu.RUnlock()
120 if initializer, ok := identity.(interface {
121 InitializeReservedSession(context.Context, session.SessionRef) error
122 }); ok && reserved {
123 if err := initializer.InitializeReservedSession(ctx, session.SessionRef{HostID: service.HostID(), SessionID: sessionID}); err != nil {
124 return ref, "", err
125 }
126 }
127 }
128 ref, err = identity.OpenSession(ctx, session.SessionRef{HostID: service.HostID(), SessionID: strings.TrimSpace(sessionID)})
129 if errors.Is(err, session.ErrSessionNotFound) {
130 operationID := ""
131 if tab := a.tabForSessionBoot(scope, workspaceRoot, sessionID); tab != nil {
132 operationID = tab.PendingCreateOperationID
133 }
134 if operationID != "" {
135 ref, workspaceID, err = a.bindFreshDesktopSessionWithIDs(ctx, scope, workspaceRoot, identity, sessionID, operationID)
136 }
137 }
138 case strings.TrimSpace(legacyPath) != "":
139 ref, err = a.openOrImportDesktopLegacySession(ctx, identity, legacyPath, session.CreateOptions{
140 CWD: desktopWorkspaceRoot(scope, workspaceRoot), Origin: session.SessionOriginLegacyImport,
141 })
142 if errors.Is(err, errUnadoptedLegacySourceMissing) {
143 evidence := a.loadSavedTabReconcileEvidence(ctx)
144 if evidence.registryErr != nil {
145 return ref, "", errors.Join(errLegacySourceRecoveryPending, evidence.registryErr)
146 }
147 if savedTabHasRecoveryOwner(desktopTabEntry{SessionPath: legacyPath}, evidence) {
148 return ref, "", errLegacySourceRecoveryPending
149 }
150 ref, workspaceID, err = a.bindFreshDesktopSession(ctx, scope, workspaceRoot, identity)
151 }
152 default:
153 ref, workspaceID, err = a.bindFreshDesktopSession(ctx, scope, workspaceRoot, identity)
154 }
155 if err == nil && workspaceID == "" {
156 workspaceID, err = a.attachDesktopSession(ctx, scope, workspaceRoot, ref)
157 }
158 if err == nil && modelFallback {
159 err = identity.SessionService().SetModel(ctx, ref, model, cfg.ModelSelectionIdentity(model))
160 }
161 return ref, workspaceID, err
162 }
163
164 func (a *App) tabForSessionBoot(scope, workspaceRoot, sessionID string) *WorkspaceTab {
165 a.mu.RLock()
166 defer a.mu.RUnlock()
167 for _, tab := range a.runtimeTabsLocked() {
168 if tab != nil && tab.Scope == scope && tab.SessionID == sessionID &&
169 (scope != "project" || sameProjectRoot(tab.WorkspaceRoot, workspaceRoot)) {
170 return tab
171 }
172 }
173 return nil
174 }
175
176 var errUnadoptedLegacySourceMissing = errors.New("unadopted legacy source is missing")
177 var errLegacySourceRecoveryPending = errors.New("legacy session recovery is pending")
178
179 // The Desktop registry owns adoption. Re-freezing a previously imported source
180 // can generate a different identity after a catalog sidecar refresh, even when
181 // the historical messages have not changed. Resolve adoption before inspecting
182 // the source so a retained canonical session also survives source removal.
183 func (a *App) openOrImportDesktopLegacySession(ctx context.Context, identity control.IdentityLifecycle, path string, options session.CreateOptions) (session.SessionRef, error) {
184 if ref, adopted, err := a.legacyCanonicalRef(ctx, path); adopted || err != nil {
185 if err != nil {
186 return session.SessionRef{}, err
187 }
188 return identity.OpenSession(ctx, ref)
189 }
190 if _, err := os.Stat(path); err != nil {
191 if os.IsNotExist(err) {
192 return session.SessionRef{}, errUnadoptedLegacySourceMissing
193 }
194 return session.SessionRef{}, err
195 }
196 if creator, ok := identity.(control.IdentityCreateLifecycle); ok {
197 return creator.ContinueLegacySessionWithOptions(ctx, path, "", options)
198 }
199 return identity.ContinueLegacySession(ctx, path, "")
200 }
201
202 func (a *App) buildSessionOpenControllerCandidate(
203 ctx context.Context,
204 extensionGeneration uint64,
205 cfg *config.Config,
206 options boot.Options,
207 ) (control.SessionAPI, string, bool, error) {
208 if hook := a.sessionOpenBuildHook; hook != nil {
209 hook(ctx)
210 if err := ctx.Err(); err != nil {
211 return nil, options.Model, false, err
212 }
213 }
214 requestedModel := options.Model
215 candidate, err := a.buildTabControllerBootFenced(ctx, extensionGeneration, options)
216 if !errors.Is(err, boot.ErrUnknownModel) {
217 return candidate, requestedModel, false, err
218 }
219 fallbackModel, _, ok := cfg.ResolveDesktopNewSessionModel()
220 if !ok || fallbackModel == requestedModel {
221 return candidate, requestedModel, false, err
222 }
223 options.Model = fallbackModel
224 candidate, err = a.buildTabControllerBootFenced(ctx, extensionGeneration, options)
225 return candidate, fallbackModel, err == nil, err
226 }
227
227 lines GO