返回 DeepSeek-Reasonix
history_slice_compatibility.go
根目录 / desktop / history_slice_compatibility.go
1 package main
2
3 import (
4 "errors"
5 "fmt"
6 "log/slog"
7 "os"
8 "path/filepath"
9 "reasonix/internal/agent"
10 "reasonix/internal/store"
11 "strings"
12 )
13
14 // coldHistorySlice pages a session file with no running controller. Supported
15 // formats use the bounded native pager. Explicitly unsupported formats retain
16 // the compatibility adapter below, which may require a full display replay.
17 func (a *App) coldHistorySlice(sessionDir, path string, req HistorySliceRequest) (HistorySlice, error) {
18 sessionDir, sessionPath, err := a.historyReadSource(sessionDir, path)
19 if err != nil {
20 return emptyHistorySlice(), err
21 }
22 info, err := os.Stat(sessionPath)
23 if err != nil {
24 return emptyHistorySlice(), err
25 }
26 if info.IsDir() {
27 return emptyHistorySlice(), fmt.Errorf("not a session file: %s", sessionPath)
28 }
29 if page, ok, err := a.pagedColdHistorySlice(a.bootContext(), sessionDir, sessionPath, req); ok {
30 return page, err
31 }
32 return a.compatibilityColdHistorySlice(sessionDir, sessionPath, info, req)
33 }
34
35 func (a *App) compatibilityColdHistorySlice(sessionDir, sessionPath string, info os.FileInfo, req HistorySliceRequest) (HistorySlice, error) {
36 if historySessionLooksEventFormat(sessionPath) {
37 // Legacy event-record format: stream-decode (constant memory) and page
38 // the decoded rows. Only ancient sessions take this path.
39 slice, err := coldEventHistorySlice(sessionPath, info, req)
40 slice.Source = "scan"
41 return slice, err
42 }
43 resolver := sessionDisplayResolver(sessionDir, sessionPath)
44 indexPath := store.SessionDisplayIndex(sessionPath)
45 idx, err := agent.LoadSessionDisplayIndex(indexPath)
46 identity, identityKnown, identityErr := agent.SessionContentIdentity(sessionPath)
47 if identityErr != nil {
48 return emptyHistorySlice(), identityErr
49 }
50 if err == nil && compatibilityDisplayIndexValid(idx, identity, identityKnown, info, indexPath, sessionPath) {
51 slice, pageErr := a.pageHistorySliceSource(coldHistorySliceSource(sessionPath, idx), req, resolver, sessionPlannerDisplayTurns(sessionDir, sessionPath), nil, sessionPath)
52 if pageErr != nil {
53 return emptyHistorySlice(), pageErr
54 }
55 slice.Source = "index"
56 return slice, nil
57 }
58
59 // Scan the checkpoint and compare its digest with the ledger before event
60 // replay. Missing sidecars remain readable without trusting same-size
61 // anchor rewrites.
62 scanned, scanErr := agent.ScanSessionDisplayIndex(sessionPath)
63 if scanErr == nil {
64 if !identityKnown || scanned.ContentDigest == identity.DigestHex {
65 if identityKnown {
66 scanned.Revision = identity.Revision
67 scanned.RevisionKnown = identity.RevisionKnown
68 }
69 if writeErr := agent.WriteSessionDisplayIndex(store.SessionDisplayIndex(sessionPath), scanned); writeErr != nil {
70 slog.Debug("desktop: history display index republish failed", "path", sessionPath, "err", writeErr)
71 }
72 slice, pageErr := a.pageHistorySliceSource(coldHistorySliceSource(sessionPath, scanned), req, resolver, sessionPlannerDisplayTurns(sessionDir, sessionPath), nil, sessionPath)
73 if pageErr != nil {
74 return emptyHistorySlice(), pageErr
75 }
76 slice.Source = "scan"
77 return slice, nil
78 }
79 }
80
81 // The event log is authoritative. During append-only saves its transcript
82 // is newer than the compatibility .jsonl anchor, so scanning the anchor
83 // would silently omit the tail even when a display index covers it.
84 if eventInfo, statErr := os.Stat(store.SessionEventLog(sessionPath)); statErr == nil && !eventInfo.IsDir() && eventInfo.Size() > 0 {
85 messages, state, repairable, loadErr := agent.LoadSessionDisplayMessages(sessionPath)
86 if loadErr != nil {
87 return emptyHistorySlice(), loadErr
88 }
89 if !repairable {
90 return emptyHistorySlice(), agent.ErrSessionDisplayReadModelDamaged
91 }
92 src := newInMemoryHistorySliceSource(strings.TrimSuffix(filepath.Base(sessionPath), ".jsonl"), messages, resolver, state, true)
93 slice, pageErr := a.pageHistorySliceSource(src, req, resolver, sessionPlannerDisplayTurns(sessionDir, sessionPath), nil, sessionPath)
94 if pageErr != nil {
95 return emptyHistorySlice(), pageErr
96 }
97 slice.Source = "event-log"
98 return slice, nil
99 }
100
101 // Legacy checkpoints have no authoritative ledger identity. Scan their
102 // bytes to obtain the digest before trusting (or republishing) offsets; this
103 // detects same-size external rewrites that a size-only comparison misses.
104 if scanErr != nil {
105 // The bounded scanner rejects malformed or oversized records. Preserve
106 // compatibility through the authoritative loader; later reads can use
107 // the file-exact index.
108 messages, state, repairable, loadErr := agent.LoadSessionDisplayMessages(sessionPath)
109 if loadErr != nil {
110 return emptyHistorySlice(), errors.Join(scanErr, loadErr)
111 }
112 if !repairable {
113 return emptyHistorySlice(), agent.ErrSessionDisplayReadModelDamaged
114 }
115 src := newInMemoryHistorySliceSource(strings.TrimSuffix(filepath.Base(sessionPath), ".jsonl"), messages, resolver, state, true)
116 slice, pageErr := a.pageHistorySliceSource(src, req, resolver, sessionPlannerDisplayTurns(sessionDir, sessionPath), nil, sessionPath)
117 if pageErr != nil {
118 return emptyHistorySlice(), pageErr
119 }
120 slice.Source = "scan"
121 return slice, nil
122 }
123 if identityKnown {
124 if scanned.ContentDigest == identity.DigestHex {
125 scanned.Revision = identity.Revision
126 scanned.RevisionKnown = identity.RevisionKnown
127 }
128 }
129 if writeErr := agent.WriteSessionDisplayIndex(store.SessionDisplayIndex(sessionPath), scanned); writeErr != nil {
130 slog.Debug("desktop: history display index republish failed", "path", sessionPath, "err", writeErr)
131 }
132 slice, pageErr := a.pageHistorySliceSource(coldHistorySliceSource(sessionPath, scanned), req, resolver, sessionPlannerDisplayTurns(sessionDir, sessionPath), nil, sessionPath)
133 if pageErr != nil {
134 return emptyHistorySlice(), pageErr
135 }
136 slice.Source = "scan"
137 return slice, nil
138 }
139
140 func compatibilityDisplayIndexValid(idx *agent.SessionDisplayIndex, identity agent.PersistedState, identityKnown bool, info os.FileInfo, indexPath, sessionPath string) bool {
141 if idx == nil || idx.TranscriptSize != info.Size() {
142 return false
143 }
144 // Without a ledger, publication order and exact size bind the index to
145 // its checkpoint. A rewrite must also pass the timestamp guard.
146 valid := !idx.RevisionKnown
147 if identityKnown {
148 valid = agent.ValidateSessionDisplayIndex(idx, identity.Revision, identity.RevisionKnown, identity.Digest, info.Size())
149 }
150 return valid && historyIndexTimestampValid(indexPath, sessionPath, info, idx, true)
151 }
152
152 lines GO