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 <noreply@anthropic.com>
This commit is contained in:
viettranxandClaude Opus 4.6 committed 2026-03-05 09:35:38 +07:00
1 parent a9ce64981d
commit 6d9374a1a5
3 files changed
+78 -4

No files matched your search

+7
View File
@@ -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 {
+27 -4
View File
@@ -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 != "" {
+44
View File
@@ -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
}