diff --git a/crates/g3-cli/src/filter_json.rs b/crates/g3-cli/src/filter_json.rs index f4d7d6f..4116197 100644 --- a/crates/g3-cli/src/filter_json.rs +++ b/crates/g3-cli/src/filter_json.rs @@ -487,4 +487,25 @@ mod tests { let result = filter_json_tool_calls(input); assert_eq!(result, "Before\n\nAfter"); } + + #[test] + fn test_tool_call_not_at_line_start_passes_through() { + // IMPORTANT: Tool calls that don't start at a line boundary should NOT be filtered. + // This is by design - the filter only suppresses tool calls that appear at the + // start of a line (after newline + optional whitespace). + // + // This test documents the behavior that caused the "auto-memory JSON leak" bug: + // When "Memory checkpoint: " was printed without a trailing newline, the LLM's + // response `{"tool": "remember", ...}` appeared on the same line and was not + // filtered. The fix was to ensure the prompt ends with a newline AND reset + // the filter state before streaming. + // + // See: send_auto_memory_reminder() in g3-core/src/lib.rs + reset_json_tool_state(); + + // Tool call immediately after text on same line - should NOT be filtered + let input = "Memory checkpoint: {\"tool\": \"remember\", \"args\": {}}"; + let result = filter_json_tool_calls(input); + assert_eq!(result, input, "Tool calls not at line start should pass through"); + } } diff --git a/crates/g3-core/src/lib.rs b/crates/g3-core/src/lib.rs index 6d6f2ae..09a479b 100644 --- a/crates/g3-core/src/lib.rs +++ b/crates/g3-core/src/lib.rs @@ -1541,7 +1541,15 @@ impl Agent { tools_called.len(), tools_called ); - self.ui_writer.print_context_status("\nMemory checkpoint: "); + // IMPORTANT: The message MUST end with a newline so the LLM's response starts on a new line. + // The JSON filter only suppresses tool calls that appear at line boundaries (after newline). + // Without the trailing newline, tool call JSON like `{"tool": "remember", ...}` would + // appear on the same line as "Memory checkpoint:" and leak through to the UI. + // See test: test_tool_call_not_at_line_start_passes_through in filter_json.rs + self.ui_writer.print_context_status("\nMemory checkpoint:\n"); + + // Reset JSON filter state so it starts fresh for this response + self.ui_writer.reset_json_filter(); let reminder = r#"MEMORY CHECKPOINT: If you discovered code locations worth remembering, call `remember` now.