Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions cmd/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,10 @@ func configureSessionHeavy(sess *engine.Session) {
// Memory initialization touches persisted state and optional bridges.
enhancedMem := memory.NewEnhancedMemoryManager(cwd)
if enhancedMem.Yaad.Ready() {
// Periodic yaad snapshots only for long-lived sessions — short-lived
// diagnostic bridges skip scheduling entirely.
enhancedMem.Yaad.EnsureBackups()

sess.MemorySvc().SetMemory(enhancedMem)
sess.MemorySvc().SetYaad(enhancedMem.Yaad)
sess.MemorySvc().SetEnhanced(enhancedMem)
Expand Down
2 changes: 1 addition & 1 deletion go.mod

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions go.sum

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

86 changes: 84 additions & 2 deletions internal/intelligence/memory/yaad_bridge.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,27 @@ import (
"github.com/GrayCodeAI/yaad/storage"
)

// Backup tuning for the yaad snapshot scheduler. Snapshots go to
// ~/.yaad/data/backups/, kept hourly with a bounded window so an idle or
// crash-prone host always has a recent consistent copy of the memory DB.
const (
yaadBackupDir = "backups"
yaadBackupInterval = time.Hour
yaadBackupKeep = 7
yaadBackupMaxAge = 30 * 24 * time.Hour
)

// yaadBackupsMu guards yaadBackupDirs. yaadBackupDirs tracks which backup
// directories already have a running scheduler so the many bridges a host
// creates (one per callsite) share a single loop per directory instead of
// stacking duplicate schedulers. Keying on the directory (not a global
// sync.Once) keeps tests with distinct temp homes isolated and still dedups
// the repeated NewYaadBridge calls a real host makes against one home.
var (
yaadBackupsMu sync.Mutex
yaadBackupDirs = make(map[string]struct{})
)

// YaadBridge connects hawk's memory system to the yaad memory graph.
// If yaad is not initialized (missing DB), operations return a BridgeError
// and log a warning on first access.
Expand All @@ -33,6 +54,9 @@ type YaadBridge struct {
ready bool
warnOnce sync.Once

dbDir string
backupSched *storage.BackupScheduler

graphSessionID string
graphScope graphcontracts.Scope
}
Expand Down Expand Up @@ -66,9 +90,52 @@ func (b *YaadBridge) init() {

b.store = store
b.engine = eng
b.dbDir = dbDir
b.ready = true
}

// EnsureBackups starts the yaad snapshot scheduler for the bridge's database
// directory. It is called once by the long-lived memory manager at session
// startup — not from NewYaadBridge — so short-lived bridges (diagnostics,
// status queries, tests) never leave scheduler goroutines writing into
// their working directories. The directory is claimed so concurrent or
// repeated calls reuse one loop; Close releases the claim and stops the
// loop this bridge started.
func (b *YaadBridge) EnsureBackups() {
if b == nil || !b.ready {
return
}

yaadBackupsMu.Lock()
if _, busy := yaadBackupDirs[b.dbDir]; busy {
yaadBackupsMu.Unlock()
return
}
// Claim the directory under the lock so concurrent callers cannot both
// start a scheduler; released on failure so a later caller can retry.
yaadBackupDirs[b.dbDir] = struct{}{}
yaadBackupsMu.Unlock()

sched, err := b.store.ScheduleBackups(
filepath.Join(b.dbDir, yaadBackupDir),
yaadBackupInterval,
yaadBackupKeep,
yaadBackupMaxAge,
)
if err != nil {
yaadBackupsMu.Lock()
delete(yaadBackupDirs, b.dbDir)
yaadBackupsMu.Unlock()
slog.Warn("[hawk/memory] yaad backup scheduler not started", "error", err)
return
}
sched.Start()

b.mu.Lock()
b.backupSched = sched
b.mu.Unlock()
}

// Ready reports whether the yaad bridge is initialized and usable.
func (b *YaadBridge) Ready() bool {
return b.ready
Expand Down Expand Up @@ -673,15 +740,30 @@ func (b *YaadBridge) GetFullContent(ids []string) ([]FullResult, error) {
}

// Close shuts down the yaad engine and closes the database connection.
// If this bridge started the backup scheduler, it is stopped here and the
// directory claim released so a later bridge can restart snapshots.
func (b *YaadBridge) Close() {
if !b.ready {
b.mu.Lock()
sched := b.backupSched
b.backupSched = nil
wasReady := b.ready
b.ready = false
b.mu.Unlock()

if sched != nil {
sched.Stop()
yaadBackupsMu.Lock()
delete(yaadBackupDirs, b.dbDir)
yaadBackupsMu.Unlock()
}

if !wasReady {
return
}
b.engine.Close()
if b.store != nil {
_ = b.store.Close()
}
b.ready = false
}

func bridgeDigest(value string) string {
Expand Down
62 changes: 62 additions & 0 deletions internal/intelligence/memory/yaad_bridge_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,68 @@ func TestYaadBridge_Close(t *testing.T) {
_ = b.store.Close()
}

// TestYaadBridge_EnsureBackups verifies the scheduler is started once per
// database directory, idempotent across repeated calls, and torn down by
// Close so a later bridge can restart snapshots.
func TestYaadBridge_EnsureBackups(t *testing.T) {
dir := t.TempDir()
t.Setenv("HOME", dir)
_ = os.MkdirAll(dir+"/.yaad/data", 0o755)

b := NewYaadBridge()
if !b.ready {
t.Skip("yaad not available")
}

b.EnsureBackups()
b.EnsureBackups() // must be a no-op, not a second scheduler

b.mu.Lock()
sched := b.backupSched
b.mu.Unlock()
if sched == nil {
t.Fatal("expected backupSched to be set after EnsureBackups")
}

// Second bridge on the same dbDir reuses the existing claim.
b2 := NewYaadBridge()
if !b2.ready {
t.Skip("yaad not available")
}
b2.EnsureBackups()
b2.mu.Lock()
dup := b2.backupSched
b2.mu.Unlock()
if dup != nil {
t.Fatal("second bridge must reuse the existing scheduler")
}
// Close a non-owning bridge: no scheduler to stop, no claim freed.
b2.Close()

b.Close()
b.mu.Lock()
stopped := b.backupSched == nil
b.mu.Unlock()
if !stopped {
t.Fatal("expected Close to clear the scheduler reference")
}

// After Close the directory claim is released: a fresh bridge can
// re-register without error.
b3 := NewYaadBridge()
if !b3.ready {
t.Skip("yaad not available")
}
b3.EnsureBackups()
b3.mu.Lock()
restarted := b3.backupSched != nil
b3.mu.Unlock()
if !restarted {
t.Fatal("expected a fresh bridge to start its own scheduler after Close")
}
b3.Close()
}

func TestConfidenceTracker_WithBridge(t *testing.T) {
b := newTestBridge(t)
if !b.ready {
Expand Down
Loading