From 6d9374a1a52728756da9aa7bebf88fa5c722cfa1 Mon Sep 17 00:00:00 2001 From: viettranx Date: Thu, 5 Mar 2026 09:35:38 +0700 Subject: [PATCH] fix: route list_files("memory") to DB in managed mode The memory interceptor handled read_file/write_file for memory paths but list_files hit the filesystem, returning "Directory does not exist". Now list_files queries memory_documents from PostgreSQL via the existing ListDocuments store method. Co-Authored-By: Claude Opus 4.6 --- cmd/gateway_managed.go | 7 +++++ internal/tools/filesystem_list.go | 31 +++++++++++++++++--- internal/tools/memory_interceptor.go | 44 ++++++++++++++++++++++++++++ 3 files changed, 78 insertions(+), 4 deletions(-) diff --git a/cmd/gateway_managed.go b/cmd/gateway_managed.go index d7f15ae7..cca6b2b6 100644 --- a/cmd/gateway_managed.go +++ b/cmd/gateway_managed.go @@ -157,6 +157,13 @@ func wireManagedExtras( } } } + if listTool, ok := toolsReg.Get("list_files"); ok { + if ia, ok := listTool.(tools.InterceptorAware); ok { + if stores.Memory != nil { + ia.SetMemoryInterceptor(tools.NewMemoryInterceptor(stores.Memory, workspace)) + } + } + } // Wire group writer cache for permission checks (managed mode only) if groupWriterCache != nil { diff --git a/internal/tools/filesystem_list.go b/internal/tools/filesystem_list.go index a06032a2..028709f5 100644 --- a/internal/tools/filesystem_list.go +++ b/internal/tools/filesystem_list.go @@ -12,10 +12,20 @@ import ( // ListFilesTool lists files in a directory, optionally through a sandbox container. type ListFilesTool struct { - workspace string - restrict bool - deniedPrefixes []string // path prefixes to deny access to (e.g. .goclaw) - sandboxMgr sandbox.Manager + workspace string + restrict bool + deniedPrefixes []string // path prefixes to deny access to (e.g. .goclaw) + sandboxMgr sandbox.Manager + contextFileIntc *ContextFileInterceptor // unused, satisfies InterceptorAware + memIntc *MemoryInterceptor // nil = no memory routing (standalone mode) +} + +func (t *ListFilesTool) SetContextFileInterceptor(intc *ContextFileInterceptor) { + t.contextFileIntc = intc +} + +func (t *ListFilesTool) SetMemoryInterceptor(intc *MemoryInterceptor) { + t.memIntc = intc } // DenyPaths adds path prefixes that list_files must reject/filter. @@ -54,6 +64,19 @@ func (t *ListFilesTool) Execute(ctx context.Context, args map[string]interface{} path = "." } + // Virtual FS: route memory directory listing to DB (managed mode) + if t.memIntc != nil { + if listing, handled, err := t.memIntc.ListFiles(ctx, path); handled { + if err != nil { + return ErrorResult(fmt.Sprintf("failed to list memory files: %v", err)) + } + if listing == "" { + return SilentResult("No memory files stored yet") + } + return SilentResult(listing) + } + } + // Sandbox routing (sandboxKey from ctx — thread-safe) sandboxKey := ToolSandboxKeyFromCtx(ctx) if t.sandboxMgr != nil && sandboxKey != "" { diff --git a/internal/tools/memory_interceptor.go b/internal/tools/memory_interceptor.go index 2bfa2e6f..ac19b6f0 100644 --- a/internal/tools/memory_interceptor.go +++ b/internal/tools/memory_interceptor.go @@ -2,6 +2,7 @@ package tools import ( "context" + "fmt" "log/slog" "path/filepath" "strings" @@ -12,6 +13,20 @@ import ( "github.com/nextlevelbuilder/goclaw/internal/store" ) +// isMemoryDir checks if a path refers to the memory directory itself. +// Handles "memory", "./memory", "/workspace/memory" etc. +func isMemoryDir(path, workspace string) bool { + clean := filepath.Clean(path) + if clean == "memory" { + return true + } + if workspace != "" && filepath.IsAbs(clean) { + expected := filepath.Join(filepath.Clean(workspace), "memory") + return clean == expected + } + return false +} + // isMemoryPath checks if a path refers to a memory file (MEMORY.md, memory.md, memory/*). // Handles both relative and absolute paths (when workspace is provided). func isMemoryPath(path, workspace string) bool { @@ -124,3 +139,32 @@ func (m *MemoryInterceptor) WriteFile(ctx context.Context, path, content string) return true, nil } + +// ListFiles lists memory documents from the DB when path is the memory directory. +// Returns (listing, true, nil) if handled, or ("", false, nil) if not a memory path. +func (m *MemoryInterceptor) ListFiles(ctx context.Context, path string) (string, bool, error) { + if !isMemoryDir(path, m.workspace) { + return "", false, nil + } + + agentID := store.AgentIDFromContext(ctx) + if agentID == uuid.Nil { + return "", false, nil + } + + userID := store.UserIDFromContext(ctx) + docs, err := m.memStore.ListDocuments(ctx, agentID.String(), userID) + if err != nil { + return "", true, err + } + + if len(docs) == 0 { + return "", true, nil + } + + var sb strings.Builder + for _, doc := range docs { + fmt.Fprintf(&sb, "[FILE] %s\n", doc.Path) + } + return sb.String(), true, nil +}