fix(chat): store bridge messages/tools as raw JSON to prevent double-escape corruption on agentic_resume
Neuron Soul CI / build (pull_request) Failing after 12m13s

bridge_save was wrapping messages and tools_json with json_safe() before
storing them as string fields. Since both are already well-formed JSON arrays
containing double quotes, json_safe added a second escape layer. agentic_resume
then called json_get() which stripped only one layer, leaving the messages array
corrupted before it was passed back into agentic_loop.

Fix: store messages as messages_raw and tools_json as tools_raw as inline raw
JSON values (unquoted), and read them back with json_get_raw. Backward
compatibility: fall back to the old string-escaped fields if the raw fields are
absent, so sessions saved before this fix can still be resumed.

Also fixes write_file returning a pre-escaped literal instead of calling
json_safe consistently with every other tool result.
This commit is contained in:
2026-06-15 13:04:51 -05:00
parent dde039b09a
commit 644d9915bf
+13 -5
View File
@@ -387,7 +387,7 @@ fn dispatch_tool(tool_name: String, tool_input: String) -> String {
let path: String = json_get(tool_input, "path")
let content: String = json_get(tool_input, "content")
fs_write(path, content)
return "{\\\"ok\\\":true}"
return json_safe("{\"ok\":true}")
}
if str_eq(tool_name, "web_get") {
let url: String = json_get(tool_input, "url")
@@ -766,10 +766,14 @@ fn agentic_loop(session_id: String, model: String, safe_sys: String, tools_json:
// stored `messages` already includes the assistant turn that requested the tool, so
// resume just appends the client's tool_result for `tool_use_id`.
fn bridge_save(session_id: String, model: String, safe_sys: String, tools_json: String, messages: String, tools_log: String, tool_use_id: String) -> Bool {
// messages and tools_json are already well-formed JSON arrays; embed them as raw
// JSON values (not string-escaped) so the round-trip through state_get/json_get_raw
// never corrupts nested quotes. Scalar strings (model, safe_sys, tools_log,
// tool_use_id) stay as string fields via json_safe as before.
let blob: String = "{\"model\":\"" + json_safe(model) + "\""
+ ",\"safe_sys\":\"" + json_safe(safe_sys) + "\""
+ ",\"tools_json\":\"" + json_safe(tools_json) + "\""
+ ",\"messages\":\"" + json_safe(messages) + "\""
+ ",\"messages_raw\":" + messages
+ ",\"tools_raw\":" + tools_json
+ ",\"tools_log\":\"" + json_safe(tools_log) + "\""
+ ",\"tool_use_id\":\"" + json_safe(tool_use_id) + "\"}"
state_set("mcp_bridge:" + session_id, blob)
@@ -789,8 +793,12 @@ fn agentic_resume(session_id: String, tool_use_id: String, content: String) -> S
let model: String = json_get(blob, "model")
let safe_sys: String = json_get(blob, "safe_sys")
let tools_json: String = json_get(blob, "tools_json")
let messages: String = json_get(blob, "messages")
// messages_raw and tools_raw are embedded as raw JSON (not string-escaped);
// fall back to legacy string-escaped fields for sessions saved before this fix.
let messages: String = json_get_raw(blob, "messages_raw")
let messages: String = if str_eq(messages, "") { json_get(blob, "messages") } else { messages }
let tools_json: String = json_get_raw(blob, "tools_raw")
let tools_json: String = if str_eq(tools_json, "") { json_get(blob, "tools_json") } else { tools_json }
let tools_log: String = json_get(blob, "tools_log")
let saved_use_id: String = json_get(blob, "tool_use_id")