返回 DeepSeek-Reasonix
topic_archive_runtime.go
根目录 / desktop / topic_archive_runtime.go
1 package main
2
3 import (
4 "errors"
5 "log/slog"
6
7 "reasonix/internal/agent"
8 )
9
10 func (a *App) captureTopicRuntimeBindings(topicID string) []removedSessionRuntime {
11 a.mu.RLock()
12 defer a.mu.RUnlock()
13 var captured []removedSessionRuntime
14 for _, tabs := range []map[string]*WorkspaceTab{a.tabs, a.detachedSessions} {
15 for _, tab := range tabs {
16 if tab == nil || tab.TopicID != topicID {
17 continue
18 }
19 item := removedRuntimeFromTab(tab, tabRuntimeSessionDir(tab), canonicalTabSessionPath(tab.currentSessionPath()))
20 item.failedStartup = a.suppressTabStartupRestoreLocked(tab)
21 captured = append(captured, item)
22 }
23 }
24 return captured
25 }
26
27 // snapshotTopicRuntimeBindings keeps bindings usable until every writable
28 // controller snapshots. The caller owns runtime admission, blocking new turns.
29 func (a *App) snapshotTopicRuntimeBindings(captured []removedSessionRuntime) error {
30 for _, item := range captured {
31 if item.ctrl == nil || item.readOnly {
32 continue
33 }
34 if item.ctrl.Running() {
35 return errTopicHasActiveWork
36 }
37 if err := item.ctrl.Snapshot(); err != nil {
38 if item.failedStartup && failedStartupSnapshotError(err) {
39 slog.Warn("desktop: skipping unavailable failed runtime snapshot before removing topic")
40 continue
41 }
42 if !errors.Is(err, agent.ErrSessionSnapshotConflict) {
43 return err
44 }
45 kind, _ := agent.SnapshotConflictKind(err)
46 slog.Warn("desktop: skipping stale runtime snapshot before removing topic", "conflict_kind", kind)
47 }
48 }
49 return nil
50 }
51
52 func (a *App) topicRuntimeBindingsMatchLocked(tabs map[string]*WorkspaceTab, topicID string, expected map[*WorkspaceTab]removedSessionRuntime) (int, bool) {
53 matched := 0
54 for _, tab := range tabs {
55 if tab == nil || tab.TopicID != topicID {
56 continue
57 }
58 item, ok := expected[tab]
59 if !ok || tab.Ctrl != item.ctrl || tabRuntimeSessionDir(tab) != item.sessionDir ||
60 canonicalTabSessionPath(tab.currentSessionPath()) != item.sessionPath ||
61 a.suppressTabStartupRestoreLocked(tab) != item.failedStartup {
62 return 0, false
63 }
64 matched++
65 }
66 return matched, true
67 }
68
69 // removeTopicRuntimeBindingsIfUnchanged commits only when the captured runtime
70 // generation still matches, so stale completion cannot remove a rebound tab.
71 func (a *App) removeTopicRuntimeBindingsIfUnchanged(topicID string, captured []removedSessionRuntime) (fallbackRuntimeTarget, bool) {
72 expected := make(map[*WorkspaceTab]removedSessionRuntime, len(captured))
73 for _, item := range captured {
74 expected[item.tab] = item
75 }
76 a.mu.Lock()
77 visible, visibleOK := a.topicRuntimeBindingsMatchLocked(a.tabs, topicID, expected)
78 detached, detachedOK := a.topicRuntimeBindingsMatchLocked(a.detachedSessions, topicID, expected)
79 if !visibleOK || !detachedOK || visible+detached != len(captured) {
80 a.mu.Unlock()
81 return fallbackRuntimeTarget{}, false
82 }
83 var fallback fallbackRuntimeTarget
84 fallbackSet := false
85 for id, tab := range a.tabs {
86 if _, ok := expected[tab]; !ok {
87 continue
88 }
89 if !fallbackSet {
90 fallback = fallbackRuntimeTarget{scope: tab.Scope, workspaceRoot: tab.WorkspaceRoot}
91 fallbackSet = true
92 }
93 a.markTabRemovedLocked(tab)
94 delete(a.tabs, id)
95 a.removeTabOrderLocked(id)
96 if a.activeTabID == id {
97 a.activeTabID = ""
98 }
99 }
100 for key, tab := range a.detachedSessions {
101 if _, ok := expected[tab]; !ok {
102 continue
103 }
104 if !fallbackSet {
105 fallback = fallbackRuntimeTarget{scope: tab.Scope, workspaceRoot: tab.WorkspaceRoot}
106 fallbackSet = true
107 }
108 a.markTabRemovedLocked(tab)
109 delete(a.detachedSessions, key)
110 }
111 if a.activeTabID == "" && len(a.tabOrder) > 0 {
112 a.activeTabID = a.tabOrder[0]
113 }
114 fallback.needs = len(captured) > 0 && len(a.tabs) == 0
115 dir, entries, activeID, version := a.saveTabsCollectLocked()
116 a.mu.Unlock()
117 a.saveTabsWrite(dir, entries, activeID, version)
118 return fallback, true
119 }
120
121 func (a *App) finalizeRemovedTopicRuntimes(removed []removedSessionRuntime) {
122 for _, item := range removed {
123 if item.sink != nil {
124 item.sink.clearContext()
125 }
126 if item.ctrl == nil || item.readOnly {
127 continue
128 }
129 item.ctrl.SetSessionPath("")
130 a.quiesceTabAutosave(item.tab)
131 }
132 }
133
134 func (a *App) tryLockRuntimeMutation(operation string) (func(), bool) {
135 if hook := a.runtimeMutationBeforeLockHook; hook != nil {
136 hook(operation)
137 }
138 if !a.runtimeRebuildMu.TryLock() {
139 return nil, false
140 }
141 if !a.runtimeAdmissionMu.TryLock() {
142 a.runtimeRebuildMu.Unlock()
143 return nil, false
144 }
145 return func() {
146 a.runtimeAdmissionMu.Unlock()
147 a.runtimeRebuildMu.Unlock()
148 }, true
149 }
150
150 lines GO