From 5e291f47ec5e70fecd9fffd40642d77c5f646718 Mon Sep 17 00:00:00 2001 From: MrnoRac Date: Thu, 25 Jun 2026 10:23:24 +0200 Subject: [PATCH] Use chat map key as authoritative id in remote endpoints editor-open-chats is keyed by the map key, but the handlers filtered and returned chats by the record's :id field, which can drift from the key for runtime-created chats. Filter/report by key in handle-list-chats and session-state, and return the path-param key in handle-get-chat. Fixes drifted chats vanishing from the web list (and on reload) and losing messages on tab switch. --- CHANGELOG.md | 1 + src/eca/remote/handlers.clj | 18 +++++++++--------- test/eca/remote/handlers_test.clj | 25 ++++++++++++++++++++++++- 3 files changed, 34 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a2a2f527..adfa37ccf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unreleased - Auto-clear completed task lists: when a new prompt is sent and all tasks are done, the list is automatically cleared. +- Bugfix: remote HTTP endpoints (`/chats`, `/session`, `/chats/:id`) now use the chat map key as the authoritative id instead of the record's `:id` field. When the two drift apart for a runtime-created chat, the chat no longer disappears from the web UI list (and on reload), and no longer loses its messages when switching tabs. ## 0.142.2 diff --git a/src/eca/remote/handlers.clj b/src/eca/remote/handlers.clj index cfbf4e1c6..4f785d045 100644 --- a/src/eca/remote/handlers.clj +++ b/src/eca/remote/handlers.clj @@ -126,10 +126,10 @@ {:name name :status (or (:status client-info) "unknown")}) (:mcp-clients db)) :chats (let [editor-open (:editor-open-chats db)] - (->> (vals (:chats db)) - (remove :subagent) - (filter #(contains? editor-open (:id %))) - (mapv chat-summary))) + (->> (:chats db) + (remove (fn [[_ chat]] (:subagent chat))) + (filter (fn [[id _]] (contains? editor-open id))) + (mapv (fn [[id chat]] (chat-summary (assoc chat :id id)))))) :startedAt (when-let [ms (:started-at db)] (.toString (Instant/ofEpochMilli ^long ms))) :welcomeMessage (handlers/welcome-message config) @@ -158,10 +158,10 @@ (defn handle-list-chats [{:keys [db*]} _request] (let [db @db* editor-open (:editor-open-chats db) - chats (->> (vals (:chats db)) - (remove :subagent) - (filter #(contains? editor-open (:id %))) - (mapv chat-summary))] + chats (->> (:chats db) + (remove (fn [[_ chat]] (:subagent chat))) + (filter (fn [[id _]] (contains? editor-open id))) + (mapv (fn [[id chat]] (chat-summary (assoc chat :id id)))))] (json-response chats))) (defn handle-get-chat [{:keys [db*]} request chat-id] @@ -176,7 +176,7 @@ {:limit (:limit params) :before (:before params) :after (:after params)})) - base {:id (:id chat) + base {:id chat-id :title (:title chat) :status (or (:status chat) :idle) :created-at (:created-at chat) diff --git a/test/eca/remote/handlers_test.clj b/test/eca/remote/handlers_test.clj index 7077ddc59..74b8e74e1 100644 --- a/test/eca/remote/handlers_test.clj +++ b/test/eca/remote/handlers_test.clj @@ -71,7 +71,18 @@ by-id (into {} (map (juxt :id identity) body))] (is (= 2 (count body))) (is (= 2 (get-in by-id ["c1" :pendingApprovalCount]))) - (is (= 0 (get-in by-id ["c2" :pendingApprovalCount])))))) + (is (= 0 (get-in by-id ["c2" :pendingApprovalCount]))))) + + (testing "uses the map key as the authoritative id when the :id field has drifted" + ;; editor-open-chats is keyed by the map key; a runtime chat's :id field + ;; may differ from its key. The list must still surface it, reported by key. + (swap! (h/db*) assoc + :chats {"key-1" {:id "stale-id" :title "Drifted" :status :idle}} + :editor-open-chats #{"key-1"}) + (let [response (handlers/handle-list-chats (components) nil) + body (json/parse-string (:body response) true)] + (is (= 1 (count body))) + (is (= "key-1" (:id (first body))))))) (deftest handle-get-chat-test (testing "returns 404 for missing chat" @@ -88,6 +99,18 @@ (is (= "c1" (:id body))) (is (= "My Chat" (:title body))))) + (testing "returns the requested key as id even when the stored :id field has drifted" + ;; The map key (path param) is authoritative; the client selects/restores + ;; by it, so the response must echo it rather than the stale :id field. + (swap! (h/db*) assoc-in [:chats "key-1"] + {:id "stale-id" :title "Drifted" :status :idle + :messages [{:role "user" :content [{:type "text" :text "hi"}]}]}) + (let [response (handlers/handle-get-chat (components) nil "key-1") + body (json/parse-string (:body response) true)] + (is (= 200 (:status response))) + (is (= "key-1" (:id body))) + (is (= 1 (count (:messages body)))))) + (testing "pendingToolCalls is an empty array when no tool calls are waiting" (swap! (h/db*) assoc-in [:chats "c1"] {:id "c1" :title "T" :status :idle}) (let [response (handlers/handle-get-chat (components) nil "c1")