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