返回 DeepSeek-Reasonix
session_lease_handoff.go
根目录 / desktop / session_lease_handoff.go
1 package main
2
3 import (
4 "errors"
5 "fmt"
6 "log/slog"
7
8 "reasonix/internal/agent"
9 "reasonix/internal/control"
10 )
11
12 // handoffSessionLease acquires path and publishes it on the tab without
13 // releasing the previous lease. Recovery callbacks use the returned lease to
14 // retire the old path only after the current authority-guarded save returns.
15 func (t *WorkspaceTab) handoffSessionLease(path string) (*agent.SessionLease, error) {
16 if t == nil || t.ReadOnly {
17 return nil, nil
18 }
19 legacyPath, ok, err := legacySessionPathForFileAccess(path)
20 if err != nil {
21 return nil, err
22 }
23 if !ok {
24 return nil, nil
25 }
26 key := sessionRuntimeKey(string(legacyPath))
27 t.sessionLeaseMu.Lock()
28 if t.sessionLease != nil && sessionRuntimeKey(t.sessionLease.Path()) == key {
29 t.storeSessionLeaseRuntimeKey(key)
30 t.sessionLeaseMu.Unlock()
31 return nil, nil
32 }
33 lease, err := agent.TryAcquireSessionLease(string(legacyPath))
34 if err != nil {
35 t.sessionLeaseMu.Unlock()
36 return nil, err
37 }
38 if hook := sessionLeaseAcquireHookForTest; hook != nil {
39 hook()
40 }
41 old := t.sessionLease
42 t.sessionLease = lease
43 t.storeSessionLeaseRuntimeKey(key)
44 t.sessionLeaseMu.Unlock()
45 return old, nil
46 }
47
48 func (t *WorkspaceTab) swapSessionLease(lease *agent.SessionLease) *agent.SessionLease {
49 if t == nil {
50 return lease
51 }
52 t.sessionLeaseMu.Lock()
53 old := t.sessionLease
54 t.sessionLease = lease
55 key := ""
56 if lease != nil {
57 key = sessionRuntimeKey(lease.Path())
58 }
59 t.storeSessionLeaseRuntimeKey(key)
60 t.sessionLeaseMu.Unlock()
61 return old
62 }
63
64 func (a *App) handoffTabRecoveryLease(tab *WorkspaceTab, recoveryPath string) error {
65 if tab == nil || tab.ReadOnly {
66 return nil
67 }
68 transition, err := a.reserveSessionRuntimePath(tab, recoveryPath)
69 if err != nil {
70 return fmt.Errorf("acquire recovery session lease: %w", userFacingSessionLeaseError("", err))
71 }
72 oldLease, err := tab.handoffSessionLease(recoveryPath)
73 if err != nil {
74 a.rollbackSessionRuntimePath(transition)
75 slog.Warn("desktop: acquire recovery session lease", "path", recoveryPath, "err", err)
76 reason := "lease_unavailable"
77 if errors.Is(err, agent.ErrSessionLeaseHeld) {
78 reason = "lease_held"
79 }
80 _ = agent.UpdateBranchMeta(recoveryPath, false, func(meta *agent.BranchMeta) error {
81 meta.VersionKind = agent.VersionRecovery
82 meta.VersionState = agent.VersionPending
83 return nil
84 })
85 a.emitRuntimeEvent("session:recovery-failed", sessionRecoveryFailedEvent{
86 Reason: reason, ConversationID: tab.TopicID, TopicID: tab.TopicID,
87 RecoveryPath: recoveryPath,
88 WorkspaceRoot: tab.WorkspaceRoot,
89 CanContinue: false, RecoveryPending: true,
90 })
91 return fmt.Errorf("acquire recovery session lease: %w", userFacingSessionLeaseError("", err))
92 }
93 if err := bindTabWriteAuthority(tab, tab.Ctrl); err != nil {
94 newLease := tab.swapSessionLease(oldLease)
95 _ = bindTabWriteAuthority(tab, tab.Ctrl)
96 a.rollbackSessionRuntimePath(transition)
97 if newLease != nil {
98 newLease.Release()
99 }
100 return fmt.Errorf("bind recovery session authority: %w", err)
101 }
102 a.commitSessionRuntimePath(transition)
103 if oldLease != nil {
104 go oldLease.Release()
105 }
106 return nil
107 }
108
109 // handleTabSessionTransition moves a tab's lease and binds the unpublished
110 // target Session before its controller switches paths. The source controller
111 // remains fully usable when any acquisition or bind step fails.
112 func (a *App) handleTabSessionTransition(tab *WorkspaceTab) func(control.SessionTransitionInfo) error {
113 return func(info control.SessionTransitionInfo) error {
114 if tab == nil || tab.ReadOnly {
115 return nil
116 }
117 if info.Reason == "fork" || info.Reason == "branch" {
118 if err := copyPinnedContextState(info.OriginalPath, info.TargetPath); err != nil {
119 return fmt.Errorf("copy pinned context to %s: %w", info.Reason, err)
120 }
121 }
122 pinnedState, err := loadPinnedContextState(info.TargetPath)
123 if err != nil {
124 return fmt.Errorf("load target pinned context: %w", err)
125 }
126 transition, err := a.reserveSessionRuntimePath(tab, info.TargetPath)
127 if err != nil {
128 return fmt.Errorf("acquire target session lease: %w", userFacingSessionLeaseError("", err))
129 }
130 oldLease, err := tab.handoffSessionLease(info.TargetPath)
131 if err != nil {
132 a.rollbackSessionRuntimePath(transition)
133 return fmt.Errorf("acquire target session lease: %w", userFacingSessionLeaseError("", err))
134 }
135 tab.sessionLeaseMu.Lock()
136 lease := tab.sessionLease
137 tab.sessionLeaseMu.Unlock()
138 if err := info.BindWriteAuthority(lease); err != nil {
139 newLease := tab.swapSessionLease(oldLease)
140 a.rollbackSessionRuntimePath(transition)
141 if newLease != nil {
142 newLease.Release()
143 }
144 return fmt.Errorf("bind target session authority: %w", err)
145 }
146 a.mu.Lock()
147 if tab.removed || !a.runtimeOwnerLiveLocked(transition.runtime) || !a.commitSessionRuntimePathLocked(transition) {
148 a.mu.Unlock()
149 newLease := tab.swapSessionLease(oldLease)
150 a.rollbackSessionRuntimePath(transition)
151 if newLease != nil {
152 newLease.Release()
153 }
154 return fmt.Errorf("bind target session: tab runtime changed; retry")
155 }
156 tab.SessionPath = canonicalTabSessionPath(info.TargetPath)
157 if a.tabs[tab.ID] == tab {
158 a.saveTabsLocked()
159 }
160 a.mu.Unlock()
161 if oldLease != nil {
162 go oldLease.Release()
163 }
164 info.OnCommit(func() {
165 tab.setPinnedFiles(pinnedState.Files)
166 a.emitRuntimeEvent(tabMetaRefreshEventChannel, TabMetaRefreshEvent{TabID: tab.ID, Meta: a.MetaForTab(tab.ID)})
167 })
168 a.emitProjectTreeChangedForSessionDirs(sessionDirectoryForPath(info.TargetPath))
169 return nil
170 }
171 }
172
172 lines GO