Fix auto-memory JSON leak: tool call printed raw to UI

The JSON filter only suppresses tool calls at line boundaries. When
"Memory checkpoint: " was printed without a trailing newline, the LLM
response `{"tool": "remember", ...}` appeared on the same line and
leaked through to the UI.

Fix:
- Add trailing newline to "Memory checkpoint:" message
- Reset JSON filter state before streaming the response

Added test: test_tool_call_not_at_line_start_passes_through
Documents the filter behavior and references the fix location.
This commit is contained in:
Dhanji R. Prasanna
2026-01-16 13:10:18 +05:30
parent 94544c8f6a
commit 7c59d1993c
2 changed files with 30 additions and 1 deletions

View File

@@ -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");
}
}

View File

@@ -1541,7 +1541,15 @@ impl<W: UiWriter> Agent<W> {
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.