|
|
@@ -0,0 +1,141 @@
|
|
|
+using InvercargillSql.Orm;
|
|
|
+using Inversion;
|
|
|
+using ClankerSaddle.Data;
|
|
|
+
|
|
|
+namespace ClankerSaddle.Services {
|
|
|
+
|
|
|
+ /**
|
|
|
+ * The cancel/retry/requeue orchestration the six row actions used
|
|
|
+ * to own (plan 16 R2, arch item 11): every path that moves a task
|
|
|
+ * or a goal on from a possibly-live daemon runs "abort the live
|
|
|
+ * session first, then mutate" through here. The orchestration
|
|
|
+ * lived in the actions only because GoalService → SaddleService
|
|
|
+ * would cycle; this service sits beside the three without a cycle
|
|
|
+ * (none of them inject it), and the actions went back to being
|
|
|
+ * auth + validation + notify wrappers.
|
|
|
+ *
|
|
|
+ * The abort contract (SaddleService.abort_live_session, encoded
|
|
|
+ * once here): the cancel is delivered to the daemon holding the
|
|
|
+ * task's open session (`POST /task-update {kind: "cancel"}`); a
|
|
|
+ * transport FAILURE marks the saddle offline and STILL PROCEEDS —
|
|
|
+ * the daemon must never keep running a task the web has moved on
|
|
|
+ * from as a zombie. The verb rides the system thread ("<verb>;
|
|
|
+ * daemon aborted" / "<verb>; daemon unreachable — saddle marked
|
|
|
+ * offline").
|
|
|
+ */
|
|
|
+ public class LifecycleService : Object {
|
|
|
+
|
|
|
+ private TaskService tasks = inject<TaskService>();
|
|
|
+ private SaddleService saddles = inject<SaddleService>();
|
|
|
+ private GoalService goals = inject<GoalService>();
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Cancels a task: its live daemon is aborted first ("cancelled"
|
|
|
+ * on the system thread), then the row goes terminal-failed
|
|
|
+ * with the cancellation summary. An abort failure marks the
|
|
|
+ * saddle offline and proceeds — the cancel itself only fails
|
|
|
+ * when the mutation does. The action's non-draft/non-terminal
|
|
|
+ * guard still applies upstream (drafts are deleted, terminal
|
|
|
+ * tasks keep their first outcome).
|
|
|
+ */
|
|
|
+ public async void cancel_task(int64 task_id) throws Error {
|
|
|
+ yield saddles.abort_live_session(tasks, task_id, "cancelled");
|
|
|
+ yield tasks.cancel_task(task_id);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Retries a failed task: any live daemon is aborted first
|
|
|
+ * ("retried"), then the attempt resets — back to the queue,
|
|
|
+ * the failure summary and completed_at withdrawn (plan 16 S4:
|
|
|
+ * the terminal cache dies with its attempt, copied nowhere).
|
|
|
+ */
|
|
|
+ public async void retry_task(int64 task_id) throws Error {
|
|
|
+ yield saddles.abort_live_session(tasks, task_id, "retried");
|
|
|
+ yield tasks.retry_task(task_id);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * The manual requeue-for-resume (a task stuck working or
|
|
|
+ * parked on a saddle that went offline): a live session is
|
|
|
+ * aborted ("stopped") and its saddle marked unreachable — the
|
|
|
+ * unreachable bookkeeping requeues the held task with its
|
|
|
+ * notice comment; without a session the task simply requeues.
|
|
|
+ * The next dispatch restores the daemon-side workspace.
|
|
|
+ */
|
|
|
+ public async void requeue_for_resume(int64 task_id) throws Error {
|
|
|
+ var session = yield tasks.open_session_for_task(task_id);
|
|
|
+ if (session != null) {
|
|
|
+ yield saddles.abort_live_session(tasks, task_id, "stopped");
|
|
|
+ yield saddles.mark_saddle_unreachable(((!)session).saddle_id, tasks);
|
|
|
+ } else {
|
|
|
+ yield tasks.requeue_task(task_id);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Cancels a goal: every running task under it has its live
|
|
|
+ * daemon aborted first ("goal cancelled" — one abort failure
|
|
|
+ * is warned and skipped; the cascade still proceeds, the
|
|
|
+ * abort's own failure contract having already handled the
|
|
|
+ * dead-daemon case), then the goal and its task tree close
|
|
|
+ * (the cascade cancels non-terminal tasks, deletes drafts).
|
|
|
+ */
|
|
|
+ public async void cancel_goal(int64 goal_id) throws Error {
|
|
|
+ foreach (var task in yield tasks.list_tasks_for_goal(goal_id)) {
|
|
|
+ if (task.status == STATUS_ASSIGNED || task.status == STATUS_WORKING) {
|
|
|
+ try {
|
|
|
+ yield saddles.abort_live_session(tasks, task.id, "goal cancelled");
|
|
|
+ } catch (Error e) {
|
|
|
+ warning("[ClankerSaddle] Goal-cancel abort of task %s failed: %s",
|
|
|
+ task.id.to_string(), e.message);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ yield goals.cancel_goal(goal_id);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Re-offers a failed goal's supervision (plan 14 B1): the run
|
|
|
+ * task's live daemon is aborted first ("supervision retried" —
|
|
|
+ * a failure is warned and skipped), the run re-queues for
|
|
|
+ * another attempt and {@link GoalService.retry_goal} revives
|
|
|
+ * the goal row. The harness row on the run thread narrates the
|
|
|
+ * retry (the narrative derives it at read time). The action's
|
|
|
+ * failed-goal/failed-run guards still apply upstream.
|
|
|
+ */
|
|
|
+ public async void retry_goal_run(int64 goal_id) throws Error {
|
|
|
+ var run = yield goals.goal_run_task(goal_id);
|
|
|
+ if (run == null) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ yield saddles.abort_live_session(tasks, ((!)run).id, "supervision retried");
|
|
|
+ } catch (Error e) {
|
|
|
+ warning("[ClankerSaddle] Goal-retry abort of run task %s failed: %s",
|
|
|
+ ((!)run).id.to_string(), e.message);
|
|
|
+ }
|
|
|
+ yield tasks.retry_task(((!)run).id);
|
|
|
+ yield goals.retry_goal(goal_id);
|
|
|
+ yield tasks.add_system_comment(((!)run).id,
|
|
|
+ @"Supervision retried — the goal agent runs again (run task #$(((!)run).id))");
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * The goal page's requeue-to-resume (plan 14 B1): the run is
|
|
|
+ * stale or parked on a dead saddle — abort its session
|
|
|
+ * best-effort, close it and requeue the run; the next dispatch
|
|
|
+ * restores the daemon's workspace context. The action's
|
|
|
+ * not-terminal guard still applies upstream.
|
|
|
+ */
|
|
|
+ public async void requeue_goal_run(int64 goal_id) throws Error {
|
|
|
+ var run = yield goals.goal_run_task(goal_id);
|
|
|
+ if (run == null) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ yield saddles.abort_live_session(tasks, ((!)run).id, "requeued");
|
|
|
+ yield tasks.requeue_task(((!)run).id);
|
|
|
+ yield tasks.add_system_comment(((!)run).id,
|
|
|
+ @"Run requeued — the daemon restores the agent's workspace on the next dispatch (run task #$(((!)run).id))");
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|