返回 DeepSeek-Reasonix
business.go
根目录 / internal / transcript / business.go
1 package transcript
2
3 import (
4 "slices"
5
6 "reasonix/internal/event"
7 "reasonix/internal/eventwire"
8 )
9
10 func (p *Projection) PersistenceFailed() {
11 p.mu.Lock()
12 defer p.mu.Unlock()
13 p.runtime.Status = event.TurnRecoveryRequired
14 p.revision++
15 p.publishChangeLocked(Change{Event: &eventwire.Event{Kind: "turn_status", Status: string(event.TurnRecoveryRequired)}})
16 p.revision++
17 p.publishChangeLocked(Change{Event: &eventwire.Event{Kind: "notice", Level: "warn", Code: "transcript_save_failed", Text: "Session output could not be saved. Displayed output is retained; recovery is required."}})
18 }
19
20 func (p *Projection) RestoreRuntime(runtime Runtime, durable uint64) {
21 p.mu.Lock()
22 defer p.mu.Unlock()
23 p.runtime, p.durable = runtime, durable
24 }
25
26 // AcceptBusiness advances coverage for every accepted batch, including batches
27 // that have no visible records. Rows come from the canonical message reader,
28 // never from a wire-event ledger. Mutable stream owners are retained separately
29 // from the bounded settled tail.
30 func (p *Projection) AcceptBusiness(rows []Message, covered uint64, turnID string, rewrite bool, finalMessageID ...string) {
31 p.acceptBusiness(rows, nil, covered, turnID, rewrite, finalMessageID...)
32 }
33
34 // AcceptRetractions invalidates the reading cut while retaining unrelated
35 // active output. The canonical reader supplies the new visible history.
36 func (p *Projection) AcceptRetractions(rows []Message, removed []string, covered uint64, turnID, finalMessageID string) {
37 p.acceptBusiness(rows, removed, covered, turnID, false, finalMessageID)
38 }
39
40 func (p *Projection) acceptBusiness(rows []Message, removed []string, covered uint64, turnID string, rewrite bool, finalMessageID ...string) {
41 p.mu.Lock()
42 defer p.mu.Unlock()
43 if covered <= p.covered {
44 return
45 }
46 if rewrite {
47 p.buffer.Reset()
48 }
49 for _, id := range removed {
50 for index, row := range slices.Backward(p.buffer.messages) {
51 if row.message.MessageID == id {
52 if row.message.Role == "user" {
53 p.buffer.userTurns--
54 }
55 p.buffer.messages = append(p.buffer.messages[:index], p.buffer.messages[index+1:]...)
56 }
57 }
58 delete(p.buffer.byMessageID, id)
59 delete(p.results, id)
60 for key, attempt := range p.attempts {
61 if attempt.MessageID == id {
62 delete(p.attempts, key)
63 }
64 }
65 }
66 reset := rewrite || len(removed) > 0
67 if reset {
68 p.identity.RewriteEpoch++
69 clear(p.snapshots)
70 p.snapshotOrder = nil
71 p.snapshotBytes = 0
72 }
73 if p.buffer.byMessageID == nil {
74 p.buffer.byMessageID = make(map[string]*bufferedMessage)
75 }
76 if p.results == nil {
77 p.results = make(map[string]uint64)
78 }
79 published := make([]Message, 0, len(rows))
80 for _, message := range rows {
81 p.ensureRecordIdentity(&message)
82 published = append(published, message)
83 if message.TurnID == "" {
84 message.TurnID = turnID
85 }
86 var row *bufferedMessage
87 for _, existing := range p.buffer.messages {
88 if existing.message.RecordID == message.RecordID {
89 row = existing
90 break
91 }
92 }
93 if row == nil {
94 row = &bufferedMessage{}
95 p.buffer.messages = append(p.buffer.messages, row)
96 if message.Role == "user" {
97 p.buffer.userTurns++
98 }
99 }
100 row.message = message
101 if message.Role == "assistant" {
102 if len(finalMessageID) == 0 {
103 p.runtime.FinalMessageID = message.MessageID
104 }
105 row.content.replace(message.Content)
106 row.reasoning.replace(message.Reasoning)
107 row.message.Content, row.message.Reasoning = "", ""
108 }
109 if message.MessageID != "" && (message.Role == "assistant" || message.Role == "user") {
110 p.buffer.byMessageID[message.MessageID] = row
111 p.results[message.MessageID] = covered
112 }
113 }
114 first := p.covered + 1
115 if len(finalMessageID) > 0 {
116 p.runtime.FinalMessageID = finalMessageID[0]
117 }
118 p.covered = covered
119 p.revision++
120 p.trimSettledLocked()
121 p.publishChangeLocked(Change{FirstSeq: first, Records: published, ResetRequired: reset})
122 }
123
124 func (p *Projection) trimSettledLocked() {
125 const retainedRecords = 96
126 if len(p.buffer.messages) <= retainedRecords {
127 return
128 }
129 keep := make([]*bufferedMessage, 0, retainedRecords+len(p.attempts))
130 for i, row := range p.buffer.messages {
131 active := row.message.Pending
132 for _, attempt := range p.attempts {
133 active = active || attempt.MessageID == row.message.MessageID
134 }
135 if active || i >= len(p.buffer.messages)-retainedRecords {
136 keep = append(keep, row)
137 } else if p.buffer.byMessageID[row.message.MessageID] == row {
138 delete(p.buffer.byMessageID, row.message.MessageID)
139 delete(p.results, row.message.MessageID)
140 }
141 }
142 p.buffer.messages = keep
143 }
144
144 lines GO