From f98c2288c99df8039ca26cd6e97bbec74f128757 Mon Sep 17 00:00:00 2001 From: Jakub Zika Date: Mon, 22 Jun 2026 19:57:41 +0200 Subject: [PATCH] Auto-clear completed task lists on new prompt When a new prompt is sent and all tasks in the task list are done, automatically clear the list so the model starts fresh. The cleared state is sent to the client as a synthetic toolCalled event, reusing the existing task-details rendering path. No protocol or client changes. --- CHANGELOG.md | 2 ++ resources/prompts/tools/task.md | 1 - src/eca/features/chat.clj | 8 +++++ src/eca/features/tools/task.clj | 19 ++++++++++- test/eca/features/tools/task_test.clj | 45 +++++++++++++++++++++++++++ 5 files changed, 73 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf0862353..4a2a2f527 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Auto-clear completed task lists: when a new prompt is sent and all tasks are done, the list is automatically cleared. + ## 0.142.2 - `/context` now shows where auto-compaction triggers: a `🔲` marker on the threshold cell of the grid plus an `Auto-compaction at N%` line. diff --git a/resources/prompts/tools/task.md b/resources/prompts/tools/task.md index 3d1dad0a3..db9661b16 100644 --- a/resources/prompts/tools/task.md +++ b/resources/prompts/tools/task.md @@ -36,4 +36,3 @@ Workflow & Strict Execution Rules: 4. SUBAGENTS & PARALLEL WORK: For sequential work, keep only one task `in_progress`; later tasks must stay pending until you start them. Multiple tasks may be `in_progress` only for concurrent separate workstreams (e.g. separate subagents). Only the main agent updates the task list. 5. COMPLETION TIMING: Once a task is verified, close it as soon as possible. Do not delay completion just to align with other tasks. 6. ADAPTABILITY: If completing tasks reveals follow-up work, use 'add' to append new tasks. -7. CLEANUP: When all tasks are done and no further work remains, use 'clear'. diff --git a/src/eca/features/chat.clj b/src/eca/features/chat.clj index 4a6f7faed..323491df7 100644 --- a/src/eca/features/chat.clj +++ b/src/eca/features/chat.clj @@ -21,6 +21,7 @@ [eca.features.skills :as f.skills] [eca.features.tools :as f.tools] [eca.features.tools.mcp :as f.mcp] + [eca.features.tools.task :as f.tools.task] [eca.llm-api :as llm-api] [eca.llm-providers.errors :as llm-providers.errors] [eca.llm-util :as llm-util] @@ -1653,6 +1654,13 @@ _ (when (and seeded-default-trust? provided-chat-id) (config/notify-fields-changed-only! {:chat {:select-trust true}} messenger db* chat-id))] (logger/with-chat-context chat-id (:parent-chat-id base-chat-ctx) + (when-let [cleared-details (f.tools.task/auto-clear-completed! db* chat-id)] + (logger/info logger-tag "Auto-cleared completed task list" {:chat-id chat-id}) + (lifecycle/send-content! base-chat-ctx :assistant + {:type :toolCalled + :server "eca" + :name "task" + :details cleared-details})) (try (prompt* params base-chat-ctx) (catch Exception e diff --git a/src/eca/features/tools/task.clj b/src/eca/features/tools/task.clj index 4eae27156..8f06f066a 100644 --- a/src/eca/features/tools/task.clj +++ b/src/eca/features/tools/task.clj @@ -256,6 +256,23 @@ [_name _arguments before-details result _ctx] (or (:details result) before-details)) +;; --- Auto-clear --- + +(defn ^:private all-tasks-done? + "Returns true if the task list has tasks and all are :done." + [state] + (let [tasks (:tasks state) + {:keys [done pending in-progress]} (status-counts tasks)] + (and (pos? done) (zero? pending) (zero? in-progress)))) + +(defn auto-clear-completed! + "Clear the task list when all tasks are done. + Returns task-details of the cleared state if clearing occurred, nil otherwise." + [db* chat-id] + (when (all-tasks-done? (get-task @db* chat-id)) + (mutate-task! db* chat-id (fn [_] {:state empty-task})) + (task-details empty-task))) + ;; --- Operations --- (defn ^:private op-read [_arguments {:keys [db chat-id]}] @@ -502,7 +519,7 @@ :items {:type "integer"} :description "Task IDs (required for start/complete/delete)"} :active_summary {:type "string" - :description "Summary of what will be done in the current active session. Required for start operation."} + :description "Summary of what will be done in the current active session. Required for start operation."} :task {:type "object" :description "Single task data (for add/update)" :properties {:subject {:type "string" :description "Task subject/title (required)"} diff --git a/test/eca/features/tools/task_test.clj b/test/eca/features/tools/task_test.clj index 1f2fcced5..3df77f69d 100644 --- a/test/eca/features/tools/task_test.clj +++ b/test/eca/features/tools/task_test.clj @@ -482,3 +482,48 @@ result nil)] (is (= (:details result) details))))) + +(deftest auto-clear-completed-test + (testing "clears task list when all tasks are done" + (let [db* (atom {:chats {"c1" {:task {:next-id 3 + :active-summary nil + :tasks [{:id 1 :subject "T1" :description "D1" :status :done :priority :medium :blocked-by #{}} + {:id 2 :subject "T2" :description "D2" :status :done :priority :medium :blocked-by #{}}]}}}}) + result (task/auto-clear-completed! db* "c1")] + (is (some? result)) + (is (= :task (:type result))) + (is (empty? (:tasks result))) + (is (= {:next-id 1 :active-summary nil :tasks []} + (task/get-task @db* "c1"))))) + + (testing "does not clear when tasks are still pending" + (let [db* (atom {:chats {"c1" {:task {:next-id 3 + :active-summary nil + :tasks [{:id 1 :subject "T1" :description "D1" :status :done :priority :medium :blocked-by #{}} + {:id 2 :subject "T2" :description "D2" :status :pending :priority :medium :blocked-by #{}}]}}}}) + result (task/auto-clear-completed! db* "c1")] + (is (nil? result)) + (is (= 2 (count (:tasks (task/get-task @db* "c1"))))))) + + (testing "does not clear when tasks are in progress" + (let [db* (atom {:chats {"c1" {:task {:next-id 3 + :active-summary "working" + :tasks [{:id 1 :subject "T1" :description "D1" :status :done :priority :medium :blocked-by #{}} + {:id 2 :subject "T2" :description "D2" :status :in-progress :priority :medium :blocked-by #{}}]}}}}) + result (task/auto-clear-completed! db* "c1")] + (is (nil? result)) + (is (= 2 (count (:tasks (task/get-task @db* "c1"))))))) + + (testing "does nothing when task list is empty" + (let [db* (atom {:chats {"c1" {:task {:next-id 1 :active-summary nil :tasks []}}}}) + result (task/auto-clear-completed! db* "c1")] + (is (nil? result)) + (is (= {:next-id 1 :active-summary nil :tasks []} + (task/get-task @db* "c1"))))) + + (testing "does nothing when chat has no task list" + (let [db* (atom {}) + result (task/auto-clear-completed! db* "c1")] + (is (nil? result)) + (is (= {:next-id 1 :active-summary nil :tasks []} + (task/get-task @db* "c1"))))))