返回 DeepSeek-Reasonix
session_history_service.go
根目录 / desktop / session_history_service.go
1 package main
2
3 import (
4 "context"
5 "encoding/base64"
6 "errors"
7 "fmt"
8 "strings"
9
10 "reasonix/internal/control"
11 "reasonix/internal/session"
12 "reasonix/internal/sessioncontent"
13 )
14
15 const sessionHistoryContentChunkBytes = 1 << 20
16
17 // SessionHistoryOutlineForTab reads durable navigation metadata before runtime startup.
18 func (a *App) SessionHistoryOutlineForTab(tabID string, req session.HistoryOutlineRequest) (session.HistoryOutlinePage, error) {
19 query, ref, err := a.canonicalSessionQuery(tabID)
20 if err != nil {
21 return session.HistoryOutlinePage{Entries: []session.HistoryOutlineEntry{}}, err
22 }
23 return query.ReadHistoryOutline(context.Background(), ref, req)
24 }
25
26 // SessionHistoryContentChunk is one bounded binary chunk from a canonical
27 // content reference. Data is base64 so the desktop JSON contract never
28 // converts arbitrary attachment bytes through UTF-8 strings.
29 type SessionHistoryContentChunk struct {
30 Data string `json:"data"`
31 NextOffset int64 `json:"nextOffset"`
32 Done bool `json:"done"`
33 }
34
35 // SessionHistoryPageForTab is the canonical v4 history endpoint. Unlike the
36 // compatibility HistorySlice API, it is identity based and obtains its fixed
37 // snapshot directly from the shared session read model.
38 func (a *App) SessionHistoryPageForTab(tabID, cursor string, limit int) (session.MessageHistoryPage, error) {
39 query, ref, err := a.canonicalSessionQuery(tabID)
40 if err != nil {
41 return session.MessageHistoryPage{}, err
42 }
43 return query.HistoryPage(context.Background(), ref, cursor, limit)
44 }
45
46 // SessionHistoryPageForTarget reads a cold or live canonical session by its
47 // explicit durable identity. It never stages the session into a tab.
48 func (a *App) SessionHistoryPageForTarget(selector SessionSelector, cursor string, limit int) (session.MessageHistoryPage, error) {
49 target, err := a.resolveSessionTargetWithArchived(selector, true)
50 if err != nil {
51 return session.MessageHistoryPage{}, err
52 }
53 if target.SessionRef.SessionID == "" {
54 return session.MessageHistoryPage{}, newSessionOperationError("unsupported", "This historical session uses the legacy history reader.")
55 }
56 return a.desktopSessionService("").Query().HistoryPage(context.Background(), target.SessionRef, cursor, limit)
57 }
58
59 // SessionOpenForTab returns the bounded recent baseline and independent
60 // preparation states without consulting either SQLite projection.
61 func (a *App) SessionOpenForTab(tabID string) (session.SessionOpenView, error) {
62 query, ref, err := a.canonicalSessionQuery(tabID)
63 if err != nil {
64 return session.SessionOpenView{}, err
65 }
66 return query.OpenSession(context.Background(), ref)
67 }
68
69 // canonicalTabHistoryFingerprint returns the same identity emitted by the
70 // canonical history-window endpoint. Branch metadata describes the legacy
71 // JSONL projection and must never be compared with canonical session pages.
72 //
73 // tabMeta calls this while holding App.mu, so this helper deliberately avoids
74 // canonicalSessionQuery (which would reacquire App.mu).
75 func (a *App) canonicalTabHistoryFingerprint(tab *WorkspaceTab) (int64, string, bool) {
76 if tab == nil || strings.TrimSpace(tab.SessionID) == "" {
77 return 0, "", false
78 }
79 if identity, ok := tab.Ctrl.(control.IdentityLifecycle); ok && identity.UsesExclusiveSession() {
80 if boundRef, bound := identity.SessionRef(); bound {
81 if service := identity.SessionService(); service != nil {
82 if runtime, live := service.Runtime(boundRef); live {
83 recent := runtime.Session().RecentSnapshot()
84 return int64(recent.DurableSequence), recent.StorageGeneration, recent.StorageGeneration != ""
85 }
86 }
87 }
88 }
89 // Cold identities are validated by the off-lock reader. Tab metadata must
90 // never open a service, read a snapshot, or probe a filesystem under App.mu.
91 return 0, "", false
92 }
93
94 func (a *App) tabHistoryFingerprint(tab *WorkspaceTab, sessionPath string) (int64, string) {
95 if revision, digest, ok := a.canonicalTabHistoryFingerprint(tab); ok {
96 return revision, digest
97 }
98 if tab != nil && strings.TrimSpace(tab.SessionID) == "" {
99 if ctrl, ok := tab.Ctrl.(historyWindowController); ok {
100 if state, known := ctrl.SessionPersistedState(); known {
101 return state.Revision, state.DigestHex
102 }
103 }
104 }
105 return 0, ""
106 }
107
108 func (a *App) SearchSessionHistoryForTab(tabID, textQuery, cursor string, limit int) (session.SearchHistoryPage, error) {
109 query, ref, err := a.canonicalSessionQuery(tabID)
110 if err != nil {
111 return session.SearchHistoryPage{}, err
112 }
113 return query.SearchHistory(context.Background(), ref, textQuery, cursor, limit)
114 }
115
116 // SearchSessionHistoryForTarget searches one explicit canonical session
117 // without consulting the active tab.
118 func (a *App) SearchSessionHistoryForTarget(selector SessionSelector, textQuery, cursor string, limit int) (session.SearchHistoryPage, error) {
119 target, err := a.resolveSessionTargetWithArchived(selector, true)
120 if err != nil {
121 return session.SearchHistoryPage{}, err
122 }
123 if target.SessionRef.SessionID == "" {
124 return session.SearchHistoryPage{}, newSessionOperationError("unsupported", "This historical session uses the legacy search index.")
125 }
126 return a.desktopSessionService("").Query().SearchHistory(context.Background(), target.SessionRef, textQuery, cursor, limit)
127 }
128
129 func (a *App) LocateSessionMessageForTab(tabID, messageID string, snapshot uint64) (session.MessageLocation, error) {
130 query, ref, err := a.canonicalSessionQuery(tabID)
131 if err != nil {
132 return session.MessageLocation{}, err
133 }
134 return query.LocateMessage(context.Background(), ref, messageID, snapshot)
135 }
136
137 // LocateSessionMessageForTarget resolves one canonical message against the
138 // explicit durable target. The active tab is deliberately irrelevant.
139 func (a *App) LocateSessionMessageForTarget(selector SessionSelector, messageID string, snapshot uint64) (session.MessageLocation, error) {
140 target, err := a.resolveSessionTargetWithArchived(selector, true)
141 if err != nil {
142 return session.MessageLocation{}, err
143 }
144 if target.SessionRef.SessionID == "" {
145 return session.MessageLocation{}, newSessionOperationError("unsupported", "This historical session uses the legacy history reader.")
146 }
147 return a.desktopSessionService("").Query().LocateMessage(context.Background(), target.SessionRef, messageID, snapshot)
148 }
149
150 // SessionHistoryContentForTab reads the next bounded chunk only after Query
151 // proves that the reference belongs to this session's durable view.
152 func (a *App) SessionHistoryContentForTab(tabID string, ref sessioncontent.Ref, offset int64) (SessionHistoryContentChunk, error) {
153 query, sessionRef, err := a.canonicalSessionQuery(tabID)
154 if err != nil {
155 return SessionHistoryContentChunk{}, err
156 }
157 return readSessionHistoryContent(query, sessionRef, ref, offset)
158 }
159
160 // SessionHistoryContentForTarget reads a content capability against one
161 // explicit canonical target without opening or selecting it.
162 func (a *App) SessionHistoryContentForTarget(selector SessionSelector, ref sessioncontent.Ref, offset int64) (SessionHistoryContentChunk, error) {
163 target, err := a.resolveSessionTargetWithArchived(selector, true)
164 if err != nil {
165 return SessionHistoryContentChunk{}, err
166 }
167 if target.SessionRef.SessionID == "" {
168 return SessionHistoryContentChunk{}, newSessionOperationError("unsupported", "This historical session uses the legacy history reader.")
169 }
170 return readSessionHistoryContent(a.desktopSessionService("").Query(), target.SessionRef, ref, offset)
171 }
172
173 func readSessionHistoryContent(query *session.Query, sessionRef session.SessionRef, ref sessioncontent.Ref, offset int64) (SessionHistoryContentChunk, error) {
174 if offset < 0 || offset > ref.Bytes {
175 return SessionHistoryContentChunk{}, errors.New("invalid session history content offset")
176 }
177 if offset == ref.Bytes {
178 return SessionHistoryContentChunk{NextOffset: offset, Done: true}, nil
179 }
180 length := min(int64(sessionHistoryContentChunkBytes), ref.Bytes-offset)
181 data, err := query.ReadContent(context.Background(), sessionRef, ref, offset, length)
182 if err != nil {
183 return SessionHistoryContentChunk{}, err
184 }
185 next := offset + int64(len(data))
186 return SessionHistoryContentChunk{Data: base64.StdEncoding.EncodeToString(data), NextOffset: next, Done: next == ref.Bytes}, nil
187 }
188
189 // SessionHistoryWindowForTab pages a bounded window around an anchor
190 // (newest/message/turn/cursor) in either direction — the history-window-v1
191 // capability. Anchors resolve through the locator index without walking pages.
192 func (a *App) SessionHistoryWindowForTab(tabID string, req session.HistoryWindowRequest) (session.HistoryWindowPage, error) {
193 query, ref, err := a.canonicalSessionQuery(tabID)
194 if err != nil {
195 return session.HistoryWindowPage{}, err
196 }
197 return query.ReadHistoryWindow(context.Background(), ref, req)
198 }
199
200 // SessionHistoryWindowForTarget resolves a fixed-snapshot window for one
201 // explicit canonical target and does not alter the visible transcript.
202 func (a *App) SessionHistoryWindowForTarget(selector SessionSelector, req session.HistoryWindowRequest) (session.HistoryWindowPage, error) {
203 target, err := a.resolveSessionTargetWithArchived(selector, true)
204 if err != nil {
205 return session.HistoryWindowPage{}, err
206 }
207 if target.SessionRef.SessionID == "" {
208 return session.HistoryWindowPage{}, newSessionOperationError("unsupported", "This historical session uses the legacy history reader.")
209 }
210 return a.desktopSessionService("").Query().ReadHistoryWindow(context.Background(), target.SessionRef, req)
211 }
212
213 // SessionMessageFieldForTab returns one bounded fragment of one top-level
214 // message field. Credentials issued when a window or page displayed the
215 // message authorize the read.
216 func (a *App) SessionMessageFieldForTab(tabID, messageID string, version int, field string, offset, length int64) (session.MessageFieldPage, error) {
217 query, ref, err := a.canonicalSessionQuery(tabID)
218 if err != nil {
219 return session.MessageFieldPage{}, err
220 }
221 return query.ReadMessageField(context.Background(), ref, messageID, version, field, offset, length)
222 }
223
224 // SessionMessageFieldForTarget reads one bounded field fragment from an
225 // explicit canonical target without consulting the active runtime.
226 func (a *App) SessionMessageFieldForTarget(selector SessionSelector, messageID string, version int, field string, offset, length int64) (session.MessageFieldPage, error) {
227 target, err := a.resolveSessionTargetWithArchived(selector, true)
228 if err != nil {
229 return session.MessageFieldPage{}, err
230 }
231 if target.SessionRef.SessionID == "" {
232 return session.MessageFieldPage{}, newSessionOperationError("unsupported", "This historical session uses the legacy history reader.")
233 }
234 return a.desktopSessionService("").Query().ReadMessageField(
235 context.Background(),
236 target.SessionRef,
237 messageID,
238 version,
239 field,
240 offset,
241 length,
242 )
243 }
244
245 func (a *App) canonicalSessionQuery(tabID string) (*session.Query, session.SessionRef, error) {
246 a.mu.RLock()
247 tab := a.tabByIDLocked(tabID)
248 var ctrl control.SessionAPI
249 var sessionID, sessionDir string
250 if tab != nil {
251 ctrl = tab.Ctrl
252 sessionID = tab.SessionID
253 sessionDir = tabSessionDir(tab)
254 }
255 a.mu.RUnlock()
256 if ctrl == nil {
257 if tab == nil {
258 return nil, session.SessionRef{}, fmt.Errorf("tab %q is not ready", tabID)
259 }
260 if sessionID == "" {
261 return nil, session.SessionRef{}, errors.New("canonical session identity is unavailable")
262 }
263 service := a.desktopSessionService(sessionDir)
264 if service == nil || service.Query() == nil {
265 return nil, session.SessionRef{}, errors.New("canonical session history is unavailable")
266 }
267 return service.Query(), session.SessionRef{HostID: service.HostID(), SessionID: sessionID}, nil
268 }
269 identity, ok := ctrl.(control.IdentityLifecycle)
270 if !ok || !identity.UsesExclusiveSession() {
271 return nil, session.SessionRef{}, errors.New("canonical session history is unavailable")
272 }
273 ref, bound := identity.SessionRef()
274 service := identity.SessionService()
275 if !bound || service == nil || service.Query() == nil {
276 return nil, session.SessionRef{}, errors.New("canonical session identity is unavailable")
277 }
278 return service.Query(), ref, nil
279 }
280
280 lines GO