memory_fallback.go
1 package daemon 2 3 import ( 4 "bufio" 5 "context" 6 "os" 7 "path/filepath" 8 "strings" 9 10 "github.com/Kocoro-lab/ShanClaw/internal/config" 11 "github.com/Kocoro-lab/ShanClaw/internal/session" 12 "github.com/Kocoro-lab/ShanClaw/internal/tools" 13 ) 14 15 // daemonFallback adapts existing session search and the agent's MEMORY.md to 16 // the tools.FallbackQuery interface. Used when the structured memory 17 // service (Kocoro Cloud memory sidecar) is unavailable so memory_recall 18 // degrades gracefully instead of erroring. 19 type daemonFallback struct { 20 sessionMgr *session.Manager 21 } 22 23 // Compile-time check that *daemonFallback satisfies tools.FallbackQuery. 24 var _ tools.FallbackQuery = (*daemonFallback)(nil) 25 26 func (d *daemonFallback) SessionKeyword(_ context.Context, query string, limit int) ([]any, error) { 27 if d.sessionMgr == nil { 28 return nil, nil 29 } 30 hits, err := d.sessionMgr.Search(query, limit) 31 if err != nil { 32 return nil, err 33 } 34 out := make([]any, 0, len(hits)) 35 for _, h := range hits { 36 out = append(out, h) 37 } 38 return out, nil 39 } 40 41 // MemoryFileSnippet does a best-effort grep of ~/.shannon/MEMORY.md for query 42 // terms. Returns the joined matching lines (capped at 4KB). Empty string + 43 // nil error if the file is absent or no match is found. Agent-scoped MEMORY.md 44 // lookup needs per-run context that the daemon-level adapter doesn't have, so 45 // only the global file is checked here. 46 func (d *daemonFallback) MemoryFileSnippet(_ context.Context, query string) (string, error) { 47 dir := config.ShannonDir() 48 if dir == "" { 49 return "", nil 50 } 51 q := strings.ToLower(strings.TrimSpace(query)) 52 if q == "" { 53 return "", nil 54 } 55 candidates := []string{ 56 filepath.Join(dir, "MEMORY.md"), 57 } 58 const cap = 4096 59 for _, p := range candidates { 60 f, err := os.Open(p) 61 if err != nil { 62 continue 63 } 64 var matches []string 65 scanner := bufio.NewScanner(f) 66 total := 0 67 for scanner.Scan() { 68 line := scanner.Text() 69 if strings.Contains(strings.ToLower(line), q) { 70 matches = append(matches, line) 71 total += len(line) + 1 72 if total > cap { 73 break 74 } 75 } 76 } 77 f.Close() 78 if len(matches) > 0 { 79 return strings.Join(matches, "\n"), nil 80 } 81 } 82 return "", nil 83 }