返回 DeepSeek-Reasonix
session_display_range.go
根目录 / internal / agent / session_display_range.go
1 package agent
2
3 import (
4 "encoding/json"
5 "fmt"
6 "io"
7 "os"
8 "strings"
9
10 "reasonix/internal/provider"
11 "reasonix/internal/store"
12 )
13
14 // LoadSessionPreviewFromDisplayIndex reads only the first authored user-message
15 // range from a current display index. It never acquires the session save lock or
16 // scans the full transcript, so runtime tree snapshots cannot wait behind a
17 // long-running save. A missing or stale index fails closed and lets the caller
18 // use another preview source.
19 func LoadSessionPreviewFromDisplayIndex(path string) (string, bool, error) {
20 path = strings.TrimSpace(path)
21 if path == "" {
22 return "", false, fmt.Errorf("empty session path")
23 }
24 index, err := LoadSessionDisplayIndex(store.SessionDisplayIndex(path))
25 if err != nil {
26 return "", false, err
27 }
28 messageIndex := -1
29 for _, entry := range index.Entries {
30 if entry.StartsTurn {
31 messageIndex = entry.Index
32 break
33 }
34 }
35 if messageIndex < 0 {
36 return "", false, nil
37 }
38 messages, checkedIndex, err := LoadSessionDisplayMessageRange(path, messageIndex, messageIndex+1)
39 if err != nil {
40 return "", false, err
41 }
42 if checkedIndex == nil || messageIndex >= len(checkedIndex.Entries) || !checkedIndex.Entries[messageIndex].StartsTurn {
43 return "", false, fmt.Errorf("session display index changed while reading preview")
44 }
45 if err := validateSessionPreviewDisplayIndex(path, checkedIndex); err != nil {
46 return "", false, err
47 }
48 preview, turns := SessionPreviewFromMessages(messages)
49 if turns == 0 || strings.TrimSpace(preview) == "" {
50 return "", false, nil
51 }
52 return preview, true, nil
53 }
54
55 func validateSessionPreviewDisplayIndex(path string, index *SessionDisplayIndex) error {
56 transcriptInfo, err := os.Stat(path)
57 if err != nil {
58 return err
59 }
60 indexInfo, err := os.Stat(store.SessionDisplayIndex(path))
61 if err != nil {
62 return err
63 }
64 // The index is published after the content it describes. Equality is
65 // ambiguous on coarse filesystems, so this latency-sensitive fallback
66 // declines to run a full digest scan to resolve the tie.
67 if !indexInfo.ModTime().After(SessionContentModTime(path)) {
68 return fmt.Errorf("session display index is not newer than session content")
69 }
70 identity, identityKnown, err := SessionContentIdentity(path)
71 if err != nil {
72 return err
73 }
74 if identityKnown {
75 if !ValidateSessionDisplayIndex(index, identity.Revision, identity.RevisionKnown, identity.Digest, transcriptInfo.Size()) {
76 return fmt.Errorf("session display index does not match content identity")
77 }
78 } else if index.RevisionKnown {
79 return fmt.Errorf("session display index has no matching content identity")
80 }
81 return nil
82 }
83
84 // LoadSessionDisplayMessageRange decodes a bounded range through the display
85 // index. Callers must separately compare the index revision and digest with
86 // the authoritative content ledger before trusting it.
87 func LoadSessionDisplayMessageRange(path string, start, end int) ([]provider.Message, *SessionDisplayIndex, error) {
88 index, err := LoadSessionDisplayIndex(store.SessionDisplayIndex(path))
89 if err != nil {
90 return nil, nil, err
91 }
92 if start < 0 || end < start || end > index.MessageCount {
93 return nil, index, fmt.Errorf("invalid display message range [%d,%d)", start, end)
94 }
95 f, err := os.Open(path)
96 if err != nil {
97 return nil, index, err
98 }
99 defer f.Close()
100 info, err := f.Stat()
101 if err != nil {
102 return nil, index, err
103 }
104 if info.Size() != index.TranscriptSize {
105 return nil, index, fmt.Errorf("display index transcript size changed")
106 }
107 out := make([]provider.Message, 0, end-start)
108 for _, entry := range index.Entries[start:end] {
109 line := make([]byte, entry.Length)
110 if _, err := f.ReadAt(line, entry.Offset); err != nil && err != io.EOF {
111 return nil, index, err
112 }
113 if len(line) == 0 || line[len(line)-1] != '\n' {
114 return nil, index, fmt.Errorf("display message %d is not newline terminated", entry.Index)
115 }
116 var message provider.Message
117 if err := json.Unmarshal(line[:len(line)-1], &message); err != nil {
118 return nil, index, fmt.Errorf("decode display message %d: %w", entry.Index, err)
119 }
120 out = append(out, message)
121 }
122 return out, index, nil
123 }
124
124 lines GO