返回 DeepSeek-Reasonix
fork_targets.go
根目录 / desktop / fork_targets.go
1 package main
2
3 import (
4 "errors"
5 "fmt"
6 "log/slog"
7 "strings"
8
9 "reasonix/internal/control"
10 "reasonix/internal/session"
11 )
12
13 type forkTargetsController interface {
14 ForkTargets() (session.ForkTargetSet, error)
15 CreateForkSession(request session.ForkRequest, name string) (string, error)
16 }
17
18 var _ forkTargetsController = (*control.Controller)(nil)
19
20 type ForkAnchorView struct {
21 SourceHostID string `json:"sourceHostId,omitempty"`
22 SourceSessionID string `json:"sourceSessionId"`
23 SessionGeneration uint64 `json:"sessionGeneration"`
24 TurnID string `json:"turnId"`
25 BoundarySequence uint64 `json:"boundarySequence"`
26 }
27
28 type ForkTargetView struct {
29 SourceHostID string `json:"sourceHostId,omitempty"`
30 SourceSessionID string `json:"sourceSessionId"`
31 SessionGeneration uint64 `json:"sessionGeneration"`
32 TurnID string `json:"turnId"`
33 BoundarySequence uint64 `json:"boundarySequence"`
34 TurnNumber int `json:"turnNumber"`
35 Status string `json:"status"`
36 MessageID string `json:"messageId,omitempty"`
37 Available bool `json:"available"`
38 Reason string `json:"reason,omitempty"`
39 }
40
41 type ForkTargetSetView struct {
42 SourceHostID string `json:"sourceHostId,omitempty"`
43 SourceSessionID string `json:"sourceSessionId,omitempty"`
44 SessionGeneration uint64 `json:"sessionGeneration,omitempty"`
45 Targets []ForkTargetView `json:"targets"`
46 Verifiable bool `json:"verifiable"`
47 }
48
49 type ForkCreationView struct {
50 SessionID string `json:"sessionId,omitempty"`
51 TabID string `json:"tabId,omitempty"`
52 OperationID string `json:"operationId,omitempty"`
53 Opened bool `json:"opened"`
54 Code string `json:"code,omitempty"`
55 Reason string `json:"reason,omitempty"`
56 Error string `json:"error,omitempty"`
57 }
58
59 func forkTargetSetView(set session.ForkTargetSet, generation uint64) ForkTargetSetView {
60 view := ForkTargetSetView{SourceHostID: set.Source.HostID, SourceSessionID: set.Source.SessionID,
61 SessionGeneration: generation, Targets: make([]ForkTargetView, 0, len(set.Targets)), Verifiable: set.Verifiable}
62 for _, target := range set.Targets {
63 view.Targets = append(view.Targets, ForkTargetView{
64 SourceHostID: set.Source.HostID, SourceSessionID: set.Source.SessionID, SessionGeneration: generation,
65 TurnID: target.TurnID, BoundarySequence: target.BoundarySequence,
66 TurnNumber: target.TurnNumber, Status: string(target.Status), MessageID: target.MessageID,
67 Available: target.Available, Reason: string(target.Reason)})
68 }
69 return view
70 }
71
72 func (a *App) ForkTargetsForTab(tabID string) (ForkTargetSetView, error) {
73 a.mu.RLock()
74 tab := a.tabByIDLocked(tabID)
75 if tab == nil || tab.Ctrl == nil {
76 a.mu.RUnlock()
77 return ForkTargetSetView{Targets: []ForkTargetView{}}, nil
78 }
79 ctrl, generation := tab.Ctrl, tab.SessionGeneration
80 a.mu.RUnlock()
81 targets, ok := ctrl.(forkTargetsController)
82 if !ok {
83 return ForkTargetSetView{Targets: []ForkTargetView{}}, nil
84 }
85 set, err := targets.ForkTargets()
86 if err != nil {
87 return ForkTargetSetView{Targets: []ForkTargetView{}}, err
88 }
89 a.mu.RLock()
90 current := a.tabs[tab.ID]
91 stale := current != tab || current.Ctrl != ctrl || current.SessionGeneration != generation ||
92 (strings.TrimSpace(current.SessionID) != "" && current.SessionID != set.Source.SessionID)
93 a.mu.RUnlock()
94 if stale {
95 return ForkTargetSetView{Targets: []ForkTargetView{}}, &session.ForkUnavailableError{Reason: session.ForkStaleSource}
96 }
97 return forkTargetSetView(set, generation), nil
98 }
99
100 func (a *App) localForkSource(tabID string, anchor ForkAnchorView) (*WorkspaceTab, forkTargetsController, session.SessionRef, error) {
101 a.mu.RLock()
102 defer a.mu.RUnlock()
103 tab := a.tabByIDLocked(tabID)
104 if tab == nil || tab.Ctrl == nil {
105 return nil, nil, session.SessionRef{}, fmt.Errorf("fork source tab is unavailable")
106 }
107 creator, ok := tab.Ctrl.(forkTargetsController)
108 if !ok {
109 return nil, nil, session.SessionRef{}, fmt.Errorf("fork is unsupported")
110 }
111 ref := session.SessionRef{HostID: strings.TrimSpace(anchor.SourceHostID), SessionID: strings.TrimSpace(anchor.SourceSessionID)}
112 if ref.SessionID == "" || tab.SessionID != ref.SessionID || tab.SessionGeneration != anchor.SessionGeneration {
113 return nil, nil, session.SessionRef{}, &session.ForkUnavailableError{TurnID: anchor.TurnID, Reason: session.ForkStaleSource}
114 }
115 return tab, creator, ref, nil
116 }
117
118 func (a *App) CreateForkForTab(tabID string, anchor ForkAnchorView) (ForkCreationView, error) {
119 tab, creator, source, err := a.localForkSource(tabID, anchor)
120 if err != nil {
121 return forkRefusalView(err), nil
122 }
123 operation, err := a.beginForkOperation(forkOperation{Surface: "local", TabID: tab.ID,
124 SourceHostID: source.HostID, SourceSessionID: source.SessionID,
125 TurnID: strings.TrimSpace(anchor.TurnID), BoundarySequence: anchor.BoundarySequence})
126 if err != nil {
127 return ForkCreationView{}, err
128 }
129 childID := operation.ChildSessionID
130 if operation.State != "completed" || childID == "" {
131 // Keep the source tab stable through the controller's source snapshot.
132 // Otherwise a reset could detach it after the App check and let the old
133 // controller create a child for a tab showing another session.
134 a.mu.RLock()
135 current := a.tabs[tab.ID]
136 var boundCreator forkTargetsController
137 controllerMatches := false
138 if current != nil {
139 boundCreator, controllerMatches = current.Ctrl.(forkTargetsController)
140 }
141 if current != tab || !controllerMatches || boundCreator != creator || current.SessionID != source.SessionID ||
142 current.SessionGeneration != anchor.SessionGeneration {
143 a.mu.RUnlock()
144 _ = a.discardForkOperation(operation.OperationID)
145 return forkRefusalView(&session.ForkUnavailableError{TurnID: anchor.TurnID, Reason: session.ForkStaleSource}), nil
146 }
147 childID, err = creator.CreateForkSession(session.ForkRequest{Source: source, TurnID: operation.TurnID,
148 BoundarySequence: operation.BoundarySequence, OperationID: operation.OperationID}, "")
149 a.mu.RUnlock()
150 if err != nil {
151 var unavailable *session.ForkUnavailableError
152 if errors.As(err, &unavailable) {
153 _ = a.discardForkOperation(operation.OperationID)
154 return forkRefusalView(err), nil
155 }
156 return ForkCreationView{}, err
157 }
158 if err := a.completeForkOperation(operation.OperationID, childID); err != nil {
159 return ForkCreationView{}, err
160 }
161 }
162 view := ForkCreationView{SessionID: childID, OperationID: operation.OperationID}
163 a.mu.RLock()
164 for _, existing := range a.tabs {
165 if existing != nil && existing.SessionID == childID {
166 view.TabID, view.Opened = existing.ID, true
167 break
168 }
169 }
170 a.mu.RUnlock()
171 if view.Opened {
172 return view, nil
173 }
174 if err := a.attachForkedDesktopSession(a.bootContext(), tab, childID); err != nil {
175 return ForkCreationView{}, fmt.Errorf("publish fork workspace membership: %w", err)
176 }
177 opened, openErr := a.openForkedSessionTabWithWorkspace(tab, forkedSessionLocator{SessionID: childID}, "")
178 view.TabID = opened.tab.ID
179 if openErr == nil && opened.tab.ID != "" {
180 view.Opened = true
181 return view, nil
182 }
183 if openErr != nil {
184 slog.Warn("fork: child session created but tab attach failed", "session", childID, "err", openErr)
185 }
186 view.Error = rewindForkAttachError
187 return view, nil
188 }
189
190 func forkRefusalView(err error) ForkCreationView {
191 var unavailable *session.ForkUnavailableError
192 if errors.As(err, &unavailable) {
193 return ForkCreationView{Code: "fork_unavailable", Reason: string(unavailable.Reason), Error: unavailable.Error()}
194 }
195 return ForkCreationView{Error: err.Error()}
196 }
197
197 lines GO