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