|
@@ -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>()
|
|
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()) {
|
|
.materialise_async()) {
|
|
|
yield db.delete_async<TaskRule>(link);
|
|
yield db.delete_async<TaskRule>(link);
|
|
|
}
|
|
}
|
|
|
foreach (var link in yield db.query<TaskCapability>()
|
|
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()) {
|
|
.materialise_async()) {
|
|
|
yield db.delete_async<TaskCapability>(link);
|
|
yield db.delete_async<TaskCapability>(link);
|
|
|
}
|
|
}
|
|
|
foreach (var row in yield db.query<TaskProgress>()
|
|
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()) {
|
|
.materialise_async()) {
|
|
|
yield db.delete_async<TaskProgress>(row);
|
|
yield db.delete_async<TaskProgress>(row);
|
|
|
}
|
|
}
|
|
|
foreach (var row in yield db.query<TaskComment>()
|
|
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()) {
|
|
.materialise_async()) {
|
|
|
yield db.delete_async<TaskComment>(row);
|
|
yield db.delete_async<TaskComment>(row);
|
|
|
}
|
|
}
|
|
|
foreach (var row in yield db.query<TaskBlock>()
|
|
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()) {
|
|
.materialise_async()) {
|
|
|
yield db.delete_async<TaskBlock>(row);
|
|
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,
|
|
throw new Error(Quark.from_string("clanker-saddle"), 53,
|
|
|
"Only draft tasks can be deleted (cancel or retry handles the rest)");
|
|
"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;
|
|
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
|
|
* Marks a task done: records `summary` and `working_branch`, stamps
|
|
|
* `completed_at`, closes any open session (outcome `outcome`) and
|
|
* `completed_at`, closes any open session (outcome `outcome`) and
|
|
@@ -1137,15 +1163,7 @@ namespace ClankerSaddle.Services {
|
|
|
return;
|
|
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 close_open_sessions(task_id, outcome);
|
|
|
yield free_saddles_running(task_id);
|
|
yield free_saddles_running(task_id);
|
|
@@ -1183,12 +1201,7 @@ namespace ClankerSaddle.Services {
|
|
|
"Draft tasks are deleted, not cancelled");
|
|
"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 close_open_sessions(task_id, "stopped");
|
|
|
yield free_saddles_running(task_id);
|
|
yield free_saddles_running(task_id);
|
|
@@ -1220,14 +1233,8 @@ namespace ClankerSaddle.Services {
|
|
|
return;
|
|
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 close_open_sessions(task_id, "failed");
|
|
|
yield free_saddles_running(task_id);
|
|
yield free_saddles_running(task_id);
|
|
@@ -1239,25 +1246,6 @@ namespace ClankerSaddle.Services {
|
|
|
((!)task).title, task_id, "failed", summary));
|
|
((!)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. */
|
|
/** Withdraws the suspension stamp after a successful resume. */
|
|
|
public async void clear_suspension(int64 task_id) throws Error {
|
|
public async void clear_suspension(int64 task_id) throws Error {
|
|
|
var task = yield db.query<TaskEntity>()
|
|
var task = yield db.query<TaskEntity>()
|