From f47c92a71a21c913c8cdb702cfb671b15cd1bae3 Mon Sep 17 00:00:00 2001 From: Tim Lingo <1timlingo@gmail.com> Date: Sat, 27 Jun 2026 12:25:26 -0500 Subject: [PATCH 1/3] feat: vision in the agentic chat path (image content block) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit handle_chat_agentic now reads body image + image_media_type and, when present, sends the current user turn as an Anthropic content-block array [{text},{image}] instead of a plain string — so the model sees raw pixels alongside memory, history, and tools (parity with the CLI). Additive: no image => output byte-identical to before. elc-clean. Pairs with neuron-ui fix/chat-vision-attachments. Co-Authored-By: Claude Opus 4.8 (1M context) --- chat.el | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/chat.el b/chat.el index 46beed9..e3094b4 100644 --- a/chat.el +++ b/chat.el @@ -1688,12 +1688,25 @@ fn handle_chat_agentic(body: String) -> String { let safe_msg: String = json_safe(message) let safe_sys: String = json_safe(system) + // Vision in the agentic brain (2026-06-27): when the client attaches an image + // (base64 in body "image", mime in "image_media_type"), send it as a real Anthropic + // image content block on THIS user turn — so the model sees raw pixels WITH memory, + // history, and tools (parity with the CLI). img_b64 == "" => byte-identical to before. + let img_b64: String = json_get(body, "image") + let img_mt_raw: String = json_get(body, "image_media_type") + let img_mt: String = if str_eq(img_mt_raw, "") { "image/png" } else { img_mt_raw } + let cur_user_content: String = if str_eq(img_b64, "") { + "\"" + safe_msg + "\"" + } else { + "[{\"type\":\"text\",\"text\":\"" + safe_msg + "\"},{\"type\":\"image\",\"source\":{\"type\":\"base64\",\"media_type\":\"" + img_mt + "\",\"data\":\"" + img_b64 + "\"}}]" + } + // Seed the messages array with recent history if available, so the LLM sees the thread. let prior_messages: String = if agentic_hist_len > 0 { let inner: String = str_slice(agentic_hist, 1, str_len(agentic_hist) - 1) - "[" + inner + ",{\"role\":\"user\",\"content\":\"" + safe_msg + "\"}]" + "[" + inner + ",{\"role\":\"user\",\"content\":" + cur_user_content + "}]" } else { - "[{\"role\":\"user\",\"content\":\"" + safe_msg + "\"}]" + "[{\"role\":\"user\",\"content\":" + cur_user_content + "}]" } let messages: String = prior_messages let api_url: String = "https://api.anthropic.com/v1/messages" From cec2aa716809bfa3f7277eca49a55e8dcdfa97ee Mon Sep 17 00:00:00 2001 From: Tim Lingo <1timlingo@gmail.com> Date: Sat, 27 Jun 2026 14:42:57 -0500 Subject: [PATCH 2/3] =?UTF-8?q?feat(connectors):=20/api/connectors/call=20?= =?UTF-8?q?=E2=80=94=20proxy=20a=20connector=20tool=20call=20(pre-chat)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds /api/connectors/call -> connectd /mcp/call, so the app can invoke a connector tool (e.g. WhatsApp get_pairing_qr / get_login_status for the pairing UI) through the soul, keeping app->soul->connectd intact (UI never hits connectd directly) and working for future remote/hosted clients. elc-clean. NOTE: soul-core change — needs dist/soul.c regen (Will), can ride the same rebuild as PR #56. Co-Authored-By: Claude Opus 4.8 (1M context) --- routes.el | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/routes.el b/routes.el index 3b5b12d..f224409 100644 --- a/routes.el +++ b/routes.el @@ -335,6 +335,12 @@ fn handle_connectors(method: String, clean: String, body: String) -> String { if str_eq(clean, "/api/connectors/oauth/start") { return connectd_post("/mcp/oauth/start", body) } + // Call a connector tool directly (pre-chat), e.g. WhatsApp get_pairing_qr / get_login_status for + // the pairing UI. Body: {"name":"mcp____","input":{...}}. Keeps the app on the + // app->soul->connectd path (the UI never hits connectd directly) and works for remote/hosted apps. + if str_eq(clean, "/api/connectors/call") { + return connectd_post("/mcp/call", body) + } return "{\"ok\":false,\"error\":\"unknown connectors route\"}" } From 3ad9dc7df7f91bde69d9b2a822b0745a840d5a73 Mon Sep 17 00:00:00 2001 From: Tim Lingo <1timlingo@gmail.com> Date: Sat, 27 Jun 2026 15:59:37 -0500 Subject: [PATCH 3/3] =?UTF-8?q?fix(api):=20/api/neuron/list/=20off-b?= =?UTF-8?q?y-one=20=E2=80=94=20slice=2016->17?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit str_slice(clean, 16, ...) left a leading slash on node_type ('/BacklogItem'), so engram_scan_nodes_by_type_json matched nothing and list/ returned [] for EVERY type — silently breaking backlog + typed-node listing across the app and MCP tools (reviewBacklog). Proven live: the literal-scan endpoint /api/neuron/knowledge returns nodes; /api/neuron/list/Knowledge returned []. elc-clean. NOTE: soul-core — needs dist/soul.c regen (Will); rides the same rebuild as #56/#57. Co-Authored-By: Claude Opus 4.8 (1M context) --- routes.el | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/routes.el b/routes.el index 3b5b12d..489da8c 100644 --- a/routes.el +++ b/routes.el @@ -459,7 +459,10 @@ fn handle_request(method: String, path: String, body: String) -> String { return handle_api_inspect_graph(method, path, body) } if str_starts_with(clean, "/api/neuron/list/") { - let node_type: String = str_slice(clean, 16, str_len(clean)) + // Offset 17 = len("/api/neuron/list/"). Was 16, which left a leading "/" on node_type + // ("/BacklogItem"), so engram_scan_nodes_by_type_json matched nothing → list/ + // returned [] for EVERY type (broke backlog/typed-node listing app- and tool-wide). + let node_type: String = str_slice(clean, 17, str_len(clean)) return handle_api_list_typed(node_type, path, body) } if str_starts_with(clean, "/api/neuron/recall") {