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 {