Skip to content

Commit d8e7671

Browse files
committed
chore: bump yaad to backup scheduler + lock fix, wire snapshots
- external/yaad -> fbc0466 (PRs #53-58: incremental HNSW, graph restore, backup rotation, backup scheduler, refcounted process lock) - go.mod pseudo-version bumped to match the gitlink (parity guard) - YaadBridge starts the hourly yaad backup scheduler once per process on the first ready bridge (~/.yaad/data/backups, keep 7, 30d)
1 parent 2e6e10f commit d8e7671

6 files changed

Lines changed: 154 additions & 6 deletions

File tree

‎cmd/options.go‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,10 @@ func configureSessionHeavy(sess *engine.Session) {
432432
// Memory initialization touches persisted state and optional bridges.
433433
enhancedMem := memory.NewEnhancedMemoryManager(cwd)
434434
if enhancedMem.Yaad.Ready() {
435+
// Periodic yaad snapshots only for long-lived sessions — short-lived
436+
// diagnostic bridges skip scheduling entirely.
437+
enhancedMem.Yaad.EnsureBackups()
438+
435439
sess.MemorySvc().SetMemory(enhancedMem)
436440
sess.MemorySvc().SetYaad(enhancedMem.Yaad)
437441
sess.MemorySvc().SetEnhanced(enhancedMem)

‎external/yaad‎

Submodule yaad updated from 8bc7a83 to fbc0466

‎go.mod‎

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎go.sum‎

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎internal/intelligence/memory/yaad_bridge.go‎

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,27 @@ import (
2323
"github.com/GrayCodeAI/yaad/storage"
2424
)
2525

26+
// Backup tuning for the yaad snapshot scheduler. Snapshots go to
27+
// ~/.yaad/data/backups/, kept hourly with a bounded window so an idle or
28+
// crash-prone host always has a recent consistent copy of the memory DB.
29+
const (
30+
yaadBackupDir = "backups"
31+
yaadBackupInterval = time.Hour
32+
yaadBackupKeep = 7
33+
yaadBackupMaxAge = 30 * 24 * time.Hour
34+
)
35+
36+
// yaadBackupsMu guards yaadBackupDirs. yaadBackupDirs tracks which backup
37+
// directories already have a running scheduler so the many bridges a host
38+
// creates (one per callsite) share a single loop per directory instead of
39+
// stacking duplicate schedulers. Keying on the directory (not a global
40+
// sync.Once) keeps tests with distinct temp homes isolated and still dedups
41+
// the repeated NewYaadBridge calls a real host makes against one home.
42+
var (
43+
yaadBackupsMu sync.Mutex
44+
yaadBackupDirs = make(map[string]struct{})
45+
)
46+
2647
// YaadBridge connects hawk's memory system to the yaad memory graph.
2748
// If yaad is not initialized (missing DB), operations return a BridgeError
2849
// and log a warning on first access.
@@ -33,6 +54,9 @@ type YaadBridge struct {
3354
ready bool
3455
warnOnce sync.Once
3556

57+
dbDir string
58+
backupSched *storage.BackupScheduler
59+
3660
graphSessionID string
3761
graphScope graphcontracts.Scope
3862
}
@@ -66,9 +90,52 @@ func (b *YaadBridge) init() {
6690

6791
b.store = store
6892
b.engine = eng
93+
b.dbDir = dbDir
6994
b.ready = true
7095
}
7196

97+
// EnsureBackups starts the yaad snapshot scheduler for the bridge's database
98+
// directory. It is called once by the long-lived memory manager at session
99+
// startup — not from NewYaadBridge — so short-lived bridges (diagnostics,
100+
// status queries, tests) never leave scheduler goroutines writing into
101+
// their working directories. The directory is claimed so concurrent or
102+
// repeated calls reuse one loop; Close releases the claim and stops the
103+
// loop this bridge started.
104+
func (b *YaadBridge) EnsureBackups() {
105+
if b == nil || !b.ready {
106+
return
107+
}
108+
109+
yaadBackupsMu.Lock()
110+
if _, busy := yaadBackupDirs[b.dbDir]; busy {
111+
yaadBackupsMu.Unlock()
112+
return
113+
}
114+
// Claim the directory under the lock so concurrent callers cannot both
115+
// start a scheduler; released on failure so a later caller can retry.
116+
yaadBackupDirs[b.dbDir] = struct{}{}
117+
yaadBackupsMu.Unlock()
118+
119+
sched, err := b.store.ScheduleBackups(
120+
filepath.Join(b.dbDir, yaadBackupDir),
121+
yaadBackupInterval,
122+
yaadBackupKeep,
123+
yaadBackupMaxAge,
124+
)
125+
if err != nil {
126+
yaadBackupsMu.Lock()
127+
delete(yaadBackupDirs, b.dbDir)
128+
yaadBackupsMu.Unlock()
129+
slog.Warn("[hawk/memory] yaad backup scheduler not started", "error", err)
130+
return
131+
}
132+
sched.Start()
133+
134+
b.mu.Lock()
135+
b.backupSched = sched
136+
b.mu.Unlock()
137+
}
138+
72139
// Ready reports whether the yaad bridge is initialized and usable.
73140
func (b *YaadBridge) Ready() bool {
74141
return b.ready
@@ -673,15 +740,30 @@ func (b *YaadBridge) GetFullContent(ids []string) ([]FullResult, error) {
673740
}
674741

675742
// Close shuts down the yaad engine and closes the database connection.
743+
// If this bridge started the backup scheduler, it is stopped here and the
744+
// directory claim released so a later bridge can restart snapshots.
676745
func (b *YaadBridge) Close() {
677-
if !b.ready {
746+
b.mu.Lock()
747+
sched := b.backupSched
748+
b.backupSched = nil
749+
wasReady := b.ready
750+
b.ready = false
751+
b.mu.Unlock()
752+
753+
if sched != nil {
754+
sched.Stop()
755+
yaadBackupsMu.Lock()
756+
delete(yaadBackupDirs, b.dbDir)
757+
yaadBackupsMu.Unlock()
758+
}
759+
760+
if !wasReady {
678761
return
679762
}
680763
b.engine.Close()
681764
if b.store != nil {
682765
_ = b.store.Close()
683766
}
684-
b.ready = false
685767
}
686768

687769
func bridgeDigest(value string) string {

‎internal/intelligence/memory/yaad_bridge_integration_test.go‎

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,68 @@ func TestYaadBridge_Close(t *testing.T) {
172172
_ = b.store.Close()
173173
}
174174

175+
// TestYaadBridge_EnsureBackups verifies the scheduler is started once per
176+
// database directory, idempotent across repeated calls, and torn down by
177+
// Close so a later bridge can restart snapshots.
178+
func TestYaadBridge_EnsureBackups(t *testing.T) {
179+
dir := t.TempDir()
180+
t.Setenv("HOME", dir)
181+
_ = os.MkdirAll(dir+"/.yaad/data", 0o755)
182+
183+
b := NewYaadBridge()
184+
if !b.ready {
185+
t.Skip("yaad not available")
186+
}
187+
188+
b.EnsureBackups()
189+
b.EnsureBackups() // must be a no-op, not a second scheduler
190+
191+
b.mu.Lock()
192+
sched := b.backupSched
193+
b.mu.Unlock()
194+
if sched == nil {
195+
t.Fatal("expected backupSched to be set after EnsureBackups")
196+
}
197+
198+
// Second bridge on the same dbDir reuses the existing claim.
199+
b2 := NewYaadBridge()
200+
if !b2.ready {
201+
t.Skip("yaad not available")
202+
}
203+
b2.EnsureBackups()
204+
b2.mu.Lock()
205+
dup := b2.backupSched
206+
b2.mu.Unlock()
207+
if dup != nil {
208+
t.Fatal("second bridge must reuse the existing scheduler")
209+
}
210+
// Close a non-owning bridge: no scheduler to stop, no claim freed.
211+
b2.Close()
212+
213+
b.Close()
214+
b.mu.Lock()
215+
stopped := b.backupSched == nil
216+
b.mu.Unlock()
217+
if !stopped {
218+
t.Fatal("expected Close to clear the scheduler reference")
219+
}
220+
221+
// After Close the directory claim is released: a fresh bridge can
222+
// re-register without error.
223+
b3 := NewYaadBridge()
224+
if !b3.ready {
225+
t.Skip("yaad not available")
226+
}
227+
b3.EnsureBackups()
228+
b3.mu.Lock()
229+
restarted := b3.backupSched != nil
230+
b3.mu.Unlock()
231+
if !restarted {
232+
t.Fatal("expected a fresh bridge to start its own scheduler after Close")
233+
}
234+
b3.Close()
235+
}
236+
175237
func TestConfidenceTracker_WithBridge(t *testing.T) {
176238
b := newTestBridge(t)
177239
if !b.ready {

0 commit comments

Comments
 (0)