Explorar o código

r1: delete_task/delete_task_rows dedup + terminal/park stamping centralised — the five-loop row teardown lives once in delete_task_rows (the human Delete and the goal cascade's quiet delete share it); mark_terminal owns the complete/fail/cancel status flip, completed_at stamp and summary precedence; set_parked owns the suspended+suspended_at stamp (the daemon's 'suspended' push calls it directly), all semantics byte-identical to the three inline copies.

clanker hai 1 mes
pai
achega
533278fa51
Modificáronse 2 ficheiros con 78 adicións e 90 borrados
  1. 1 1
      src/Services/SaddleService.vala
  2. 77 89
      src/Services/TaskService.vala

+ 1 - 1
src/Services/SaddleService.vala

@@ -1673,7 +1673,7 @@ namespace ClankerSaddle.Services {
                     // the web drives resumption — a wake of the affine
                     // saddle once a goal event lands, the resume
                     // assignment carrying the coalesced report.
-                    yield tasks.suspend_task(task_id);
+                    yield tasks.set_parked(task_id);
                     yield park_if_current(saddle.id, task_id);
                     break;
                 case "working":

+ 77 - 89
src/Services/TaskService.vala

@@ -616,45 +616,56 @@ namespace ClankerSaddle.Services {
         }
 
         /**
-         * The delete the goal cascade uses on its own draft tasks:
-         * every attached row dies with the task and no dependents
-         * guard applies (the whole goal is going away together).
+         * The row teardown every task deletion runs (the shared core
+         * of the human Delete action and the goal cascade's quiet
+         * delete): attached rules, capabilities, progress, comments
+         * and blocks die with the task, predecessor references clear,
+         * the row goes and the pulse fires.
          */
-        public async void delete_task_quietly(int64 id) throws Error {
-            var task = yield db.query<TaskEntity>()
-                .where(expr("id == $0", elem<int64?>(id)))
-                .first_async();
-            if (task == null) {
-                return;
-            }
+        private async void delete_task_rows(TaskEntity task) throws Error {
             foreach (var link in yield db.query<TaskRule>()
-                    .where(expr("task_id == $0", elem<int64?>(id)))
+                    .where(expr("task_id == $0", elem<int64?>(task.id)))
                     .materialise_async()) {
                 yield db.delete_async<TaskRule>(link);
             }
             foreach (var link in yield db.query<TaskCapability>()
-                    .where(expr("task_id == $0", elem<int64?>(id)))
+                    .where(expr("task_id == $0", elem<int64?>(task.id)))
                     .materialise_async()) {
                 yield db.delete_async<TaskCapability>(link);
             }
             foreach (var row in yield db.query<TaskProgress>()
-                    .where(expr("task_id == $0", elem<int64?>(id)))
+                    .where(expr("task_id == $0", elem<int64?>(task.id)))
                     .materialise_async()) {
                 yield db.delete_async<TaskProgress>(row);
             }
             foreach (var row in yield db.query<TaskComment>()
-                    .where(expr("task_id == $0", elem<int64?>(id)))
+                    .where(expr("task_id == $0", elem<int64?>(task.id)))
                     .materialise_async()) {
                 yield db.delete_async<TaskComment>(row);
             }
             foreach (var row in yield db.query<TaskBlock>()
-                    .where(expr("task_id == $0", elem<int64?>(id)))
+                    .where(expr("task_id == $0", elem<int64?>(task.id)))
                     .materialise_async()) {
                 yield db.delete_async<TaskBlock>(row);
             }
-            yield clear_enqueue_after_references(id);
-            yield db.delete_async<TaskEntity>((!)task);
-            yield pulse.bump(id, "task deleted");
+            yield clear_enqueue_after_references(task.id);
+            yield db.delete_async<TaskEntity>(task);
+            yield pulse.bump(task.id, "task deleted");
+        }
+
+        /**
+         * The delete the goal cascade uses on its own draft tasks:
+         * every attached row dies with the task and no dependents
+         * guard applies (the whole goal is going away together).
+         */
+        public async void delete_task_quietly(int64 id) throws Error {
+            var task = yield db.query<TaskEntity>()
+                .where(expr("id == $0", elem<int64?>(id)))
+                .first_async();
+            if (task == null) {
+                return;
+            }
+            yield delete_task_rows((!)task);
         }
 
         /**
@@ -675,35 +686,7 @@ namespace ClankerSaddle.Services {
                 throw new Error(Quark.from_string("clanker-saddle"), 53,
                     "Only draft tasks can be deleted (cancel or retry handles the rest)");
             }
-
-            foreach (var link in yield db.query<TaskRule>()
-                    .where(expr("task_id == $0", elem<int64?>(id)))
-                    .materialise_async()) {
-                yield db.delete_async<TaskRule>(link);
-            }
-            foreach (var link in yield db.query<TaskCapability>()
-                    .where(expr("task_id == $0", elem<int64?>(id)))
-                    .materialise_async()) {
-                yield db.delete_async<TaskCapability>(link);
-            }
-            foreach (var row in yield db.query<TaskProgress>()
-                    .where(expr("task_id == $0", elem<int64?>(id)))
-                    .materialise_async()) {
-                yield db.delete_async<TaskProgress>(row);
-            }
-            foreach (var row in yield db.query<TaskComment>()
-                    .where(expr("task_id == $0", elem<int64?>(id)))
-                    .materialise_async()) {
-                yield db.delete_async<TaskComment>(row);
-            }
-            foreach (var row in yield db.query<TaskBlock>()
-                    .where(expr("task_id == $0", elem<int64?>(id)))
-                    .materialise_async()) {
-                yield db.delete_async<TaskBlock>(row);
-            }
-            yield clear_enqueue_after_references(id);
-            yield db.delete_async<TaskEntity>((!)task);
-            yield pulse.bump(id, "task deleted");
+            yield delete_task_rows((!)task);
         }
 
         /**
@@ -1115,6 +1098,49 @@ namespace ClankerSaddle.Services {
             return "Human answer: " + answer;
         }
 
+        /**
+         * The ONE terminal stamping path (complete/fail/cancel): owns
+         * the status flip, the `completed_at` stamp and the summary
+         * precedence — a non-empty `summary` replaces whatever the
+         * row holds, else `empty_summary` applies only when the row
+         * holds nothing. `working_branch` records when non-empty (the
+         * done push's branch report). Callers keep their own
+         * idempotence guard (terminal states are sticky, plan 13 B2).
+         */
+        private async void mark_terminal(TaskEntity task, string status, string? summary,
+                string? working_branch = null, string? empty_summary = null) throws Error {
+            task.status = status;
+            task.completed_at = new DateTime.now_local();
+            if (summary != null && summary.length > 0) {
+                task.summary = summary;
+            } else if ((task.summary ?? "").length == 0 && empty_summary != null) {
+                task.summary = empty_summary;
+            }
+            if (working_branch != null && working_branch.length > 0) {
+                task.working_branch = working_branch;
+            }
+            yield db.update_async<TaskEntity>(task);
+        }
+
+        /**
+         * The ONE park stamping path: `suspended` plus the
+         * `suspended_at` stamp, stamped once per parking window so the
+         * resume trigger only sees goal events that landed after the
+         * suspension. No-op when already parked (the stamp survives).
+         */
+        public async void set_parked(int64 task_id) throws Error {
+            var task = yield db.query<TaskEntity>()
+                .where(expr("id == $0", elem<int64?>(task_id)))
+                .first_async();
+            if (task == null || ((!)task).status == STATUS_SUSPENDED) {
+                return;
+            }
+            ((!)task).status = STATUS_SUSPENDED;
+            ((!)task).suspended_at = new DateTime.now_local();
+            yield db.update_async<TaskEntity>((!)task);
+            yield pulse.bump(task_id, "task suspended");
+        }
+
         /**
          * Marks a task done: records `summary` and `working_branch`, stamps
          * `completed_at`, closes any open session (outcome `outcome`) and
@@ -1137,15 +1163,7 @@ namespace ClankerSaddle.Services {
                 return;
             }
 
-            ((!)task).status = STATUS_DONE;
-            ((!)task).completed_at = new DateTime.now_local();
-            if (summary != null && summary.length > 0) {
-                ((!)task).summary = summary;
-            }
-            if (working_branch != null && working_branch.length > 0) {
-                ((!)task).working_branch = working_branch;
-            }
-            yield db.update_async<TaskEntity>((!)task);
+            yield mark_terminal((!)task, STATUS_DONE, summary, working_branch);
 
             yield close_open_sessions(task_id, outcome);
             yield free_saddles_running(task_id);
@@ -1183,12 +1201,7 @@ namespace ClankerSaddle.Services {
                     "Draft tasks are deleted, not cancelled");
             }
 
-            ((!)task).status = STATUS_FAILED;
-            ((!)task).completed_at = new DateTime.now_local();
-            if (((!)task).summary == null || ((!)task).summary.length == 0) {
-                ((!)task).summary = "Cancelled by user";
-            }
-            yield db.update_async<TaskEntity>((!)task);
+            yield mark_terminal((!)task, STATUS_FAILED, null, null, "Cancelled by user");
 
             yield close_open_sessions(task_id, "stopped");
             yield free_saddles_running(task_id);
@@ -1220,14 +1233,8 @@ namespace ClankerSaddle.Services {
                 return;
             }
 
-            ((!)task).status = STATUS_FAILED;
-            ((!)task).completed_at = new DateTime.now_local();
-            if (summary != null && summary.length > 0) {
-                ((!)task).summary = summary;
-            } else if (((!)task).summary == null || ((!)task).summary.length == 0) {
-                ((!)task).summary = "Saddle reported a failure";
-            }
-            yield db.update_async<TaskEntity>((!)task);
+            yield mark_terminal((!)task, STATUS_FAILED, summary, null,
+                "Saddle reported a failure");
 
             yield close_open_sessions(task_id, "failed");
             yield free_saddles_running(task_id);
@@ -1239,25 +1246,6 @@ namespace ClankerSaddle.Services {
                 ((!)task).title, task_id, "failed", summary));
         }
 
-        /**
-         * Parks a suspended task (`saddle suspend`): stores the status
-         * and stamps `suspended_at` once per parking window, so the
-         * resume trigger only sees goal events that landed after the
-         * suspension (a parked goal agent between supervision rounds).
-         */
-        public async void suspend_task(int64 task_id) throws Error {
-            var task = yield db.query<TaskEntity>()
-                .where(expr("id == $0", elem<int64?>(task_id)))
-                .first_async();
-            if (task == null || ((!)task).status == STATUS_SUSPENDED) {
-                return;
-            }
-            ((!)task).status = STATUS_SUSPENDED;
-            ((!)task).suspended_at = new DateTime.now_local();
-            yield db.update_async<TaskEntity>((!)task);
-            yield pulse.bump(task_id, "task suspended");
-        }
-
         /** Withdraws the suspension stamp after a successful resume. */
         public async void clear_suspension(int64 task_id) throws Error {
             var task = yield db.query<TaskEntity>()