返回 DeepSeek-Reasonix
session_content.go
根目录 / internal / agent / session_content.go
1 package agent
2
3 import (
4 "bytes"
5 "context"
6 "fmt"
7 "os"
8 "time"
9
10 "reasonix/internal/provider"
11 "reasonix/internal/store"
12 )
13
14 // ContentDigest returns the canonical digest used by the session WAL and
15 // revision ledger for the current in-memory transcript.
16 func (s *Session) ContentDigest() (string, error) {
17 if s == nil {
18 return "", fmt.Errorf("nil session")
19 }
20 return ContentDigestForMessages(s.Snapshot())
21 }
22
23 // ContentDigestForMessages returns the canonical transcript digest for an
24 // immutable message snapshot. Frontends use it to bind a rendered history page
25 // to the exact content it contains instead of sampling a sidecar revision that
26 // may have advanced before or after the page was built.
27 func ContentDigestForMessages(msgs []provider.Message) (string, error) {
28 digest, err := digestSessionMessages(msgs)
29 if err != nil {
30 return "", err
31 }
32 return digestString(digest), nil
33 }
34
35 // SessionsShareContent reports whether two saved sessions decode to the same
36 // transcript. It replaces byte-comparing the .jsonl checkpoints, which stopped
37 // implying transcript equality once the event log became authoritative: two
38 // identical checkpoints can hide diverged event logs.
39 func SessionsShareContent(pathA, pathB string) (bool, error) {
40 msgsA, _, _, err := loadSessionMessages(pathA)
41 if err != nil {
42 return false, err
43 }
44 msgsB, _, _, err := loadSessionMessages(pathB)
45 if err != nil {
46 return false, err
47 }
48 digestA, err := digestSessionMessages(msgsA)
49 if err != nil {
50 return false, err
51 }
52 digestB, err := digestSessionMessages(msgsB)
53 if err != nil {
54 return false, err
55 }
56 return bytes.Equal(digestA[:], digestB[:]), nil
57 }
58
59 // SessionUserMessage is one complete user-role message with the best-known
60 // wall-clock time. Keeping the provider.Message preserves durable origin and
61 // RawContent so current display/history consumers never fall back to text
62 // prefixes. Messages restored from a replace event (compaction, rewind) lose
63 // their per-turn times and report zero; callers apply their own fallback.
64 type SessionUserMessage struct {
65 Message provider.Message
66 At time.Time
67 }
68
69 // LoadSessionUserMessages returns the session's user-role messages in
70 // transcript order, event-log aware. Direct .jsonl decoding misses everything
71 // after the first save once an event log exists, so surfaces like prompt
72 // history must use this instead.
73 func LoadSessionUserMessages(path string) ([]SessionUserMessage, error) {
74 return loadSessionUserMessagesWithLimits(path, defaultSessionReplayLimits)
75 }
76
77 func loadSessionUserMessagesWithLimits(path string, limits sessionReplayLimits) ([]SessionUserMessage, error) {
78 res, err := loadSessionTranscript(context.Background(), path, limits, nil)
79 if err != nil {
80 return nil, err
81 }
82 out := make([]SessionUserMessage, 0, len(res.msgs))
83 for i, m := range res.msgs {
84 if m.Role != provider.RoleUser || IsPinnedContextRevision(m) {
85 continue
86 }
87 at := time.Time{}
88 if i < len(res.times) {
89 at = res.times[i]
90 }
91 if m.CreatedAt > 0 {
92 at = time.UnixMilli(m.CreatedAt)
93 }
94 out = append(out, SessionUserMessage{Message: m, At: at})
95 }
96 return out, nil
97 }
98
99 // SessionContentModTime returns when the session transcript last changed on
100 // disk: the newer of the .jsonl checkpoint and the event log. The checkpoint
101 // alone goes stale between checkpoints, so recency ordering must use this.
102 func SessionContentModTime(path string) time.Time {
103 var mod time.Time
104 if info, err := os.Stat(path); err == nil && !info.IsDir() {
105 mod = info.ModTime()
106 }
107 if logPath := store.SessionEventLog(path); logPath != "" {
108 if info, err := os.Stat(logPath); err == nil && !info.IsDir() && info.ModTime().After(mod) {
109 mod = info.ModTime()
110 }
111 }
112 return mod
113 }
114
114 lines GO