|
@@ -52,17 +52,6 @@ namespace ClankerSaddle.Services {
|
|
|
|| body.contains("parked until");
|
|
|| body.contains("parked until");
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /**
|
|
|
|
|
- * The agent-celebration entry type (`saddle yeehaw <message>`): the
|
|
|
|
|
- * daemon pushes it as a round-results entry carrying the message in
|
|
|
|
|
- * the output field, so it stores like any other `session_commands`
|
|
|
|
|
- * row — nothing new is persisted for the badge or the card.
|
|
|
|
|
- */
|
|
|
|
|
- public const string ENTRY_YEEHAW = "yeehaw";
|
|
|
|
|
-
|
|
|
|
|
- /** The task page's yeehaw card cap (newest entries kept). */
|
|
|
|
|
- public const int YEEHAW_CARD_MAX = 3;
|
|
|
|
|
-
|
|
|
|
|
/**
|
|
/**
|
|
|
* The stable verdict wordings (plan 11): they are stored as the
|
|
* The stable verdict wordings (plan 11): they are stored as the
|
|
|
* block answers AND ride the resuming allocation's
|
|
* block answers AND ride the resuming allocation's
|
|
@@ -113,6 +102,19 @@ namespace ClankerSaddle.Services {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * One LLM-reported progress line on a task — the ledger's progress
|
|
|
|
|
+ * events projected back into the feed shape the pages read (id is
|
|
|
|
|
+ * the event id, summary the payload's text).
|
|
|
|
|
+ */
|
|
|
|
|
+ public class TaskProgress : Object {
|
|
|
|
|
+
|
|
|
|
|
+ public int64 id { get; set; }
|
|
|
|
|
+ public int64 task_id { get; set; }
|
|
|
|
|
+ public string summary { get; set; default = ""; }
|
|
|
|
|
+ public DateTime? created_at { get; set; default = null; }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* The full task aggregate loaded by {@link TaskService.get_task}: the
|
|
* The full task aggregate loaded by {@link TaskService.get_task}: the
|
|
|
* task itself plus its comments, blocks, progress history and
|
|
* task itself plus its comments, blocks, progress history and
|
|
@@ -126,25 +128,6 @@ namespace ClankerSaddle.Services {
|
|
|
public ImmutableLot<Session> sessions { get; set; }
|
|
public ImmutableLot<Session> sessions { get; set; }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /**
|
|
|
|
|
- * One agent celebration (`saddle yeehaw`) read back for display: the
|
|
|
|
|
- * message from the entry's output field and when it landed.
|
|
|
|
|
- */
|
|
|
|
|
- public class YeehawEntry : Object {
|
|
|
|
|
- public string message { get; set; default = ""; }
|
|
|
|
|
- public DateTime? at { get; set; }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * The task's celebration feed for the yeehaw card: the newest
|
|
|
|
|
- * {@link YEEHAW_CARD_MAX} entries plus the uncapped total (the
|
|
|
|
|
- * "+N more" hint derives from the two).
|
|
|
|
|
- */
|
|
|
|
|
- public class YeehawFeed : Object {
|
|
|
|
|
- public Vector<YeehawEntry> latest { get; set; default = new Vector<YeehawEntry>(); }
|
|
|
|
|
- public int64 total { get; set; }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
/**
|
|
/**
|
|
|
* Task storage and lifecycle: creation, enqueue-after-aware
|
|
* Task storage and lifecycle: creation, enqueue-after-aware
|
|
|
* readiness, agent interaction primitives (comments, blocks,
|
|
* readiness, agent interaction primitives (comments, blocks,
|
|
@@ -159,6 +142,7 @@ namespace ClankerSaddle.Services {
|
|
|
private Connection connection = inject<Connection>();
|
|
private Connection connection = inject<Connection>();
|
|
|
private StatePulse pulse = inject<StatePulse>();
|
|
private StatePulse pulse = inject<StatePulse>();
|
|
|
private PushService push = inject<PushService>();
|
|
private PushService push = inject<PushService>();
|
|
|
|
|
+ private EventService events = inject<EventService>();
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* Inserts a task (status 'draft' until its creator enqueues
|
|
* Inserts a task (status 'draft' until its creator enqueues
|
|
@@ -230,9 +214,26 @@ namespace ClankerSaddle.Services {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
yield pulse.bump(task.id, "task created");
|
|
yield pulse.bump(task.id, "task created");
|
|
|
|
|
+ yield append_state_change(task.id, "", status, "Task created",
|
|
|
|
|
+ creator == CREATOR_HUMAN ? EVENT_ACTOR_USER : EVENT_ACTOR_MODEL);
|
|
|
return task;
|
|
return task;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Appends a task-scoped state_change event carrying `from`/`to`
|
|
|
|
|
+ * plus a human-readable line the timeline and narrative read
|
|
|
|
|
+ * back verbatim.
|
|
|
|
|
+ */
|
|
|
|
|
+ private async void append_state_change(int64 task_id, string from, string to,
|
|
|
|
|
+ string text, string actor) throws Error {
|
|
|
|
|
+ var payload = new Json.Object();
|
|
|
|
|
+ payload.set_string_member("from", from);
|
|
|
|
|
+ payload.set_string_member("to", to);
|
|
|
|
|
+ payload.set_string_member("text", text);
|
|
|
|
|
+ yield events.append(EVENT_STATE_CHANGE, actor, EVENT_SCOPE_TASK, task_id,
|
|
|
|
|
+ payload);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* Loads the full aggregate for `id`: the task, its comments,
|
|
* Loads the full aggregate for `id`: the task, its comments,
|
|
|
* blocks, progress and sessions. Returns null when the task
|
|
* blocks, progress and sessions. Returns null when the task
|
|
@@ -483,6 +484,7 @@ namespace ClankerSaddle.Services {
|
|
|
return null;
|
|
return null;
|
|
|
}
|
|
}
|
|
|
if (((!)task).status != status) {
|
|
if (((!)task).status != status) {
|
|
|
|
|
+ string from = ((!)task).status;
|
|
|
if (status != STATUS_DONE && status != STATUS_FAILED) {
|
|
if (status != STATUS_DONE && status != STATUS_FAILED) {
|
|
|
// Terminal states are sticky (plan 13 B2's
|
|
// Terminal states are sticky (plan 13 B2's
|
|
|
// invariant): a NON-terminal transition must never
|
|
// invariant): a NON-terminal transition must never
|
|
@@ -518,6 +520,12 @@ namespace ClankerSaddle.Services {
|
|
|
yield db.update_async<TaskEntity>((!)task);
|
|
yield db.update_async<TaskEntity>((!)task);
|
|
|
}
|
|
}
|
|
|
yield pulse.bump(id, @"task $(status)");
|
|
yield pulse.bump(id, @"task $(status)");
|
|
|
|
|
+ var moved = new Json.Object();
|
|
|
|
|
+ moved.set_string_member("from", from);
|
|
|
|
|
+ moved.set_string_member("to", status);
|
|
|
|
|
+ moved.set_string_member("text", @"Status → $(status)");
|
|
|
|
|
+ yield events.append(EVENT_STATE_CHANGE, EVENT_ACTOR_SYSTEM,
|
|
|
|
|
+ EVENT_SCOPE_TASK, id, moved);
|
|
|
}
|
|
}
|
|
|
return task;
|
|
return task;
|
|
|
}
|
|
}
|
|
@@ -560,6 +568,8 @@ namespace ClankerSaddle.Services {
|
|
|
((!)task).status = STATUS_QUEUED;
|
|
((!)task).status = STATUS_QUEUED;
|
|
|
yield db.update_async<TaskEntity>((!)task);
|
|
yield db.update_async<TaskEntity>((!)task);
|
|
|
yield pulse.bump(id, "task enqueued");
|
|
yield pulse.bump(id, "task enqueued");
|
|
|
|
|
+ yield append_state_change(id, STATUS_DRAFT, STATUS_QUEUED,
|
|
|
|
|
+ "Task enqueued", actor == CREATOR_HUMAN ? EVENT_ACTOR_USER : EVENT_ACTOR_MODEL);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -641,11 +651,6 @@ namespace ClankerSaddle.Services {
|
|
|
.materialise_async()) {
|
|
.materialise_async()) {
|
|
|
yield db.delete_async<TaskCapability>(link);
|
|
yield db.delete_async<TaskCapability>(link);
|
|
|
}
|
|
}
|
|
|
- foreach (var row in yield db.query<TaskProgress>()
|
|
|
|
|
- .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>()
|
|
foreach (var row in yield db.query<TaskComment>()
|
|
|
.where(expr("task_id == $0", elem<int64?>(task.id)))
|
|
.where(expr("task_id == $0", elem<int64?>(task.id)))
|
|
|
.materialise_async()) {
|
|
.materialise_async()) {
|
|
@@ -656,6 +661,7 @@ namespace ClankerSaddle.Services {
|
|
|
.materialise_async()) {
|
|
.materialise_async()) {
|
|
|
yield db.delete_async<TaskBlock>(row);
|
|
yield db.delete_async<TaskBlock>(row);
|
|
|
}
|
|
}
|
|
|
|
|
+ yield events.delete_for_task(task.id);
|
|
|
yield clear_enqueue_after_references(task.id);
|
|
yield clear_enqueue_after_references(task.id);
|
|
|
yield db.delete_async<TaskEntity>(task);
|
|
yield db.delete_async<TaskEntity>(task);
|
|
|
yield pulse.bump(task.id, "task deleted");
|
|
yield pulse.bump(task.id, "task deleted");
|
|
@@ -713,119 +719,46 @@ namespace ClankerSaddle.Services {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * Appends a progress line (`saddle progress-update`) to the task's
|
|
|
|
|
- * history and returns the new row.
|
|
|
|
|
|
|
+ * Appends a progress line to the task's ledger (the daemon's
|
|
|
|
|
+ * progress events land in the same ledger through the chunks
|
|
|
|
|
+ * push; the goal-era results-entry path that wrote
|
|
|
|
|
+ * `task_progress` rows is gone).
|
|
|
*/
|
|
*/
|
|
|
public async TaskProgress add_progress(int64 task_id, string summary) throws Error {
|
|
public async TaskProgress add_progress(int64 task_id, string summary) throws Error {
|
|
|
- var row = new TaskProgress() {
|
|
|
|
|
|
|
+ var payload = new Json.Object();
|
|
|
|
|
+ payload.set_string_member("text", summary);
|
|
|
|
|
+ var record = yield events.append(EVENT_PROGRESS, EVENT_ACTOR_MODEL,
|
|
|
|
|
+ EVENT_SCOPE_TASK, task_id, payload);
|
|
|
|
|
+ yield pulse.bump(task_id, "progress");
|
|
|
|
|
+ return new TaskProgress() {
|
|
|
|
|
+ id = record.id,
|
|
|
task_id = task_id,
|
|
task_id = task_id,
|
|
|
summary = summary,
|
|
summary = summary,
|
|
|
- created_at = new DateTime.now_local()
|
|
|
|
|
|
|
+ created_at = record.created_at
|
|
|
};
|
|
};
|
|
|
- yield db.insert_async<TaskProgress>(row);
|
|
|
|
|
- yield pulse.bump(task_id, "progress");
|
|
|
|
|
- return row;
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/** The task's progress history, newest first. */
|
|
/** The task's progress history, newest first. */
|
|
|
public async ImmutableLot<TaskProgress> list_progress(int64 task_id) throws Error {
|
|
public async ImmutableLot<TaskProgress> list_progress(int64 task_id) throws Error {
|
|
|
- return yield db.query<TaskProgress>()
|
|
|
|
|
- .where(expr("task_id == $0", elem<int64?>(task_id)))
|
|
|
|
|
- .order_by_desc(expr("id"))
|
|
|
|
|
- .materialise_async();
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * Every task id whose sessions carry a `yeehaw` entry (stringly
|
|
|
|
|
- * keyed) — the batch presence check the task list and the
|
|
|
|
|
- * dashboard's task references badge from, one DISTINCT query
|
|
|
|
|
- * instead of one EXISTS per row.
|
|
|
|
|
- */
|
|
|
|
|
- public async Invercargill.DataStructures.HashSet<string> yeehaw_task_ids() throws Error {
|
|
|
|
|
- var ids = new Invercargill.DataStructures.HashSet<string>();
|
|
|
|
|
- var command = connection.create_command(
|
|
|
|
|
- "SELECT DISTINCT s.task_id AS task_id FROM sessions s"
|
|
|
|
|
- + " JOIN session_commands sc ON sc.session_id = s.id"
|
|
|
|
|
- + " WHERE sc.type = :kind");
|
|
|
|
|
- command.with_parameter<Invercargill.Element>("kind", elem(ENTRY_YEEHAW));
|
|
|
|
|
- foreach (var row in yield command.execute_query_async()) {
|
|
|
|
|
- var element = row.get("task_id");
|
|
|
|
|
- int64? value = 0;
|
|
|
|
|
- if (element != null && element.try_get_as<int64?>(out value) && value != null) {
|
|
|
|
|
- ids.add(((!)value).to_string());
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ var rows = new Vector<TaskProgress>();
|
|
|
|
|
+ foreach (var record in yield events.list_for_task(task_id,
|
|
|
|
|
+ { EVENT_PROGRESS })) {
|
|
|
|
|
+ rows.add(new TaskProgress() {
|
|
|
|
|
+ id = record.id,
|
|
|
|
|
+ task_id = task_id,
|
|
|
|
|
+ summary = EventService.payload_string(record, "text"),
|
|
|
|
|
+ created_at = record.created_at
|
|
|
|
|
+ });
|
|
|
}
|
|
}
|
|
|
- return ids;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * The task's yeehaw card data: the newest
|
|
|
|
|
- * {@link YEEHAW_CARD_MAX} celebrations (message + when, newest
|
|
|
|
|
- * first) plus the uncapped total. Reads the existing
|
|
|
|
|
- * `session_commands` rows through the sessions join — the card
|
|
|
|
|
- * stores nothing of its own.
|
|
|
|
|
- */
|
|
|
|
|
- public async YeehawFeed yeehaw_feed(int64 task_id) throws Error {
|
|
|
|
|
- var total_command = connection.create_command(
|
|
|
|
|
- "SELECT COUNT(*) AS total FROM session_commands sc"
|
|
|
|
|
- + " JOIN sessions s ON s.id = sc.session_id"
|
|
|
|
|
- + " WHERE s.task_id = :task AND sc.type = :kind");
|
|
|
|
|
- total_command.with_parameter<Invercargill.Element>("task", elem<int64?>(task_id));
|
|
|
|
|
- total_command.with_parameter<Invercargill.Element>("kind", elem(ENTRY_YEEHAW));
|
|
|
|
|
- var total = yield total_command.execute_scalar_async();
|
|
|
|
|
- int64? total_value = 0;
|
|
|
|
|
- if (total != null) {
|
|
|
|
|
- total.try_get_as<int64?>(out total_value);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- var feed = new YeehawFeed() { total = total_value };
|
|
|
|
|
- var rows_command = connection.create_command(
|
|
|
|
|
- "SELECT sc.output_text AS message, sc.command_text AS command,"
|
|
|
|
|
- + " sc.started_at AS started, sc.completed_at AS completed"
|
|
|
|
|
- + " FROM session_commands sc JOIN sessions s ON s.id = sc.session_id"
|
|
|
|
|
- + " WHERE s.task_id = :task AND sc.type = :kind"
|
|
|
|
|
- + " ORDER BY sc.id DESC LIMIT :cap");
|
|
|
|
|
- rows_command.with_parameter<Invercargill.Element>("task", elem<int64?>(task_id));
|
|
|
|
|
- rows_command.with_parameter<Invercargill.Element>("kind", elem(ENTRY_YEEHAW));
|
|
|
|
|
- rows_command.with_parameter<Invercargill.Element>("cap", elem<int64?>(YEEHAW_CARD_MAX));
|
|
|
|
|
- foreach (var row in yield rows_command.execute_query_async()) {
|
|
|
|
|
- string message = string_of(row.get("message"));
|
|
|
|
|
- if (message.length == 0) {
|
|
|
|
|
- // The message rides the output field; the command
|
|
|
|
|
- // line is the fallback for empty-output pushes.
|
|
|
|
|
- message = string_of(row.get("command"));
|
|
|
|
|
- }
|
|
|
|
|
- DateTime? at = date_time_of(row.get("started"));
|
|
|
|
|
- if (at == null) {
|
|
|
|
|
- at = date_time_of(row.get("completed"));
|
|
|
|
|
- }
|
|
|
|
|
- feed.latest.add(new YeehawEntry() { message = message, at = at });
|
|
|
|
|
- }
|
|
|
|
|
- return feed;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /** A raw query column back as a string ("" when null/unreadable). */
|
|
|
|
|
- private static string string_of(Invercargill.Element? element) {
|
|
|
|
|
- if (element == null) {
|
|
|
|
|
- return "";
|
|
|
|
|
- }
|
|
|
|
|
- string value = "";
|
|
|
|
|
- return ((!)element).try_get_as<string>(out value) ? value : "";
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /** A raw query column back as a DateTime (null when null/unreadable). */
|
|
|
|
|
- private static DateTime? date_time_of(Invercargill.Element? element) {
|
|
|
|
|
- if (element == null) {
|
|
|
|
|
- return null;
|
|
|
|
|
- }
|
|
|
|
|
- DateTime value;
|
|
|
|
|
- return ((!)element).try_get_as<DateTime>(out value) ? value : null;
|
|
|
|
|
|
|
+ return rows.to_immutable_buffer();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* Appends a comment; `author` is 'agent', 'user:<name>' or
|
|
* Appends a comment; `author` is 'agent', 'user:<name>' or
|
|
|
* 'system' (the harness-authored bookkeeping rows — plan 15
|
|
* 'system' (the harness-authored bookkeeping rows — plan 15
|
|
|
- * C2's distinct system-event class, never agent content).
|
|
|
|
|
|
|
+ * C2's distinct system-event class, never agent content). The
|
|
|
|
|
+ * comment event references the row (the task_comments table
|
|
|
|
|
+ * stays; the ledger carries the reference plus the body).
|
|
|
*/
|
|
*/
|
|
|
public async TaskComment add_comment(int64 task_id, string author, string body,
|
|
public async TaskComment add_comment(int64 task_id, string author, string body,
|
|
|
string? quote = null) throws Error {
|
|
string? quote = null) throws Error {
|
|
@@ -837,6 +770,17 @@ namespace ClankerSaddle.Services {
|
|
|
created_at = new DateTime.now_local()
|
|
created_at = new DateTime.now_local()
|
|
|
};
|
|
};
|
|
|
yield db.insert_async<TaskComment>(comment);
|
|
yield db.insert_async<TaskComment>(comment);
|
|
|
|
|
+ var payload = new Json.Object();
|
|
|
|
|
+ payload.set_int_member("comment_id", comment.id);
|
|
|
|
|
+ payload.set_string_member("body", body);
|
|
|
|
|
+ payload.set_string_member("author", author);
|
|
|
|
|
+ if (comment.quote != null) {
|
|
|
|
|
+ payload.set_string_member("quote", (!)comment.quote);
|
|
|
|
|
+ }
|
|
|
|
|
+ yield events.append(EVENT_COMMENT,
|
|
|
|
|
+ author == "agent" ? EVENT_ACTOR_MODEL
|
|
|
|
|
+ : author.has_prefix("user:") ? EVENT_ACTOR_USER : EVENT_ACTOR_SYSTEM,
|
|
|
|
|
+ EVENT_SCOPE_TASK, task_id, payload);
|
|
|
yield pulse.bump(task_id, "comment");
|
|
yield pulse.bump(task_id, "comment");
|
|
|
return comment;
|
|
return comment;
|
|
|
}
|
|
}
|
|
@@ -852,28 +796,6 @@ namespace ClankerSaddle.Services {
|
|
|
return yield add_comment(task_id, COMMENT_AUTHOR_SYSTEM, body);
|
|
return yield add_comment(task_id, COMMENT_AUTHOR_SYSTEM, body);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /**
|
|
|
|
|
- * Appends a terminal entry to the task's open session so
|
|
|
|
|
- * conversation-level events (blocks, plans, progress, reviews,
|
|
|
|
|
- * human comments) appear in the streamed log. A no-op when the
|
|
|
|
|
- * task has no open session.
|
|
|
|
|
- */
|
|
|
|
|
- public async void log_session_entry(int64 task_id, string entry_type,
|
|
|
|
|
- string command, string? output) throws Error {
|
|
|
|
|
- var session = yield open_session_for_task(task_id);
|
|
|
|
|
- if (session == null) {
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
- var entry = new SessionCommand() {
|
|
|
|
|
- session_id = ((!)session).id,
|
|
|
|
|
- command_text = command,
|
|
|
|
|
- output_text = output,
|
|
|
|
|
- entry_type = entry_type,
|
|
|
|
|
- started_at = new DateTime.now_local()
|
|
|
|
|
- };
|
|
|
|
|
- yield db.insert_async<SessionCommand>(entry);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
/**
|
|
/**
|
|
|
* Raises a blocking prompt on a task — `question` (optionally with
|
|
* Raises a blocking prompt on a task — `question` (optionally with
|
|
|
* multiple-choice `options` and a markdown `body`), `rfc` or
|
|
* multiple-choice `options` and a markdown `body`), `rfc` or
|
|
@@ -905,6 +827,7 @@ namespace ClankerSaddle.Services {
|
|
|
var task = yield db.query<TaskEntity>()
|
|
var task = yield db.query<TaskEntity>()
|
|
|
.where(expr("id == $0", elem<int64?>(task_id)))
|
|
.where(expr("id == $0", elem<int64?>(task_id)))
|
|
|
.first_async();
|
|
.first_async();
|
|
|
|
|
+ string previous_status = task != null ? ((!)task).status : "";
|
|
|
if (task != null && (((!)task).status == STATUS_ASSIGNED || ((!)task).status == STATUS_WORKING
|
|
if (task != null && (((!)task).status == STATUS_ASSIGNED || ((!)task).status == STATUS_WORKING
|
|
|
|| ((!)task).status == STATUS_BLOCKED || ((!)task).status == STATUS_REVIEW_REQUESTED)) {
|
|
|| ((!)task).status == STATUS_BLOCKED || ((!)task).status == STATUS_REVIEW_REQUESTED)) {
|
|
|
// The park flip is a guarded update: a completion
|
|
// The park flip is a guarded update: a completion
|
|
@@ -929,6 +852,15 @@ namespace ClankerSaddle.Services {
|
|
|
yield park_open_sessions(task_id);
|
|
yield park_open_sessions(task_id);
|
|
|
}
|
|
}
|
|
|
yield pulse.bump(task_id, "block raised");
|
|
yield pulse.bump(task_id, "block raised");
|
|
|
|
|
+ var raised_payload = new Json.Object();
|
|
|
|
|
+ raised_payload.set_string_member("from", previous_status);
|
|
|
|
|
+ raised_payload.set_string_member("to", block.block_type == BLOCK_REVIEW
|
|
|
|
|
+ ? STATUS_REVIEW_REQUESTED : STATUS_BLOCKED);
|
|
|
|
|
+ raised_payload.set_string_member("block_type", block.block_type);
|
|
|
|
|
+ raised_payload.set_string_member("question", block.question);
|
|
|
|
|
+ raised_payload.set_string_member("text", @"$(block.block_type) raised: $(block.question)");
|
|
|
|
|
+ yield events.append(EVENT_STATE_CHANGE, EVENT_ACTOR_SYSTEM, EVENT_SCOPE_TASK,
|
|
|
|
|
+ task_id, raised_payload);
|
|
|
// Web Push (plan 11 task E): every raised block notifies all
|
|
// Web Push (plan 11 task E): every raised block notifies all
|
|
|
// push-subscribed users globally — the goal agent's mirrored
|
|
// push-subscribed users globally — the goal agent's mirrored
|
|
|
// blocks stay quiet (the goal-level block owns the
|
|
// blocks stay quiet (the goal-level block owns the
|
|
@@ -973,6 +905,32 @@ namespace ClankerSaddle.Services {
|
|
|
((!)block).answered_by = answered_by;
|
|
((!)block).answered_by = answered_by;
|
|
|
yield db.update_async<TaskBlock>((!)block);
|
|
yield db.update_async<TaskBlock>((!)block);
|
|
|
|
|
|
|
|
|
|
+ bool review_family = ((!)block).block_type == BLOCK_REVIEW
|
|
|
|
|
+ || ((!)block).block_type == BLOCK_PLAN
|
|
|
|
|
+ || ((!)block).block_type == BLOCK_COMPLETION;
|
|
|
|
|
+ var verdict_payload = new Json.Object();
|
|
|
|
|
+ verdict_payload.set_int_member("block_id", block_id);
|
|
|
|
|
+ verdict_payload.set_string_member("block_type", ((!)block).block_type);
|
|
|
|
|
+ if (((!)block).answered_by != null) {
|
|
|
|
|
+ verdict_payload.set_string_member("by", ((!)block).answered_by);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (review_family) {
|
|
|
|
|
+ bool approved = answer == NOTICE_PLAN_APPROVED
|
|
|
|
|
+ || answer == NOTICE_REVIEW_APPROVED;
|
|
|
|
|
+ verdict_payload.set_string_member("verdict",
|
|
|
|
|
+ approved ? "approved" : "feedback");
|
|
|
|
|
+ verdict_payload.set_string_member("text", @"$(approved ? "approved" : "feedback") — $(answer)");
|
|
|
|
|
+ yield events.append(EVENT_VERDICT,
|
|
|
|
|
+ ((!)block).answered_by != null ? EVENT_ACTOR_USER : EVENT_ACTOR_SYSTEM,
|
|
|
|
|
+ EVENT_SCOPE_TASK, ((!)block).task_id, verdict_payload);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ verdict_payload.set_string_member("answer", answer);
|
|
|
|
|
+ verdict_payload.set_string_member("text", @"$(((!)block).block_type) answered: $(answer)");
|
|
|
|
|
+ yield events.append(EVENT_ANSWER,
|
|
|
|
|
+ ((!)block).answered_by != null ? EVENT_ACTOR_USER : EVENT_ACTOR_SYSTEM,
|
|
|
|
|
+ EVENT_SCOPE_TASK, ((!)block).task_id, verdict_payload);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
var task = yield db.query<TaskEntity>()
|
|
var task = yield db.query<TaskEntity>()
|
|
|
.where(expr("id == $0", elem<int64?>(((!)block).task_id)))
|
|
.where(expr("id == $0", elem<int64?>(((!)block).task_id)))
|
|
|
.first_async();
|
|
.first_async();
|
|
@@ -1194,6 +1152,7 @@ namespace ClankerSaddle.Services {
|
|
|
*/
|
|
*/
|
|
|
private async void mark_terminal(TaskEntity task, string status, string? summary,
|
|
private async void mark_terminal(TaskEntity task, string status, string? summary,
|
|
|
string? empty_summary = null) throws Error {
|
|
string? empty_summary = null) throws Error {
|
|
|
|
|
+ string from = task.status;
|
|
|
task.status = status;
|
|
task.status = status;
|
|
|
task.completed_at = new DateTime.now_local();
|
|
task.completed_at = new DateTime.now_local();
|
|
|
if (summary != null && summary.length > 0) {
|
|
if (summary != null && summary.length > 0) {
|
|
@@ -1202,6 +1161,16 @@ namespace ClankerSaddle.Services {
|
|
|
task.summary = empty_summary;
|
|
task.summary = empty_summary;
|
|
|
}
|
|
}
|
|
|
yield db.update_async<TaskEntity>(task);
|
|
yield db.update_async<TaskEntity>(task);
|
|
|
|
|
+ var payload = new Json.Object();
|
|
|
|
|
+ payload.set_string_member("from", from);
|
|
|
|
|
+ payload.set_string_member("to", status);
|
|
|
|
|
+ payload.set_string_member("text", status == STATUS_DONE
|
|
|
|
|
+ ? "Task completed" : "Task failed");
|
|
|
|
|
+ if ((task.summary ?? "").length > 0) {
|
|
|
|
|
+ payload.set_string_member("summary", (!)task.summary);
|
|
|
|
|
+ }
|
|
|
|
|
+ yield events.append(EVENT_STATE_CHANGE, EVENT_ACTOR_SYSTEM,
|
|
|
|
|
+ EVENT_SCOPE_TASK, task.id, payload);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -1225,6 +1194,8 @@ namespace ClankerSaddle.Services {
|
|
|
yield db.update_async<TaskEntity>((!)task);
|
|
yield db.update_async<TaskEntity>((!)task);
|
|
|
yield park_open_sessions(task_id);
|
|
yield park_open_sessions(task_id);
|
|
|
yield pulse.bump(task_id, "task suspended");
|
|
yield pulse.bump(task_id, "task suspended");
|
|
|
|
|
+ yield append_state_change(task_id, STATUS_WORKING, STATUS_SUSPENDED,
|
|
|
|
|
+ "Parked — awaiting the resume", EVENT_ACTOR_SYSTEM);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -1287,6 +1258,12 @@ namespace ClankerSaddle.Services {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
yield mark_terminal((!)task, STATUS_FAILED, null, "Cancelled by user");
|
|
yield mark_terminal((!)task, STATUS_FAILED, null, "Cancelled by user");
|
|
|
|
|
+ {
|
|
|
|
|
+ var payload = new Json.Object();
|
|
|
|
|
+ payload.set_string_member("text", "Task cancelled by user");
|
|
|
|
|
+ yield events.append(EVENT_CANCEL, EVENT_ACTOR_USER, EVENT_SCOPE_TASK,
|
|
|
|
|
+ task_id, payload);
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
yield release_task(task_id, OUTCOME_STOPPED, false);
|
|
yield release_task(task_id, OUTCOME_STOPPED, false);
|
|
|
yield pulse.bump(task_id, "task cancelled");
|
|
yield pulse.bump(task_id, "task cancelled");
|
|
@@ -1374,17 +1351,10 @@ namespace ClankerSaddle.Services {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
if (park == null) {
|
|
if (park == null) {
|
|
|
- foreach (var session in yield db.query<Session>()
|
|
|
|
|
- .where(expr("task_id == $0", elem<int64?>(task_id)))
|
|
|
|
|
- .materialise_async()) {
|
|
|
|
|
- foreach (var entry in yield db.query<SessionCommand>()
|
|
|
|
|
- .where(expr("session_id == $0", elem<int64?>(session.id)))
|
|
|
|
|
- .materialise_async()) {
|
|
|
|
|
- if (entry.started_at != null
|
|
|
|
|
- && (park == null
|
|
|
|
|
- || ((!)entry.started_at).compare((!)park) > 0)) {
|
|
|
|
|
- park = (!)entry.started_at;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ foreach (var record in yield events.list_for_task(task_id, new string[0])) {
|
|
|
|
|
+ if (record.created_at != null) {
|
|
|
|
|
+ park = (!)record.created_at;
|
|
|
|
|
+ break;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -1407,11 +1377,10 @@ namespace ClankerSaddle.Services {
|
|
|
return true;
|
|
return true;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
- foreach (var progress in yield db.query<TaskProgress>()
|
|
|
|
|
- .where(expr("task_id == $0", elem<int64?>(sibling.id)))
|
|
|
|
|
- .materialise_async()) {
|
|
|
|
|
- if (progress.created_at != null
|
|
|
|
|
- && progress.created_at.compare((!)park) > 0) {
|
|
|
|
|
|
|
+ foreach (var record in yield events.list_for_task(sibling.id,
|
|
|
|
|
+ { EVENT_PROGRESS })) {
|
|
|
|
|
+ if (record.created_at != null
|
|
|
|
|
+ && ((!)record.created_at).compare((!)park) > 0) {
|
|
|
return true;
|
|
return true;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -1494,6 +1463,8 @@ namespace ClankerSaddle.Services {
|
|
|
|
|
|
|
|
yield release_task(task_id, OUTCOME_STOPPED, false);
|
|
yield release_task(task_id, OUTCOME_STOPPED, false);
|
|
|
yield add_system_comment(task_id, "Task retried by user");
|
|
yield add_system_comment(task_id, "Task retried by user");
|
|
|
|
|
+ yield append_state_change(task_id, STATUS_FAILED, STATUS_QUEUED,
|
|
|
|
|
+ "Task retried by user", EVENT_ACTOR_USER);
|
|
|
yield pulse.bump(task_id, "task retried");
|
|
yield pulse.bump(task_id, "task retried");
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -1589,26 +1560,6 @@ namespace ClankerSaddle.Services {
|
|
|
return newest != null && ((!)newest).answer != null;
|
|
return newest != null && ((!)newest).answer != null;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /** All commands recorded against a session, oldest first. */
|
|
|
|
|
- public async ImmutableLot<SessionCommand> list_commands(int64 session_id) throws Error {
|
|
|
|
|
- return yield db.query<SessionCommand>()
|
|
|
|
|
- .where(expr("session_id == $0", elem<int64?>(session_id)))
|
|
|
|
|
- .order_by(expr("id"))
|
|
|
|
|
- .materialise_async();
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * True when the session carries raw PTY log rows — the terminal
|
|
|
|
|
- * view then renders the concatenated raw log instead of the
|
|
|
|
|
- * typed entry stream.
|
|
|
|
|
- */
|
|
|
|
|
- public async bool has_raw_log(int64 session_id) throws Error {
|
|
|
|
|
- var probe = yield db.query<SessionRawLog>()
|
|
|
|
|
- .where(expr("session_id == $0", elem<int64?>(session_id)))
|
|
|
|
|
- .first_async();
|
|
|
|
|
- return probe != null;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
/**
|
|
/**
|
|
|
* The session still open for `task_id` (no `ended_at`), or null.
|
|
* The session still open for `task_id` (no `ended_at`), or null.
|
|
|
* The null check runs in Vala: the expression layer cannot compare
|
|
* The null check runs in Vala: the expression layer cannot compare
|
|
@@ -1695,11 +1646,14 @@ namespace ClankerSaddle.Services {
|
|
|
|| ((!)task).status == STATUS_WORKING || ((!)task).status == STATUS_BLOCKED
|
|
|| ((!)task).status == STATUS_WORKING || ((!)task).status == STATUS_BLOCKED
|
|
|
|| ((!)task).status == STATUS_REVIEW_REQUESTED
|
|
|| ((!)task).status == STATUS_REVIEW_REQUESTED
|
|
|
|| ((!)task).status == STATUS_SUSPENDED)) {
|
|
|| ((!)task).status == STATUS_SUSPENDED)) {
|
|
|
|
|
+ string from = ((!)task).status;
|
|
|
((!)task).status = STATUS_QUEUED;
|
|
((!)task).status = STATUS_QUEUED;
|
|
|
if (((!)task).suspended_at != null) {
|
|
if (((!)task).suspended_at != null) {
|
|
|
((!)task).suspended_at = null;
|
|
((!)task).suspended_at = null;
|
|
|
}
|
|
}
|
|
|
yield db.update_async<TaskEntity>((!)task);
|
|
yield db.update_async<TaskEntity>((!)task);
|
|
|
|
|
+ yield append_state_change(task_id, from, STATUS_QUEUED,
|
|
|
|
|
+ "Task requeued", EVENT_ACTOR_SYSTEM);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
var sessions = yield db.query<Session>()
|
|
var sessions = yield db.query<Session>()
|