返回 DeepSeek-Reasonix
session_migration_stream.go
根目录 / internal / agent / session_migration_stream.go
1 package agent
2
3 import (
4 "context"
5 "crypto/sha256"
6 "encoding/json"
7 "errors"
8 "fmt"
9 "io"
10 "os"
11
12 "reasonix/internal/provider"
13 "reasonix/internal/store"
14 )
15
16 // MigrationMessageStream is the bounded legacy migration result. Messages is
17 // the final normalized count after replace records and persisted-safe repairs.
18 type MigrationMessageStream struct {
19 Messages int
20 FromEvents bool
21 }
22
23 // StreamSessionMessagesForMigration emits the authoritative frozen transcript
24 // without retaining cumulative schema-1/checkpoint history. reset is called
25 // before the initial stream and whenever a schema-1 replace record supersedes
26 // its prefix. Schema-2 DAGs still use their graph-specific compatibility
27 // reader; their disk-index migration is a separate adapter.
28 func StreamSessionMessagesForMigration(ctx context.Context, path, headID string, reset func() error, emit func(provider.Message) error) (MigrationMessageStream, error) {
29 if ctx == nil {
30 ctx = context.Background()
31 }
32 if reset == nil || emit == nil {
33 return MigrationMessageStream{}, errors.New("session migration stream requires reset and emit callbacks")
34 }
35 probe, err := probeSessionEventLogWithLimits(path, migrationSessionReplayLimits())
36 if err != nil {
37 return MigrationMessageStream{}, err
38 }
39 if probe.futureSchema {
40 return MigrationMessageStream{}, fmt.Errorf("session event log for %s uses unsupported schema %d", path, probe.schemaVersion)
41 }
42 if probe.dag || headID != "" {
43 var loaded *Session
44 if headID == "" {
45 loaded, err = LoadSessionForMigration(ctx, path)
46 } else {
47 loaded, err = LoadSessionHeadForMigration(ctx, path, headID)
48 }
49 if err != nil {
50 return MigrationMessageStream{}, err
51 }
52 if err := reset(); err != nil {
53 return MigrationMessageStream{}, err
54 }
55 messages := loaded.Snapshot()
56 for _, message := range messages {
57 if err := ctx.Err(); err != nil {
58 return MigrationMessageStream{}, err
59 }
60 if err := emit(message); err != nil {
61 return MigrationMessageStream{}, err
62 }
63 }
64 return MigrationMessageStream{Messages: len(messages), FromEvents: probe.dag}, nil
65 }
66 emitter := newMigrationTurnEmitter(path, reset, emit)
67 if err := emitter.resetAll(); err != nil {
68 return MigrationMessageStream{}, err
69 }
70 if probe.native && probe.size > 0 {
71 records, err := streamSchemaOneMigration(ctx, store.SessionEventLog(path), emitter)
72 if err != nil {
73 return MigrationMessageStream{FromEvents: true}, err
74 }
75 if records > 0 {
76 if err := emitter.finish(); err != nil {
77 return MigrationMessageStream{FromEvents: true}, err
78 }
79 return MigrationMessageStream{Messages: emitter.count, FromEvents: true}, nil
80 }
81 }
82 if err := emitter.resetAll(); err != nil {
83 return MigrationMessageStream{}, err
84 }
85 file, err := os.Open(path)
86 if err != nil {
87 return MigrationMessageStream{}, err
88 }
89 defer file.Close()
90 decoder := json.NewDecoder(&contextReader{ctx: ctx, reader: file})
91 for {
92 var message provider.Message
93 if err := decoder.Decode(&message); err != nil {
94 if errors.Is(err, io.EOF) {
95 break
96 }
97 return MigrationMessageStream{}, fmt.Errorf("decode %s: %w", path, err)
98 }
99 if err := emitter.add(message); err != nil {
100 return MigrationMessageStream{}, err
101 }
102 }
103 if err := emitter.finish(); err != nil {
104 return MigrationMessageStream{}, err
105 }
106 return MigrationMessageStream{Messages: emitter.count}, nil
107 }
108
109 type migrationTurnEmitter struct {
110 path string
111 reset func() error
112 emit func(provider.Message) error
113 pending []provider.Message
114 runningHash hashWriter
115 count int
116 sourceCount int
117 }
118
119 type hashWriter interface {
120 Write([]byte) (int, error)
121 Sum([]byte) []byte
122 Reset()
123 }
124
125 func newMigrationTurnEmitter(path string, reset func() error, emit func(provider.Message) error) *migrationTurnEmitter {
126 return &migrationTurnEmitter{path: path, reset: reset, emit: emit, runningHash: sha256.New()}
127 }
128
129 func (e *migrationTurnEmitter) resetAll() error {
130 e.pending = nil
131 e.runningHash.Reset()
132 e.count, e.sourceCount = 0, 0
133 return e.reset()
134 }
135
136 func (e *migrationTurnEmitter) add(message provider.Message) error {
137 // Persisted-safe normalization never pairs work across a real user turn.
138 // Buffering one turn preserves receipt/tool repair semantics without
139 // retaining the cumulative conversation.
140 if message.Role == provider.RoleUser && !message.LocalOnly && len(e.pending) > 0 {
141 if err := e.flushTurn(); err != nil {
142 return err
143 }
144 }
145 e.pending = append(e.pending, message)
146 e.sourceCount++
147 return nil
148 }
149
150 func (e *migrationTurnEmitter) finish() error { return e.flushTurn() }
151
152 func (e *migrationTurnEmitter) flushTurn() error {
153 if len(e.pending) == 0 {
154 return nil
155 }
156 messages := migrateLegacyProviderContent(NormalizeSession(e.pending))
157 branchID := BranchID(e.path)
158 for _, message := range messages {
159 identity, err := json.Marshal(messageForSessionIdentity(message))
160 if err != nil {
161 return err
162 }
163 _, _ = e.runningHash.Write(identity)
164 _, _ = e.runningHash.Write([]byte{'\n'})
165 if message.ID == "" {
166 message.ID = legacyMessageID(branchID, e.count, e.runningHash.Sum(nil))
167 }
168 if err := e.emit(message); err != nil {
169 return err
170 }
171 e.count++
172 }
173 e.pending = nil
174 return nil
175 }
176
177 func streamSchemaOneMigration(ctx context.Context, path string, emitter *migrationTurnEmitter) (int, error) {
178 file, err := os.Open(path)
179 if err != nil {
180 return 0, err
181 }
182 defer file.Close()
183 decoder := json.NewDecoder(&contextReader{ctx: ctx, reader: file})
184 records := 0
185 for {
186 if err := ctx.Err(); err != nil {
187 return records, err
188 }
189 token, err := decoder.Token()
190 if errors.Is(err, io.EOF) {
191 return records, nil
192 }
193 if err != nil {
194 return records, fmt.Errorf("%w: decode schema-1 record: %w", ErrSessionHistoryDamaged, err)
195 }
196 if delimiter, ok := token.(json.Delim); !ok || delimiter != '{' {
197 return records, fmt.Errorf("%w: schema-1 record is not an object", ErrSessionHistoryDamaged)
198 }
199 var schema int
200 var kind string
201 var messageIndex int
202 sawMessages := false
203 for decoder.More() {
204 nameToken, err := decoder.Token()
205 if err != nil {
206 return records, fmt.Errorf("%w: decode schema-1 field: %w", ErrSessionHistoryDamaged, err)
207 }
208 name, ok := nameToken.(string)
209 if !ok {
210 return records, fmt.Errorf("%w: invalid schema-1 field name", ErrSessionHistoryDamaged)
211 }
212 switch name {
213 case "schema_version":
214 err = decoder.Decode(&schema)
215 case "type":
216 err = decoder.Decode(&kind)
217 case "message_index":
218 err = decoder.Decode(&messageIndex)
219 case "messages":
220 if schema != sessionEventSchemaVersion || (kind != sessionEventTypeReplace && kind != sessionEventTypeAppend) {
221 return records, fmt.Errorf("%w: schema/type must precede messages", ErrSessionHistoryDamaged)
222 }
223 if kind == sessionEventTypeReplace {
224 if err := emitter.resetAll(); err != nil {
225 return records, err
226 }
227 } else if messageIndex != emitter.sourceCount {
228 return records, fmt.Errorf("%w: append index %d does not match %d", ErrSessionHistoryDamaged, messageIndex, emitter.sourceCount)
229 }
230 err = decodeMigrationMessageArray(ctx, decoder, emitter)
231 sawMessages = true
232 default:
233 var discard json.RawMessage
234 err = decoder.Decode(&discard)
235 }
236 if err != nil {
237 return records, fmt.Errorf("%w: decode schema-1 %s: %w", ErrSessionHistoryDamaged, name, err)
238 }
239 }
240 if _, err := decoder.Token(); err != nil {
241 return records, fmt.Errorf("%w: close schema-1 record: %w", ErrSessionHistoryDamaged, err)
242 }
243 if schema != sessionEventSchemaVersion || !sawMessages {
244 return records, fmt.Errorf("%w: incomplete schema-1 record", ErrSessionHistoryDamaged)
245 }
246 records++
247 }
248 }
249
250 func decodeMigrationMessageArray(ctx context.Context, decoder *json.Decoder, emitter *migrationTurnEmitter) error {
251 token, err := decoder.Token()
252 if err != nil {
253 return err
254 }
255 if delimiter, ok := token.(json.Delim); !ok || delimiter != '[' {
256 return errors.New("messages must be an array")
257 }
258 for decoder.More() {
259 if err := ctx.Err(); err != nil {
260 return err
261 }
262 var message provider.Message
263 if err := decoder.Decode(&message); err != nil {
264 return err
265 }
266 if err := emitter.add(message); err != nil {
267 return err
268 }
269 }
270 _, err = decoder.Token()
271 return err
272 }
273
273 lines GO