返回 DeepSeek-Reasonix
observer.go
根目录 / internal / checkpoint / observer.go
1 package checkpoint
2
3 import (
4 "fmt"
5 "sort"
6 "sync"
7 "sync/atomic"
8 "time"
9
10 "reasonix/internal/diff"
11 )
12
13 // writerRegistry is shared across parent/child observers so background writer
14 // registration is race-free under a single mutex.
15 type writerRegistry struct {
16 mu sync.Mutex
17 writers map[string]ActiveWriter
18 barrierHeld map[string]bool
19 }
20
21 // MutationObserver is the host-side unified file mutation observer that replaces
22 // a single onPreEdit hook. It captures preimages before mutations and after
23 // fingerprints regardless of tool success/failure.
24 //
25 // The observer is passed through Agent Options/context to sub-agents. It does
26 // not change provider-visible tool names, schemas, or system prompts.
27 type MutationObserver struct {
28 store *Store
29
30 mu sync.Mutex
31 // shared writers registry (parent and clones share the same pointer).
32 reg *writerRegistry
33 // ownershipTurn is the turn that owns the current observation context.
34 // Foreground sub-agents inherit the parent turn; background ones keep the
35 // turn that spawned them.
36 ownershipTurn int
37 // writerID identifies the current agent for AfterMutation bookkeeping.
38 writerID string
39 // background marks a background sub-agent writer.
40 background bool
41 // seq is a monotonic mutation counter shared across the store session.
42 seq *atomic.Int64
43 }
44
45 // ObserverOptions configures a MutationObserver bound to a store.
46 type ObserverOptions struct {
47 Store *Store
48 OwnershipTurn int
49 WriterID string
50 Background bool
51 Seq *atomic.Int64
52 }
53
54 // NewMutationObserver binds an observer to store. A nil store yields a no-op observer.
55 func NewMutationObserver(opts ObserverOptions) *MutationObserver {
56 seq := opts.Seq
57 if seq == nil {
58 seq = &atomic.Int64{}
59 }
60 return &MutationObserver{
61 store: opts.Store,
62 reg: &writerRegistry{
63 writers: map[string]ActiveWriter{},
64 barrierHeld: map[string]bool{},
65 },
66 ownershipTurn: opts.OwnershipTurn,
67 writerID: opts.WriterID,
68 background: opts.Background,
69 seq: seq,
70 }
71 }
72
73 // CloneForSubagent returns a child observer that shares the store, mutation
74 // sequence, and writer registry but has its own writer identity and ownership turn.
75 func (o *MutationObserver) CloneForSubagent(writerID string, ownershipTurn int, background bool) *MutationObserver {
76 if o == nil {
77 return nil
78 }
79 return &MutationObserver{
80 store: o.store,
81 reg: o.reg,
82 ownershipTurn: ownershipTurn,
83 writerID: writerID,
84 background: background,
85 seq: o.seq,
86 }
87 }
88
89 // Store returns the underlying checkpoint store.
90 func (o *MutationObserver) Store() *Store {
91 if o == nil {
92 return nil
93 }
94 return o.store
95 }
96
97 // SetOwnershipTurn updates the turn that owns subsequent captures.
98 func (o *MutationObserver) SetOwnershipTurn(turn int) {
99 if o == nil {
100 return
101 }
102 o.mu.Lock()
103 o.ownershipTurn = turn
104 o.mu.Unlock()
105 }
106
107 // OwnershipTurn returns the current ownership turn.
108 func (o *MutationObserver) OwnershipTurn() int {
109 if o == nil {
110 return 0
111 }
112 o.mu.Lock()
113 defer o.mu.Unlock()
114 return o.ownershipTurn
115 }
116
117 // RegisterWriter marks a background writer as active. Rollback precheck returns
118 // busy while any writer is registered.
119 func (o *MutationObserver) RegisterWriter(id, kind string, turn int) error {
120 if o == nil || id == "" || o.reg == nil {
121 return nil
122 }
123 o.reg.mu.Lock()
124 defer o.reg.mu.Unlock()
125 if o.reg.writers == nil {
126 o.reg.writers = map[string]ActiveWriter{}
127 }
128 if o.reg.barrierHeld == nil {
129 o.reg.barrierHeld = map[string]bool{}
130 }
131 if o.reg.barrierHeld[id] {
132 return nil
133 }
134 o.reg.writers[id] = ActiveWriter{ID: id, Turn: turn, StartedAt: time.Now(), Kind: kind}
135 snap := o.snapshotWritersLocked()
136 if o.store != nil {
137 o.store.SetActiveWriters(snap)
138 if err := o.store.Barrier().EnterWrite(); err != nil {
139 delete(o.reg.writers, id)
140 o.store.SetActiveWriters(o.snapshotWritersLocked())
141 return fmt.Errorf("register background writer: %w", err)
142 }
143 }
144 o.reg.barrierHeld[id] = true
145 return nil
146 }
147
148 // UnregisterWriter removes a background writer.
149 func (o *MutationObserver) UnregisterWriter(id string) {
150 if o == nil || id == "" || o.reg == nil {
151 return
152 }
153 o.reg.mu.Lock()
154 defer o.reg.mu.Unlock()
155 delete(o.reg.writers, id)
156 snap := o.snapshotWritersLocked()
157 if o.store != nil {
158 o.store.SetActiveWriters(snap)
159 if o.reg.barrierHeld[id] {
160 o.store.Barrier().ExitWrite()
161 }
162 }
163 delete(o.reg.barrierHeld, id)
164 }
165
166 // ActiveWriters returns a copy of currently registered writers.
167 func (o *MutationObserver) ActiveWriters() []ActiveWriter {
168 if o == nil || o.reg == nil {
169 return nil
170 }
171 o.reg.mu.Lock()
172 defer o.reg.mu.Unlock()
173 return o.snapshotWritersLocked()
174 }
175
176 // Caller must hold o.reg.mu.
177 func (o *MutationObserver) snapshotWritersLocked() []ActiveWriter {
178 if o.reg == nil || len(o.reg.writers) == 0 {
179 return nil
180 }
181 out := make([]ActiveWriter, 0, len(o.reg.writers))
182 for _, w := range o.reg.writers {
183 out = append(out, w)
184 }
185 sort.Slice(out, func(i, j int) bool { return out[i].ID < out[j].ID })
186 return out
187 }
188
189 // HasActiveWriters reports whether any background writer is still running.
190 func (o *MutationObserver) HasActiveWriters() bool {
191 return len(o.ActiveWriters()) > 0
192 }
193
194 // BeforeMutation captures the preimage for a known path before a tool or hook runs.
195 // Prefer this over the legacy Snapshot(diff.Change) path for built-in tools.
196 func (o *MutationObserver) BeforeMutation(path, tool string, source CaptureSource) {
197 if o == nil || o.store == nil || path == "" {
198 return
199 }
200 if source == "" {
201 source = CaptureBeforeMutation
202 }
203 o.store.CaptureBefore(path, CaptureBeforeOpts{
204 Tool: tool,
205 Source: source,
206 WriterID: o.writerID,
207 OwnershipTurn: o.OwnershipTurn(),
208 Background: o.background,
209 })
210 }
211
212 // BeforeMutationFromChange is the Previewer-compatible path: uses OldText when
213 // provided for encoding-stable text captures, otherwise falls back to disk.
214 func (o *MutationObserver) BeforeMutationFromChange(ch diff.Change, tool string) {
215 if o == nil || o.store == nil || ch.Path == "" {
216 return
217 }
218 o.store.CaptureBeforeFromChange(ch, CaptureBeforeOpts{
219 Tool: tool,
220 Source: CapturePreviewer,
221 WriterID: o.writerID,
222 OwnershipTurn: o.OwnershipTurn(),
223 Background: o.background,
224 })
225 }
226
227 // AfterMutation re-reads the path after a tool attempt (success or failure) and
228 // records the after fingerprint under Reasonix ownership.
229 func (o *MutationObserver) AfterMutation(path, tool string) {
230 if o == nil || o.store == nil || path == "" {
231 return
232 }
233 seq := o.seq.Add(1)
234 o.store.CaptureAfter(path, CaptureAfterOpts{
235 Seq: seq,
236 Tool: tool,
237 Source: CaptureAfterMutation,
238 WriterID: o.writerID,
239 OwnershipTurn: o.OwnershipTurn(),
240 Background: o.background,
241 })
242 }
243
244 // RecordGap attaches an explicit coverage gap (bash, hook, MCP, …).
245 func (o *MutationObserver) RecordGap(gap CoverageGap) {
246 if o == nil || o.store == nil {
247 return
248 }
249 o.store.RecordGap(gap)
250 }
251
252 // NoteCrossTurnBackgroundWriter records a gap when a new user turn begins while
253 // a background writer from an earlier turn is still active.
254 func (o *MutationObserver) NoteCrossTurnBackgroundWriter(newTurn int) {
255 if o == nil {
256 return
257 }
258 for _, w := range o.ActiveWriters() {
259 if w.Turn < newTurn {
260 o.RecordGap(CoverageGap{
261 Reason: GapBackgroundWriter,
262 Detail: "background writer from earlier turn still active",
263 Tool: w.Kind,
264 })
265 }
266 }
267 }
268
269 // CaptureBeforeOpts configures a preimage capture.
270 type CaptureBeforeOpts struct {
271 Tool string
272 Source CaptureSource
273 WriterID string
274 OwnershipTurn int
275 Background bool
276 }
277
278 // CaptureAfterOpts configures an after-fingerprint capture.
279 type CaptureAfterOpts struct {
280 Seq int64
281 Tool string
282 Source CaptureSource
283 WriterID string
284 OwnershipTurn int
285 Background bool
286 }
287
287 lines GO