返回 DeepSeek-Reasonix
session_history_native_navigation.go
根目录 / desktop / session_history_native_navigation.go
1 package main
2
3 import (
4 "context"
5 "database/sql"
6 "errors"
7 "fmt"
8 "path/filepath"
9 "strings"
10 "unicode"
11
12 "reasonix/internal/agent"
13 "reasonix/internal/session"
14 "reasonix/internal/textutil"
15 )
16
17 func nativeHistorySequence(p *agent.DisplayPager) uint64 {
18 if p.Header.RevisionKnown && p.Header.Revision > 0 {
19 return uint64(p.Header.Revision)
20 }
21 return 0
22 }
23
24 func nativeHistoryEntryID(r *desktopHistoryReader, p *agent.DisplayPager, position int) string {
25 return fmt.Sprintf("s%s:r%d:m%d:o0", strings.TrimSuffix(filepath.Base(r.path), ".jsonl"), nativeHistorySequence(p), position)
26 }
27
28 func nativeNavigationPager(r *desktopHistoryReader) (*agent.DisplayPager, error) {
29 if r.native == nil {
30 return nil, agent.ErrDisplayFormatUnsupported
31 }
32 p, err := r.native.wait(r.ctx)
33 if err != nil {
34 return nil, err
35 }
36 p = p.WithContext(r.ctx)
37 return p, p.Validate()
38 }
39
40 func nativeNavigationStatus(r *desktopHistoryReader, err error) string {
41 if r.ctx.Err() != nil || errors.Is(err, agent.ErrDisplaySourceChanged) {
42 return "stale_cursor"
43 }
44 if errors.Is(err, agent.ErrDisplayFormatUnsupported) {
45 return "unsupported"
46 }
47 return "failed"
48 }
49
50 func nativeHistoryCursor(r *desktopHistoryReader, p *agent.DisplayPager, before int) string {
51 return encodeHistorySliceCursor(historySliceCursor{V: 1, Revision: p.Header.Revision, RevKnown: p.Header.RevisionKnown,
52 Digest: p.Header.ContentDigest, Before: before, Source: r.native.key})
53 }
54
55 func nativeHistoryLocation(r *desktopHistoryReader, p *agent.DisplayPager, messageID string, snapshot uint64) (session.MessageLocation, error) {
56 sequence := nativeHistorySequence(p)
57 location := session.MessageLocation{Status: "not_found", MessageID: messageID, Generation: r.native.key, SnapshotSequence: sequence, CoverageSequence: sequence}
58 if snapshot != 0 && snapshot != sequence {
59 location.Status = "stale_cursor"
60 return location, nil
61 }
62 position, sub, legacyRow, ok := parseHistoryEntryID(messageID)
63 // Cold outline identities name the first display row of a provider message.
64 // Require an exact round trip; permissive Sscanf parsing alone is not proof
65 // of this source, rewrite epoch, or an existing display identity.
66 if !ok || sub != 0 || legacyRow != -1 || position < 0 || messageID != nativeHistoryEntryID(r, p, position) {
67 return location, nil
68 }
69 entry, err := p.Entry(position)
70 if errors.Is(err, sql.ErrNoRows) {
71 return location, nil
72 }
73 if err != nil {
74 return location, err
75 }
76 location.Status, location.Position, location.VisibleTurn = "ready", int64(position), entry.AuthoredTurn
77 location.Cursor = nativeHistoryCursor(r, p, position+1)
78 return location, p.Validate()
79 }
80
81 func (a *App) readNativeHistoryOutline(r *desktopHistoryReader, req session.HistoryOutlineRequest) (session.HistoryOutlinePage, error) {
82 page := session.HistoryOutlinePage{Entries: []session.HistoryOutlineEntry{}}
83 p, err := nativeNavigationPager(r)
84 if err != nil {
85 page.Status = nativeNavigationStatus(r, err)
86 if page.Status != "failed" {
87 err = nil
88 }
89 return page, err
90 }
91 page.Generation, page.SnapshotSequence, page.CoverageSequence = r.native.key, nativeHistorySequence(p), nativeHistorySequence(p)
92 page.TotalTurns, page.NextTurn = p.Header.AuthoredTurns, max(req.StartTurn, 1)
93 if req.Generation != "" && req.Generation != page.Generation || req.SnapshotSequence != nil && *req.SnapshotSequence != page.SnapshotSequence {
94 page.Status = "stale_cursor"
95 return page, nil
96 }
97 limit := req.Limit
98 if limit <= 0 {
99 limit = 128
100 }
101 entries, err := p.TurnEntries(page.NextTurn, min(limit, 1000))
102 if err != nil {
103 return page, err
104 }
105 for _, entry := range entries {
106 // Only requested prompt positions are read, one at a time. Never retain
107 // the entire transcript or decode tool/attachment records between turns.
108 var prompt string
109 if p.DAG || p.SchemaOne {
110 messages, readErr := p.EventMessages(entry.Index, entry.Index+1)
111 err = readErr
112 if err == nil && len(messages) == 1 {
113 prompt = agent.UserMessageText(messages[0])
114 }
115 } else {
116 messages, readErr := readSessionMessagesAtOffsetsContext(r.ctx, r.path, []agent.DisplayIndexEntry{entry})
117 err = readErr
118 if err == nil && len(messages) == 1 {
119 prompt = agent.UserMessageText(messages[0])
120 }
121 }
122 if err != nil {
123 return page, err
124 }
125 prompt, err = nativeHistoryPrompt(r.ctx, prompt)
126 if err != nil {
127 return page, err
128 }
129 page.Entries = append(page.Entries, session.HistoryOutlineEntry{MessageID: nativeHistoryEntryID(r, p, entry.Index), Turn: entry.AuthoredTurn, Position: int64(entry.Index), Prompt: prompt})
130 page.NextTurn = entry.AuthoredTurn + 1
131 }
132 if err := p.Validate(); err != nil {
133 return session.HistoryOutlinePage{Entries: []session.HistoryOutlineEntry{}, Status: nativeNavigationStatus(r, err)}, err
134 }
135 page.Status, page.Done = "ready", page.NextTurn > page.TotalTurns
136 return page, nil
137 }
138
139 // Normalize only a bounded prefix. strings.Fields over a multi-megabyte
140 // prompt would allocate an array for every word before clipping the preview.
141 func nativeHistoryPrompt(ctx context.Context, text string) (string, error) {
142 var out strings.Builder
143 space, count, nextCheck := false, 0, 0
144 for offset, ch := range text {
145 if offset >= nextCheck {
146 if err := ctx.Err(); err != nil {
147 return "", err
148 }
149 nextCheck = offset + (64 << 10)
150 }
151 if unicode.IsSpace(ch) {
152 space = out.Len() > 0
153 continue
154 }
155 if space {
156 out.WriteByte(' ')
157 space = false
158 }
159 out.WriteRune(ch)
160 count++
161 if count >= 512 {
162 out.WriteString("…")
163 break
164 }
165 }
166 return textutil.ClipGraphemes(out.String(), 50, "…"), ctx.Err()
167 }
168
169 // Resolve a direct jump using the same generation as paging and outline.
170 // Cursor navigation never walks pages from the newest end to find a target.
171 func nativeHistorySliceAnchor(r *desktopHistoryReader, p *agent.DisplayPager, req HistorySliceRequest) (HistorySliceRequest, string, error) {
172 if err := p.WithContext(r.ctx).Validate(); err != nil {
173 return req, nativeNavigationStatus(r, err), err
174 }
175 if req.Generation != "" && req.Generation != r.native.key || req.SnapshotSequence != nil && *req.SnapshotSequence != nativeHistorySequence(p) {
176 return req, "stale_cursor", nil
177 }
178 switch req.Anchor {
179 case "", "newest", "cursor":
180 return req, "ready", nil
181 case "turn":
182 entries, err := p.WithContext(r.ctx).TurnEntries(max(req.Turn, 1), 1)
183 if err != nil {
184 return req, "failed", err
185 }
186 if len(entries) == 0 || entries[0].AuthoredTurn != max(req.Turn, 1) {
187 return req, "not_found", nil
188 }
189 req.Cursor = nativeHistoryCursor(r, p, entries[0].Index+1)
190 case "message":
191 location, err := nativeHistoryLocation(r, p.WithContext(r.ctx), req.MessageID, 0)
192 if err != nil || location.Status != "ready" {
193 return req, location.Status, err
194 }
195 req.Cursor = location.Cursor
196 default:
197 return req, "unsupported", nil
198 }
199 req.Newer = false
200 return req, "ready", nil
201 }
202
202 lines GO