返回 DeepSeek-Reasonix
snapshot.go
根目录 / internal / transcript / snapshot.go
1 package transcript
2
3 import (
4 "encoding/json"
5 "errors"
6 "reflect"
7 "unicode/utf8"
8 )
9
10 const (
11 // MaxResponseBytes includes the complete encoded response and its newline.
12 MaxResponseBytes = 2 << 20
13 defaultPageRecords = 120
14 maxPageRecords = 1000
15 defaultPageBytes = 512 << 10
16 maxPageBytes = MaxResponseBytes
17 inlineFieldBytes = 64 << 10
18 previewBytes = 4 << 10
19 contentChunkBytes = 64 << 10
20 )
21
22 type PageRequest struct {
23 SnapshotID string `json:"snapshotId"`
24 Before int `json:"before"`
25 Records int `json:"records"`
26 Bytes int `json:"bytes"`
27 }
28
29 type ContentRef struct {
30 SnapshotID string `json:"snapshotId"`
31 RecordID string `json:"recordId"`
32 Path []string `json:"path"`
33 Bytes int `json:"bytes"`
34 }
35
36 type Record struct {
37 ID string `json:"id"`
38 Order int `json:"order"`
39 Message Message `json:"message"`
40 Refs []ContentRef `json:"refs"`
41 }
42
43 type Snapshot struct {
44 Boundary
45 Records []Record `json:"records"`
46 Runtime Runtime `json:"runtime"`
47 ActiveAttempts []ActiveAttempt `json:"activeAttempts"`
48 ActiveRecords []Record `json:"activeRecords"`
49 Before int `json:"before"`
50 HasOlder bool `json:"hasOlder"`
51 TotalRecords int `json:"totalRecords"`
52 TotalTurns int `json:"totalTurns"`
53 Stale bool `json:"stale"`
54 }
55
56 type ContentRequest struct {
57 ContentRef
58 Offset int `json:"offset"`
59 }
60
61 type ContentChunk struct {
62 Data string `json:"data"`
63 NextOffset int `json:"nextOffset"`
64 Done bool `json:"done"`
65 Stale bool `json:"stale"`
66 }
67
68 // Snapshot returns a bounded immutable window. Pages and content refs are
69 // bound to this exact revision. A bounded cache retains immutable cuts while
70 // streaming continues; an evicted cut returns Stale rather than mixed history.
71 func (p *Projection) Snapshot(req PageRequest) (Snapshot, error) {
72 p.mu.Lock()
73 defer p.mu.Unlock()
74 frozen, err := p.freezeLocked(req.SnapshotID)
75 if err != nil {
76 return Snapshot{}, err
77 }
78 if frozen == nil {
79 return Snapshot{Boundary: p.boundaryLocked(), Stale: true}, nil
80 }
81 return frozen.snapshotCurrent(req)
82 }
83
84 func (p *Projection) snapshotCurrent(req PageRequest) (Snapshot, error) {
85 out := Snapshot{Boundary: p.boundaryLocked(), Records: []Record{}, ActiveAttempts: []ActiveAttempt{}, ActiveRecords: []Record{}}
86 if req.SnapshotID != "" && req.SnapshotID != out.SnapshotID {
87 out.Stale = true
88 return out, nil
89 }
90 limit := req.Records
91 if limit <= 0 {
92 limit = defaultPageRecords
93 }
94 limit = min(limit, maxPageRecords)
95 budget := req.Bytes
96 if budget <= 0 {
97 budget = defaultPageBytes
98 }
99 budget = min(budget, maxPageBytes)
100 end := len(p.buffer.messages)
101 if req.SnapshotID != "" {
102 end = min(max(req.Before, 0), end)
103 }
104 out.TotalRecords = len(p.buffer.messages)
105 out.TotalTurns = p.buffer.userTurns
106 used := 0
107 for i := end - 1; i >= 0 && len(out.Records) < limit; i-- {
108 row, err := boundedRecord(p.buffer.messages[i].materialize(), out.SnapshotID)
109 if err != nil {
110 return Snapshot{}, err
111 }
112 row.Order = i
113 encoded, err := json.Marshal(row)
114 if err != nil {
115 return Snapshot{}, err
116 }
117 if len(encoded) > maxPageBytes {
118 return Snapshot{}, errors.New("transcript record metadata exceeds the page limit")
119 }
120 if len(out.Records) > 0 && used+len(encoded) > budget {
121 break
122 }
123 out.Records = append(out.Records, row)
124 used += len(encoded)
125 }
126 for i, j := 0, len(out.Records)-1; i < j; i, j = i+1, j-1 {
127 out.Records[i], out.Records[j] = out.Records[j], out.Records[i]
128 }
129 out.Before = end - len(out.Records)
130 out.HasOlder = out.Before > 0
131 runtime, attempts := p.runtimeLocked()
132 // Detach nested prompt pointers before releasing the mutex.
133 encoded, err := json.Marshal(runtime)
134 if err != nil {
135 return Snapshot{}, err
136 }
137 if err = json.Unmarshal(encoded, &out.Runtime); err != nil {
138 return Snapshot{}, err
139 }
140 out.ActiveAttempts = attempts
141 // A running assistant can precede a large batch of tool result rows. Keep
142 // every still-mutable owner in the same snapshot cut so a later delta can
143 // never be appended to an unloaded prefix.
144 {
145 present := make(map[string]struct{}, len(out.Records))
146 for _, record := range out.Records {
147 present[record.ID] = struct{}{}
148 }
149 for _, i := range activeRecordIndexes(p.buffer.messages, out.Before, runtime) {
150 m := p.buffer.messages[i].materialize()
151 active := (!runtime.Status.Terminal() && m.Pending) || (m.Role == "user" && m.TurnID == runtime.TurnID && runtime.TurnID != "")
152 for _, call := range m.ToolCalls {
153 active = active || (!runtime.Status.Terminal() && call.Pending)
154 }
155 if !active {
156 continue
157 }
158 row, err := boundedRecord(m, out.SnapshotID)
159 if err != nil {
160 return Snapshot{}, err
161 }
162 row.Order = i
163 if _, alreadyPresent := present[row.ID]; alreadyPresent {
164 continue
165 }
166 present[row.ID] = struct{}{}
167 out.ActiveRecords = append(out.ActiveRecords, row)
168 }
169 }
170 encoded, err = json.Marshal(out)
171 if err != nil {
172 return Snapshot{}, err
173 }
174 if len(encoded)+1 > maxPageBytes {
175 return Snapshot{}, errors.New("transcript snapshot metadata exceeds the page limit")
176 }
177 return out, nil
178 }
179
180 // activeRecordIndexes walks only the current mutable turn. Older settled
181 // history cannot gain a streamed suffix, so scanning it on every page request
182 // only extends the projection mutex hold time with the total transcript size.
183 // A missing runtime turn ID keeps the conservative full-prefix behavior for
184 // legacy baselines that do not carry turn identity.
185 func activeRecordIndexes(messages []*bufferedMessage, before int, runtime Runtime) []int {
186 if runtime.Status.Terminal() {
187 return nil
188 }
189 if runtime.TurnID == "" {
190 indexes := make([]int, 0, before)
191 for i := range before {
192 indexes = append(indexes, i)
193 }
194 return indexes
195 }
196 indexes := make([]int, 0, 8)
197 seenCurrentTurn := false
198 for i := before - 1; i >= 0; i-- {
199 m := messages[i].materialize()
200 isCurrentTurn := m.TurnID == runtime.TurnID
201 active := (!runtime.Status.Terminal() && m.Pending) || (m.Role == "user" && isCurrentTurn)
202 for _, call := range m.ToolCalls {
203 active = active || (!runtime.Status.Terminal() && call.Pending)
204 }
205 if active {
206 indexes = append(indexes, i)
207 }
208 if isCurrentTurn {
209 seenCurrentTurn = true
210 continue
211 }
212 if seenCurrentTurn {
213 break
214 }
215 }
216 return indexes
217 }
218
219 func (p *Projection) Content(req ContentRequest) (ContentChunk, error) {
220 p.mu.Lock()
221 defer p.mu.Unlock()
222 if req.SnapshotID == "" {
223 return ContentChunk{}, errors.New("snapshotId is required")
224 }
225 frozen, err := p.freezeLocked(req.SnapshotID)
226 if err != nil {
227 return ContentChunk{}, err
228 }
229 if frozen == nil {
230 return ContentChunk{Stale: true}, nil
231 }
232 return frozen.contentCurrent(req)
233 }
234
235 func (p *Projection) contentCurrent(req ContentRequest) (ContentChunk, error) {
236 if req.SnapshotID != p.boundaryLocked().SnapshotID {
237 return ContentChunk{Stale: true}, nil
238 }
239 if req.Offset < 0 || len(req.Path) == 0 || len(req.Path) > 16 {
240 return ContentChunk{}, errors.New("invalid transcript content request")
241 }
242 for _, row := range p.buffer.messages {
243 if row.message.RecordID != req.RecordID {
244 continue
245 }
246 text, ok := contentStringAt(row.materialize(), req.Path)
247 if !ok || req.Offset > len(text) || (req.Offset < len(text) && !utf8.RuneStart(text[req.Offset])) {
248 return ContentChunk{}, errors.New("invalid transcript content offset")
249 }
250 end := runeBoundary(text, min(req.Offset+contentChunkBytes, len(text)))
251 return ContentChunk{Data: text[req.Offset:end], NextOffset: end, Done: end == len(text)}, nil
252 }
253 return ContentChunk{}, errors.New("transcript record not found")
254 }
255
256 func boundedRecord(message Message, snapshotID string) (Record, error) {
257 if message.RecordID == "" {
258 return Record{}, errors.New("transcript snapshot record identity is missing")
259 }
260 out := Record{ID: message.RecordID, Refs: []ContentRef{}}
261 out.Message = mapContentStrings(reflect.ValueOf(message), nil, func(text string, path []string) string {
262 if len(text) <= inlineFieldBytes {
263 return text
264 }
265 out.Refs = append(out.Refs, ContentRef{SnapshotID: snapshotID, RecordID: out.ID, Path: path, Bytes: len(text)})
266 return text[:runeBoundary(text, previewBytes)]
267 }).Interface().(Message)
268 return out, nil
269 }
270
271 func runeBoundary(text string, offset int) int {
272 for offset > 0 && offset < len(text) && !utf8.RuneStart(text[offset]) {
273 offset--
274 }
275 return offset
276 }
277
277 lines GO