From 7ba3a5115aa8b5f62fa7331a55e4c52b957f184d Mon Sep 17 00:00:00 2001 From: viettranx Date: Thu, 2 Apr 2026 19:28:04 +0700 Subject: [PATCH] fix(tools): hint model when shell commands target DB-backed memory files Memory files (MEMORY.md, memory/*.md) are stored in PostgreSQL, not on disk. When the model uses exec to run cat/ls/grep/find on memory paths, short-circuit with a hint to use memory_search or memory_get instead. Also append [Source: database, not filesystem] to read_file and list_files results served from the memory interceptor. --- internal/tools/exec_memory_hints.go | 47 +++++++++++++++++++++++++++++ internal/tools/filesystem.go | 2 +- internal/tools/filesystem_list.go | 2 +- internal/tools/shell.go | 5 +++ 4 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 internal/tools/exec_memory_hints.go diff --git a/internal/tools/exec_memory_hints.go b/internal/tools/exec_memory_hints.go new file mode 100644 index 00000000..f4710e7c --- /dev/null +++ b/internal/tools/exec_memory_hints.go @@ -0,0 +1,47 @@ +package tools + +import "strings" + +// memoryHintPrefixes are shell commands that read/list/search filesystem content. +// If these target memory paths, the model should use dedicated memory tools instead. +var memoryHintPrefixes = []string{ + "cat ", "head ", "tail ", "less ", "more ", + "grep ", "rg ", "ag ", + "find ", "ls ", "wc ", +} + +// memoryPathTokens are path fragments indicating memory file access. +var memoryPathTokens = []string{ + "MEMORY.md", "memory.md", "memory/", +} + +// MaybeMemoryExecHint checks if a shell command targets memory files +// (which live in the database, not on disk) and returns an LLM hint. +// Returns empty string if no memory path detected. +func MaybeMemoryExecHint(command string) string { + trimmed := strings.TrimSpace(command) + if trimmed == "" { + return "" + } + + // Check if command uses a filesystem read/list/search binary. + hasReadCmd := false + for _, prefix := range memoryHintPrefixes { + if strings.HasPrefix(trimmed, prefix) { + hasReadCmd = true + break + } + } + if !hasReadCmd { + return "" + } + + // Check if the command references a memory path. + for _, token := range memoryPathTokens { + if strings.Contains(trimmed, token) { + return "[HINT] Memory files are in the database, not on disk. Use memory_search or memory_get tool instead." + } + } + + return "" +} diff --git a/internal/tools/filesystem.go b/internal/tools/filesystem.go index 7505a2a9..c0e89319 100644 --- a/internal/tools/filesystem.go +++ b/internal/tools/filesystem.go @@ -139,7 +139,7 @@ func (t *ReadFileTool) Execute(ctx context.Context, args map[string]any) *Result if content == "" { return SilentResult(fmt.Sprintf("(memory file %s does not exist yet — it will be created when memory is saved)", path)) } - return SilentResult(content) + return SilentResult(content + "\n\n[Source: database, not filesystem]") } } diff --git a/internal/tools/filesystem_list.go b/internal/tools/filesystem_list.go index 36ebea91..409a7239 100644 --- a/internal/tools/filesystem_list.go +++ b/internal/tools/filesystem_list.go @@ -73,7 +73,7 @@ func (t *ListFilesTool) Execute(ctx context.Context, args map[string]any) *Resul if listing == "" { return SilentResult("No memory files stored yet") } - return SilentResult(listing) + return SilentResult(listing + "\n[Source: database, not filesystem]") } } diff --git a/internal/tools/shell.go b/internal/tools/shell.go index a843a23b..5d7e1829 100644 --- a/internal/tools/shell.go +++ b/internal/tools/shell.go @@ -199,6 +199,11 @@ func (t *ExecTool) Execute(ctx context.Context, args map[string]any) *Result { } } + // Memory path hint: shell commands can't access DB-backed memory files. + if hint := MaybeMemoryExecHint(normalizedCommand); hint != "" { + return SilentResult(hint) + } + // Credentialed exec: if command matches a configured binary, use Direct Exec Mode. // This bypasses approval (admin trust) and shell (security). if cred, binary, cmdArgs := t.lookupCredentialedBinary(ctx, command); cred != nil {