返回 DeepSeek-Reasonix
history_slice_rebuild.go
根目录 / desktop / history_slice_rebuild.go
1 package main
2
3 import (
4 "log/slog"
5 "strings"
6
7 "reasonix/internal/agent"
8 "reasonix/internal/control"
9 )
10
11 // kickHistoryIndexRebuild single-flight schedules a background display-index
12 // rebuild for a live session whose on-disk index did not validate. It never
13 // blocks the request path.
14 func (a *App) kickHistoryIndexRebuild(sessionPath string) {
15 if strings.TrimSpace(sessionPath) == "" {
16 return
17 }
18 a.historySliceMu.Lock()
19 if a.historyIndexRebuilds == nil {
20 a.historyIndexRebuilds = map[string]chan struct{}{}
21 }
22 if _, ok := a.historyIndexRebuilds[sessionPath]; ok {
23 a.historySliceMu.Unlock()
24 return
25 }
26 done := make(chan struct{})
27 a.historyIndexRebuilds[sessionPath] = done
28 a.historySliceMu.Unlock()
29 a.goSafe("historyIndexRebuild", func() {
30 defer func() {
31 a.historySliceMu.Lock()
32 close(done)
33 delete(a.historyIndexRebuilds, sessionPath)
34 a.historySliceMu.Unlock()
35 }()
36 a.rebuildHistoryIndexForLiveSession(sessionPath)
37 })
38 }
39
40 // kickHistoryReadModelRepair single-flights the stronger cold-session repair:
41 // replay the authoritative event log under the save lock, atomically refresh
42 // the JSONL random-read model, then publish matching offsets. The cold request
43 // already returned from its in-memory recovery source before this work starts.
44 func (a *App) kickHistoryReadModelRepair(sessionPath string) {
45 if strings.TrimSpace(sessionPath) == "" {
46 return
47 }
48 key := "read-model:" + agent.CanonicalSessionPath(sessionPath)
49 a.historySliceMu.Lock()
50 if a.historyIndexRebuilds == nil {
51 a.historyIndexRebuilds = map[string]chan struct{}{}
52 }
53 if _, ok := a.historyIndexRebuilds[key]; ok {
54 a.historySliceMu.Unlock()
55 return
56 }
57 done := make(chan struct{})
58 a.historyIndexRebuilds[key] = done
59 a.historySliceMu.Unlock()
60 a.goSafe("historyReadModelRepair", func() {
61 defer func() {
62 a.historySliceMu.Lock()
63 close(done)
64 delete(a.historyIndexRebuilds, key)
65 a.historySliceMu.Unlock()
66 }()
67 if err := agent.RepairSessionDisplayReadModel(sessionPath); err != nil {
68 slog.Debug("desktop: history read-model repair failed", "path", sessionPath, "err", err)
69 }
70 })
71 }
72
73 // rebuildHistoryIndexForLiveSession republishes the display index for a live
74 // session, but only when the in-memory log is exactly the persisted
75 // transcript — an append-only tail means the next save will publish a
76 // covering index anyway, and a scanned .jsonl anchor cannot describe the
77 // event-log tail.
78 func (a *App) rebuildHistoryIndexForLiveSession(sessionPath string) {
79 a.mu.RLock()
80 ctrls := make([]control.SessionAPI, 0, len(a.tabs))
81 for _, tab := range a.tabs {
82 if tab != nil && tab.Ctrl != nil {
83 ctrls = append(ctrls, tab.Ctrl)
84 }
85 }
86 a.mu.RUnlock()
87 var ctrl control.SessionAPI
88 for _, c := range ctrls {
89 if c.SessionPath() == sessionPath {
90 ctrl = c
91 break
92 }
93 }
94 wc, ok := ctrl.(historyWindowController)
95 if !ok {
96 return
97 }
98 ps, ok := wc.SessionPersistedState()
99 if !ok || !ps.UnchangedSincePersisted {
100 return
101 }
102 if err := agent.RepairSessionDisplayReadModel(sessionPath); err != nil {
103 slog.Debug("desktop: live history read-model rebuild failed", "path", sessionPath, "err", err)
104 }
105 }
106
106 lines GO