/ internal / daemon / memory_fallback.go
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  }