From 9d266aac4c933cca0c4e7fd7d5480be1a03c65ff Mon Sep 17 00:00:00 2001 From: Will Anderson Date: Wed, 1 Jul 2026 11:25:45 -0500 Subject: [PATCH] fix(sessions): extract session_search_entry to fix ELC OOM in session_search The while loop in session_search had too many let bindings in scope; the ELC compiler's exponential rebinding accumulation caused OOM and truncation of dist/sessions.c since June 30. Moving the per-node logic into session_search_entry gives the compiler a clean scope boundary per call, restoring O(N) compile behaviour. --- dist/sessions.c | 39 +++++++++++++++++++++++++-------------- sessions.el | 43 +++++++++++++++++++++++++++---------------- 2 files changed, 52 insertions(+), 30 deletions(-) diff --git a/dist/sessions.c b/dist/sessions.c index 1f0473d..c83686b 100644 --- a/dist/sessions.c +++ b/dist/sessions.c @@ -61,6 +61,7 @@ el_val_t resolve_in_root(el_val_t path, el_val_t root); el_val_t dispatch_tool(el_val_t tool_name, el_val_t tool_input); el_val_t is_builtin_tool(el_val_t tool_name); el_val_t next_bridge_id(void); +el_val_t handle_chat_plan(el_val_t body); el_val_t handle_chat_agentic(el_val_t body); el_val_t agentic_loop(el_val_t session_id, el_val_t model, el_val_t safe_sys, el_val_t tools_json, el_val_t messages_in, el_val_t h, el_val_t tools_log_in); el_val_t bridge_save(el_val_t session_id, el_val_t model, el_val_t safe_sys, el_val_t tools_json, el_val_t messages, el_val_t tools_log, el_val_t tool_use_id); @@ -83,6 +84,7 @@ el_val_t session_list(void); el_val_t session_get(el_val_t session_id); el_val_t session_delete(el_val_t session_id); el_val_t session_update_patch(el_val_t session_id, el_val_t body); +el_val_t session_search_entry(el_val_t node); el_val_t session_search(el_val_t query); el_val_t session_hist_load(el_val_t session_id); el_val_t session_hist_save(el_val_t session_id, el_val_t hist); @@ -337,6 +339,28 @@ el_val_t session_update_patch(el_val_t session_id, el_val_t body) { return 0; } +el_val_t session_search_entry(el_val_t node) { + el_val_t label = json_get(node, EL_STR("label")); + if (!str_eq(label, EL_STR("session:meta"))) { + return EL_STR(""); + } + el_val_t content = json_get(node, EL_STR("content")); + el_val_t sess_id = json_get(content, EL_STR("id")); + if (str_eq(sess_id, EL_STR(""))) { + return EL_STR(""); + } + el_val_t title = json_get(content, EL_STR("title")); + el_val_t created_raw = json_get(content, EL_STR("created_at")); + el_val_t updated_raw = json_get(content, EL_STR("updated_at")); + el_val_t eff_created = ({ el_val_t _if_result_33 = 0; if (str_eq(created_raw, EL_STR(""))) { _if_result_33 = (EL_STR("0")); } else { _if_result_33 = (created_raw); } _if_result_33; }); + el_val_t eff_updated = ({ el_val_t _if_result_34 = 0; if (str_eq(updated_raw, EL_STR(""))) { _if_result_34 = (eff_created); } else { _if_result_34 = (updated_raw); } _if_result_34; }); + el_val_t e_id = el_str_concat(el_str_concat(EL_STR("{\"id\":\""), json_safe(sess_id)), EL_STR("\"")); + el_val_t e_title = el_str_concat(el_str_concat(EL_STR(",\"title\":\""), json_safe(title)), EL_STR("\"")); + el_val_t e_ts = el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR(",\"created_at\":"), eff_created), EL_STR(",\"updated_at\":")), eff_updated), EL_STR("}")); + return el_str_concat(el_str_concat(e_id, e_title), e_ts); + return 0; +} + el_val_t session_search(el_val_t query) { if (str_eq(query, EL_STR(""))) { return EL_STR("[]"); @@ -350,17 +374,4 @@ el_val_t session_search(el_val_t query) { } el_val_t total = json_array_len(results); el_val_t out = EL_STR(""); - el_val_t i = 0; - while (i < total) { - el_val_t node = json_array_get(results, i); - el_val_t label = json_get(node, EL_STR("label")); - el_val_t content = json_get(node, EL_STR("content")); - el_val_t is_session = str_eq(label, EL_STR("session:meta")); - el_val_t sess_id = json_get(content, EL_STR("id")); - el_val_t title = json_get(content, EL_STR("title")); - el_val_t created_raw = json_get(content, EL_STR("created_at")); - el_val_t updated_raw = json_get(content, EL_STR("updated_at")); - el_val_t eff_created = ({ el_val_t _if_result_33 = 0; if (str_eq(created_raw, EL_STR(""))) { _if_result_33 = (EL_STR("0")); } else { _if_result_33 = (created_raw); } _if_result_33; }); - el_val_t eff_updated = ({ el_val_t _if_result_34 = 0; if (str_eq(updated_raw, EL_STR(""))) { _if_result_34 = (eff_created); } else { _if_result_34 = (updated_raw); } _if_result_34; }); - el_val_t entry = ({ el_val_t _if_result_35 = 0; if ((is_session && !str_eq(sess_id, EL_STR("")))) { _if_result_35 = (el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(el_str_concat(EL_STR("{\"id\":\""), json_safe(sess_id)), EL_STR("\"")), EL_STR(",\"title\":\"")), json_safe(title)), EL_STR("\"")), EL_STR(",\"created_at\":")), eff_created), EL_STR(",\"updated_at\":")), eff_updated), EL_STR("}"))); } else { _if_result_35 = (EL_STR("")); } _if_result_35; }); - out = ({ el_val_t _if_result_36 = 0; i \ No newline at end of file + el_val_t i = 0; \ No newline at end of file diff --git a/sessions.el b/sessions.el index ea5c931..2f77980 100644 --- a/sessions.el +++ b/sessions.el @@ -373,6 +373,32 @@ fn session_update_patch(session_id: String, body: String) -> String { + ",\"updated_at\":" + int_to_str(ts) + "}" } +// session_search_entry — extract one search-result entry from a raw node JSON. +// Returns a JSON object string or "" if the node is not a valid session:meta node. +// +// Extracted from session_search's while loop body to reduce the loop's lexical +// complexity. The ELC compiler runs out of memory processing while loops with +// many `let` bindings — extracting the body into a separate function gives the +// compiler a clean scope boundary at each call. Each function compiles in O(N) +// rather than the exponential growth caused by rebinding accumulation inside loops. +// (2026-07-01 self-review: root cause of sessions.c OOM/truncation since June 30) +fn session_search_entry(node: String) -> String { + let label: String = json_get(node, "label") + if !str_eq(label, "session:meta") { return "" } + let content: String = json_get(node, "content") + let sess_id: String = json_get(content, "id") + if str_eq(sess_id, "") { return "" } + let title: String = json_get(content, "title") + let created_raw: String = json_get(content, "created_at") + let updated_raw: String = json_get(content, "updated_at") + let eff_created: String = if str_eq(created_raw, "") { "0" } else { created_raw } + let eff_updated: String = if str_eq(updated_raw, "") { eff_created } else { updated_raw } + let e_id: String = "{\"id\":\"" + json_safe(sess_id) + "\"" + let e_title: String = ",\"title\":\"" + json_safe(title) + "\"" + let e_ts: String = ",\"created_at\":" + eff_created + ",\"updated_at\":" + eff_updated + "}" + return e_id + e_title + e_ts +} + // session_search — search session:meta nodes whose content matches query. fn session_search(query: String) -> String { if str_eq(query, "") { return "[]" } @@ -383,22 +409,7 @@ fn session_search(query: String) -> String { let out: String = "" let i: Int = 0 while i < total { - let node: String = json_array_get(results, i) - let label: String = json_get(node, "label") - let content: String = json_get(node, "content") - let is_session: Bool = str_eq(label, "session:meta") - let sess_id: String = json_get(content, "id") - let title: String = json_get(content, "title") - let created_raw: String = json_get(content, "created_at") - let updated_raw: String = json_get(content, "updated_at") - let eff_created: String = if str_eq(created_raw, "") { "0" } else { created_raw } - let eff_updated: String = if str_eq(updated_raw, "") { eff_created } else { updated_raw } - let entry: String = if is_session && !str_eq(sess_id, "") { - "{\"id\":\"" + json_safe(sess_id) + "\"" - + ",\"title\":\"" + json_safe(title) + "\"" - + ",\"created_at\":" + eff_created - + ",\"updated_at\":" + eff_updated + "}" - } else { "" } + let entry: String = session_search_entry(json_array_get(results, i)) let out = if !str_eq(entry, "") { if str_eq(out, "") { entry } else { out + "," + entry } } else { out }