返回 DeepSeek-Reasonix
tab_controller_boot.go
根目录 / desktop / tab_controller_boot.go
1 package main
2
3 import (
4 "context"
5 "errors"
6 "path/filepath"
7
8 "reasonix/internal/boot"
9 "reasonix/internal/control"
10 "reasonix/internal/session"
11 )
12
13 // Historical directories share one owner per root, just like canonical
14 // sessions. Repeated opens and rebuilds must not manufacture competing writers.
15 func (a *App) historicalSessionService(root string) (*session.Service, error) {
16 root = filepath.Clean(root)
17 a.sessionServicesMu.Lock()
18 defer a.sessionServicesMu.Unlock()
19 if existing := a.historicalSessionServices[root]; existing != nil {
20 return existing, nil
21 }
22 service, err := session.NewService(localDesktopHostID, session.NewFilesystemPersistence(root))
23 if err != nil {
24 return nil, err
25 }
26 if a.historicalSessionServices == nil {
27 a.historicalSessionServices = map[string]*session.Service{}
28 }
29 a.historicalSessionServices[root] = service
30 service.UseIdlePool(&a.historyIdlePool)
31 service.ConfigureHistoryMaintenance(&a.historyMaintenance)
32 return service, nil
33 }
34
35 var errTabControllerExtensionsChanged = errors.New("desktop: controller extensions changed during build")
36
37 // buildTabControllerBoot is a thin wrapper around boot.Build so the large
38 // controller assembly path can stay under function-size / complexity budgets.
39 func (a *App) buildTabControllerBoot(ctx context.Context, opts boot.Options) (control.SessionAPI, error) {
40 if opts.NativeLegacySession && opts.SessionService == nil {
41 // A path-backed source may have a newer paired event store in its
42 // historical root. Keep that authority discoverable without importing
43 // either source into the current Desktop root.
44 var err error
45 opts.SessionService, err = a.historicalSessionService(desktopSessionRoot(opts.SessionDir))
46 if err != nil {
47 return nil, err
48 }
49 }
50 if opts.SessionService == nil {
51 opts.SessionService = a.desktopSessionService(opts.SessionDir)
52 }
53 if opts.SessionCreateService == nil {
54 opts.SessionCreateService = a.desktopSessionService(opts.SessionDir)
55 }
56 if opts.OnSessionRotation == nil {
57 opts.OnSessionRotation = a.prepareDesktopSessionRotation
58 }
59 // Every controller on this host watches through one shared service, so a
60 // rebuild reuses the helper process instead of spawning another one.
61 if opts.SharedSkillWatchService == nil {
62 opts.SharedSkillWatchService = a.sharedSkillWatchService()
63 }
64 return boot.Build(ctx, opts)
65 }
66
67 func desktopSessionRoot(sessionDir string) string {
68 return session.RootForLegacyDir(sessionDir)
69 }
70
71 func (a *App) desktopSessionService(sessionDir string) *session.Service {
72 if a == nil {
73 return nil
74 }
75 a.sessionServicesMu.Lock()
76 defer a.sessionServicesMu.Unlock()
77 root := a.desktopSessions.root
78 // Zero-value Apps in narrow tests retain an isolated legacy-derived root;
79 // NewApp always supplies the production v5 root.
80 if root == "" {
81 root = desktopSessionRoot(sessionDir)
82 a.desktopSessions.root = root
83 }
84 if root == "" {
85 return nil
86 }
87 if a.sessionServices == nil {
88 a.sessionServices = map[string]*session.Service{}
89 }
90 for _, service := range a.sessionServices {
91 // There is deliberately one local service even when a caller still
92 // carries a project-local legacy sessionDir during the cutover.
93 if service != nil {
94 return service
95 }
96 }
97 if service := a.sessionServices[root]; service != nil {
98 return service
99 }
100 service, err := session.NewService("local", session.NewFilesystemPersistence(root))
101 if err != nil {
102 return nil
103 }
104 a.sessionServices[root] = service
105 service.UseIdlePool(&a.historyIdlePool)
106 service.ConfigureHistoryMaintenance(&a.historyMaintenance)
107 return service
108 }
109
110 // buildTabControllerBootFenced keeps optimistic builds concurrent with each
111 // other but excludes live MCP mutation. The generation check happens after the
112 // gate so a build that loaded stale configuration never launches extensions.
113 func (a *App) buildTabControllerBootFenced(ctx context.Context, generation uint64, opts boot.Options) (control.SessionAPI, error) {
114 a.extensionBuildMu.RLock()
115 defer a.extensionBuildMu.RUnlock()
116 if a.currentExtensionGeneration() != generation {
117 return nil, errTabControllerExtensionsChanged
118 }
119 return a.buildTabControllerBoot(ctx, opts)
120 }
121
122 // lockTabControllerPublication makes extension generation and project
123 // maintenance reservations part of the same publication admission. An MCP
124 // writer bumps the generation before releasing runtimeAdmissionMu, while a
125 // worktree mutation publishes its canonical reservation before releasing the
126 // write side, so neither stale registries nor a late project controller can be
127 // installed afterward.
128 func (a *App) lockTabControllerPublication(generation uint64, scope, workspaceRoot string) (func(), bool) {
129 a.runtimeAdmissionMu.RLock()
130 if a.currentExtensionGeneration() != generation {
131 a.runtimeAdmissionMu.RUnlock()
132 return nil, false
133 }
134 if scope != "project" {
135 return a.runtimeAdmissionMu.RUnlock, true
136 }
137 key := canonicalRuntimeRoot(workspaceRoot)
138 if key == "" || a.workspaceMergeReservedSnapshot(key) {
139 a.runtimeAdmissionMu.RUnlock()
140 return nil, false
141 }
142 return a.runtimeAdmissionMu.RUnlock, true
143 }
144
145 func (a *App) handleTabControllerBootError(
146 tab *WorkspaceTab,
147 registration *sharedHostMCPRegistration,
148 rootKey string,
149 buildGeneration uint64,
150 wailsCtx context.Context,
151 err error,
152 ) bool {
153 if err == nil {
154 return false
155 }
156 registration.rollback()
157 if errors.Is(err, errTabControllerExtensionsChanged) {
158 a.abandonSupersededBuild(tab, nil, rootKey, "")
159 a.scheduleDeferredStartupBuild(tab.ID)
160 return true
161 }
162 a.mu.Lock()
163 if a.tabBuildSupersededLocked(tab, buildGeneration) {
164 a.mu.Unlock()
165 a.abandonSupersededBuild(tab, nil, rootKey, "")
166 return true
167 }
168 leaseHeld, save := a.markTabStartupFailureLocked(tab, err, keepStartupRestore)
169 hostKey := takeTabSharedHostKey(tab)
170 tab.releaseSessionLease()
171 a.mu.Unlock()
172 a.writeTabsSaveRequest(save)
173 if hostKey != "" {
174 a.releaseSharedHost(hostKey)
175 }
176 if leaseHeld {
177 a.scheduleDeferredStartupBuild(tab.ID)
178 }
179 a.emitReady(wailsCtx, tab.ID)
180 return true
181 }
182
182 lines GO