fix(chat): prevent double-escape corruption of messages/tools in agentic bridge #20
Reference in New Issue
Block a user
Delete Branch "fix/bridge-save-serialization"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
bridge_savewas callingjson_safe()onmessagesandtools_jsonbefore storing them as JSON string fields. Both values are already well-formed JSON arrays containing double quotes, sojson_safeadded a second escape layer.agentic_resumecalledjson_get()which strips only one layer, leaving the messages array corrupted before passing it back toagentic_loop.messagesasmessages_rawandtools_jsonastools_rawas inline raw JSON values (not string-escaped), and read them back withjson_get_raw. Scalar strings (model,safe_sys,tools_log,tool_use_id) remain as string fields viajson_safe/json_getsince they do not contain nested JSON structure.agentic_resumefalls back to the old string-escaped fields if the raw fields are absent, so any session suspended before this fix can still be resumed.write_filereturning a pre-escaped literal instead of callingjson_safeconsistently with every other tool result.Test plan
write_filetool result parses correctly in the tool loopReview: fix(chat): prevent double-escape corruption of messages/tools in agentic bridge
The core approach is correct — embedding
messagesandtools_jsonas raw JSON values in the bridge blob rather than string-escaping them eliminates the round-trip corruption that was silently destroying nested quotes on resume. The direction is right. Two blockers need addressing before merge.Blockers
1. Double typed
letdeclaration (chat.el, agentic_resume)Lines 798-801 use
let messages: String = ...twice in the same scope, and same fortools_json. This pattern does not appear elsewhere in the codebase — every other variable fallback uses untyped reassignment (let messages = ...). If El treats the second typed declaration as a new binding that shadows the first, the self-referential RHS (if str_eq(messages, "") { ... } else { messages }) reads from the first binding — which may work but is semantically unusual and depends on El's specific scoping semantics. If it does not permit typed re-declaration, the fallback path is dead code.Use the established untyped reassignment pattern:
Also confirm: does
json_get_rawreturn""(not"null") when the key is absent? The guard assumes"".2. No validation against empty messages/tools_json in bridge_save
The blob construction now does:
If either value is an empty string (bug in a caller, or a partially-initialised loop state), the result is invalid JSON.
state_get+json_get_rawwill then silently return""on resume, and the agentic loop will start a fresh turn with no conversation context rather than surfacing an error.Add a guard at the top of
agentic_resumeafter the fallback resolution:Warnings
session_list() fallback for empty session_index — the old
route_sessions()(removed) hit engram directly.session_list()in sessions.el needs to handle an emptysession_indexstate key on first boot or for users upgrading from pre-sessions-index builds. Worth verifying the fallback path exists there.safe_sys double-escape —
handle_chat_agenticpassessafe_sys = json_safe(system)tobridge_save, which callsjson_safe(safe_sys)again on it. On resume,json_getun-escapes once, restoring the singly-escaped form, which is then embedded directly in the API body. This is consistent but fragile — a comment confirming the invariant would prevent future breakage.Minor
The
write_filefix (json_safe("{\"ok\":true}")) is correct. A constant string throughjson_safeis harmless.The DELETE and PATCH session routes added to routes.el look structurally correct and consistent with the existing GET/POST patterns.
Missing tests
messages/tools_jsonfields, no_rawvariants)agentic_resumeroute_sessions()459a9cbff2to8db3c8c7f7