From 907f409dc4e8d74ca52c7900acb457b149220852 Mon Sep 17 00:00:00 2001 From: PINKIIILQWQ Date: Tue, 9 Jun 2026 21:13:51 +0800 Subject: [PATCH] fix(kanban): archive_task now kills the worker process MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit archive_task() was a pure DB operation — it cleared worker_pid, claim_lock, and status from the tasks row but never sent SIGTERM to the actual OS process. A running worker stayed alive until it next called kanban_complete/kanban_block and discovered it was archived, burning API quota and compute resources. Fix: snapshot pid+claim_lock before the write_txn clears them, then call _terminate_reclaimed_worker() — same function reclaim_task uses — which sends SIGTERM, waits 5s, then SIGKILL if still alive. The termination metadata is included in the 'archived' event so operators can see what happened. Order matches reclaim_task: terminate first, then DB update. For non-running / non-local tasks, _terminate_reclaimed_worker returns immediately as a no-op. Closes #33774 reprise: the scratch-workspace side was fixed in fc8afd500, but the orphaned-process side was never addressed. --- hermes_cli/kanban_db.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/hermes_cli/kanban_db.py b/hermes_cli/kanban_db.py index 4a80385b6f..04e747c57e 100644 --- a/hermes_cli/kanban_db.py +++ b/hermes_cli/kanban_db.py @@ -3495,6 +3495,18 @@ def specify_triage_task( def archive_task(conn: sqlite3.Connection, task_id: str) -> bool: + # Snapshot pid+claim before the DB update clears them, so we can + # terminate the worker process even after those columns are NULLed. + row = conn.execute( + "SELECT worker_pid, claim_lock FROM tasks WHERE id = ?", + (task_id,), + ).fetchone() + prev_pid = row["worker_pid"] if row else None + prev_lock = row["claim_lock"] if row else None + # Terminate the worker process first (same order as reclaim_task). + # If the task wasn't running on this host, this is a safe no-op. + termination = _terminate_reclaimed_worker(prev_pid, prev_lock) + with write_txn(conn): cur = conn.execute( "UPDATE tasks SET status = 'archived', " @@ -3508,7 +3520,9 @@ def archive_task(conn: sqlite3.Connection, task_id: str) -> bool: conn, task_id, outcome="reclaimed", status="reclaimed", summary="task archived with run still active", ) - _append_event(conn, task_id, "archived", None, run_id=run_id) + payload = {} + payload.update(termination) + _append_event(conn, task_id, "archived", payload, run_id=run_id) # ``archived`` parents no longer block children; promote them now. recompute_ready(conn) # Reap the workspace on archive too (never-completed tasks kept it forever).