|
@@ -19,7 +19,11 @@ namespace ClankerSaddle.Services {
|
|
|
public const string GOAL_FAILED = "failed";
|
|
public const string GOAL_FAILED = "failed";
|
|
|
public const string GOAL_CANCELLED = "cancelled";
|
|
public const string GOAL_CANCELLED = "cancelled";
|
|
|
|
|
|
|
|
- /** Valid {@link GoalBlock.block_type} values. */
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Valid goal block types (plan 15 C1: the values of the goal-run
|
|
|
|
|
+ * task's `task_blocks.type` rows — question, rfc, plan and the
|
|
|
|
|
+ * completion review).
|
|
|
|
|
+ */
|
|
|
public const string GOAL_BLOCK_QUESTION = "question";
|
|
public const string GOAL_BLOCK_QUESTION = "question";
|
|
|
public const string GOAL_BLOCK_RFC = "rfc";
|
|
public const string GOAL_BLOCK_RFC = "rfc";
|
|
|
public const string GOAL_BLOCK_PLAN = "plan";
|
|
public const string GOAL_BLOCK_PLAN = "plan";
|
|
@@ -46,14 +50,17 @@ namespace ClankerSaddle.Services {
|
|
|
public const int64 GOAL_RUN_PRIORITY = 3;
|
|
public const int64 GOAL_RUN_PRIORITY = 3;
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * Goal storage and lifecycle (plan 12): creation with branch-name
|
|
|
|
|
- * uniqueness validation, the draft → planning → plan review →
|
|
|
|
|
- * active → completion review → complete/failed/cancelled flow
|
|
|
|
|
- * (with paused freezing supervision in between), the goal agent's
|
|
|
|
|
- * run task, plan/completion review flows with revisions, goal
|
|
|
|
|
- * comments and blocks, the cancel cascade and the event-driven
|
|
|
|
|
- * resume machinery (task-terminal events coalesce ~10 s into one
|
|
|
|
|
- * goal-agent resume carrying the batched status report).
|
|
|
|
|
|
|
+ * Goal storage and lifecycle (plan 12; blocks consolidated plan
|
|
|
|
|
+ * 15 C1): creation with branch-name uniqueness validation, the
|
|
|
|
|
+ * draft → planning → plan review → active → completion review →
|
|
|
|
|
+ * complete/failed/cancelled flow (with paused freezing
|
|
|
|
|
+ * supervision in between), the goal agent's run task, plan/
|
|
|
|
|
+ * completion review flows with revisions, goal comments, the
|
|
|
|
|
+ * cancel cascade and the event-driven resume machinery (task-
|
|
|
|
|
+ * terminal events coalesce ~10 s into one goal-agent resume
|
|
|
|
|
+ * carrying the batched status report). The goal agent's BLOCKS
|
|
|
|
|
+ * ride the run task's own `task_blocks` rows — one row per
|
|
|
|
|
+ * question, answered in place through {@link answer_block}.
|
|
|
*/
|
|
*/
|
|
|
public class GoalService : Object {
|
|
public class GoalService : Object {
|
|
|
|
|
|
|
@@ -349,95 +356,70 @@ namespace ClankerSaddle.Services {
|
|
|
// ------------------------------------------------------------------
|
|
// ------------------------------------------------------------------
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * The goal agent submitted (or revised) its plan: a new
|
|
|
|
|
- * goal_blocks row of type 'plan' carrying the markdown, the
|
|
|
|
|
- * goal moves to 'plan_review', and the goal-run task parks on
|
|
|
|
|
- * a mirrored task block so the human verdict resumes it. The
|
|
|
|
|
- * caller has already parked the task via the results state.
|
|
|
|
|
|
|
+ * The goal agent submitted (or revised) its plan: ONE
|
|
|
|
|
+ * review-kind block of type 'plan' on the goal-run task
|
|
|
|
|
+ * carries the markdown, the goal moves to 'plan_review' and
|
|
|
|
|
+ * the run parks on it (the verdict resume rides the answered
|
|
|
|
|
+ * block). The caller has already parked the task via the
|
|
|
|
|
+ * results state. Plan 15 C1: the block lives on the run task
|
|
|
|
|
+ * — there is no second row to keep in sync.
|
|
|
*/
|
|
*/
|
|
|
- public async GoalBlock submit_plan(int64 goal_id, string markdown) throws Error {
|
|
|
|
|
|
|
+ public async TaskBlock? submit_plan(int64 goal_id, string markdown) throws Error {
|
|
|
var goal = yield require_goal(goal_id);
|
|
var goal = yield require_goal(goal_id);
|
|
|
// Idempotent on the pending submission: a redelivered push
|
|
// Idempotent on the pending submission: a redelivered push
|
|
|
// (outbox retry, the entry + state pair of one `saddle
|
|
// (outbox retry, the entry + state pair of one `saddle
|
|
|
// plan`) must not stack duplicate revisions — a NEW row
|
|
// plan`) must not stack duplicate revisions — a NEW row
|
|
|
// only appears once the previous one was answered
|
|
// only appears once the previous one was answered
|
|
|
// (rejected → the agent revises).
|
|
// (rejected → the agent revises).
|
|
|
- foreach (var existing in yield list_blocks(goal_id)) {
|
|
|
|
|
- if (existing.block_type == GOAL_BLOCK_PLAN && existing.answer == null) {
|
|
|
|
|
- return existing;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ TaskBlock? raised = yield raise_run_block(((!)goal), "Plan submitted for review",
|
|
|
|
|
+ GOAL_BLOCK_PLAN, null, markdown, true);
|
|
|
|
|
+ if (raised == null || ((!)raised).answer != null) {
|
|
|
|
|
+ // No pending submission: a fresh revision supersedes
|
|
|
|
|
+ // the answered history.
|
|
|
|
|
+ raised = yield raise_run_block(((!)goal), "Plan submitted for review",
|
|
|
|
|
+ GOAL_BLOCK_PLAN, null, markdown, false);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (raised == null) {
|
|
|
|
|
+ return null;
|
|
|
}
|
|
}
|
|
|
- var block = new GoalBlock() {
|
|
|
|
|
- goal_id = goal_id,
|
|
|
|
|
- question = "Plan submitted for review",
|
|
|
|
|
- block_type = GOAL_BLOCK_PLAN,
|
|
|
|
|
- body = markdown,
|
|
|
|
|
- created_at = new DateTime.now_local()
|
|
|
|
|
- };
|
|
|
|
|
- yield db.insert_async<GoalBlock>(block);
|
|
|
|
|
((!)goal).status = GOAL_PLAN_REVIEW;
|
|
((!)goal).status = GOAL_PLAN_REVIEW;
|
|
|
- yield db.update_async<Goal>((!)goal);
|
|
|
|
|
|
|
+ yield db.update_async<Goal>(((!)goal));
|
|
|
yield record_event(goal_id, "plan-submitted",
|
|
yield record_event(goal_id, "plan-submitted",
|
|
|
- @"Plan revision #$(block.id) submitted for review");
|
|
|
|
|
- yield mirror_block_on_run_task(((!)goal), block);
|
|
|
|
|
- yield pulse.bump(0, "goal plan submitted");
|
|
|
|
|
- push.notify_global(PushService.goal_block_notification(
|
|
|
|
|
- ((!)goal).title, goal_id, GOAL_BLOCK_PLAN, block.question));
|
|
|
|
|
- return block;
|
|
|
|
|
|
|
+ @"Plan revision #$(((!)raised).id) submitted for review");
|
|
|
|
|
+ return raised;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * Records a goal-agent question/RFC block and parks the
|
|
|
|
|
- * goal-run task on its mirrored task block. Idempotent on the
|
|
|
|
|
- * pending question: a redelivered blocked push (outbox retry,
|
|
|
|
|
- * the park-unwinding round-end push) must not stack duplicates.
|
|
|
|
|
|
|
+ * Records a goal-agent question/RFC as ONE block on the
|
|
|
|
|
+ * goal-run task and parks it there. Idempotent on the pending
|
|
|
|
|
+ * question: a redelivered blocked push (outbox retry, the
|
|
|
|
|
+ * park-unwinding round-end push) must not stack duplicates.
|
|
|
*/
|
|
*/
|
|
|
- public async GoalBlock raise_agent_block(int64 goal_id, string question,
|
|
|
|
|
|
|
+ public async TaskBlock? raise_agent_block(int64 goal_id, string question,
|
|
|
string block_type, string[]? options = null, string? body = null) throws Error {
|
|
string block_type, string[]? options = null, string? body = null) throws Error {
|
|
|
var goal = yield require_goal(goal_id);
|
|
var goal = yield require_goal(goal_id);
|
|
|
- foreach (var existing in yield list_blocks(goal_id)) {
|
|
|
|
|
- if (existing.question == question && existing.answer == null) {
|
|
|
|
|
- return existing;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ TaskBlock? raised = yield raise_run_block(((!)goal), question,
|
|
|
|
|
+ block_type.length > 0 ? block_type : GOAL_BLOCK_QUESTION,
|
|
|
|
|
+ options, body, false);
|
|
|
|
|
+ if (raised == null) {
|
|
|
|
|
+ return null;
|
|
|
}
|
|
}
|
|
|
- string options_json = null;
|
|
|
|
|
- if (options != null && options.length > 0) {
|
|
|
|
|
- var array = new Json.Array();
|
|
|
|
|
- foreach (var option in options) {
|
|
|
|
|
- array.add_string_element(option);
|
|
|
|
|
- }
|
|
|
|
|
- options_json = Json.to_string(
|
|
|
|
|
- new Json.Node(Json.NodeType.ARRAY).init_array(array), false);
|
|
|
|
|
- }
|
|
|
|
|
- var block = new GoalBlock() {
|
|
|
|
|
- goal_id = goal_id,
|
|
|
|
|
- question = question,
|
|
|
|
|
- block_type = block_type.length > 0 ? block_type : GOAL_BLOCK_QUESTION,
|
|
|
|
|
- options = options_json,
|
|
|
|
|
- body = body != null && body.length > 0 ? body : null,
|
|
|
|
|
- created_at = new DateTime.now_local()
|
|
|
|
|
- };
|
|
|
|
|
- yield db.insert_async<GoalBlock>(block);
|
|
|
|
|
yield record_event(goal_id, "block-raised",
|
|
yield record_event(goal_id, "block-raised",
|
|
|
- @"$(block.block_type) raised: $(question)");
|
|
|
|
|
- yield mirror_block_on_run_task(((!)goal), block);
|
|
|
|
|
- yield pulse.bump(0, "goal block raised");
|
|
|
|
|
- push.notify_global(PushService.goal_block_notification(
|
|
|
|
|
- ((!)goal).title, goal_id, block.block_type, question));
|
|
|
|
|
- return block;
|
|
|
|
|
|
|
+ @"$(((!)raised).block_type) raised: $(question)");
|
|
|
|
|
+ return raised;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* The goal agent submitted its completion report (`saddle
|
|
* The goal agent submitted its completion report (`saddle
|
|
|
* done` in goal context): the goal moves to
|
|
* done` in goal context): the goal moves to
|
|
|
- * 'completion_review', the report is stored (goal row +
|
|
|
|
|
- * goal_blocks type 'completion') and the goal-run task parks
|
|
|
|
|
- * on the mirrored block. The daemon enforces the
|
|
|
|
|
- * branch-push rule before letting `done` through. Idempotent
|
|
|
|
|
- * on terminal goals (plan 13 B2): a redelivered done push
|
|
|
|
|
- * must not resurrect a finished goal.
|
|
|
|
|
|
|
+ * 'completion_review', the report is stored (goal row + ONE
|
|
|
|
|
+ * completion-kind block on the goal-run task) and the run
|
|
|
|
|
+ * parks on it. The daemon enforces the branch-push rule
|
|
|
|
|
+ * before letting `done` through. Idempotent on terminal goals
|
|
|
|
|
+ * (plan 13 B2): a redelivered done push must not resurrect a
|
|
|
|
|
+ * finished goal.
|
|
|
*/
|
|
*/
|
|
|
- public async GoalBlock submit_completion(int64 goal_id, string report) throws Error {
|
|
|
|
|
|
|
+ public async TaskBlock? submit_completion(int64 goal_id, string report) throws Error {
|
|
|
var goal = yield require_goal(goal_id);
|
|
var goal = yield require_goal(goal_id);
|
|
|
if (((!)goal).status == GOAL_COMPLETE || ((!)goal).status == GOAL_FAILED
|
|
if (((!)goal).status == GOAL_COMPLETE || ((!)goal).status == GOAL_FAILED
|
|
|
|| ((!)goal).status == GOAL_CANCELLED) {
|
|
|| ((!)goal).status == GOAL_CANCELLED) {
|
|
@@ -447,44 +429,31 @@ namespace ClankerSaddle.Services {
|
|
|
@"The goal is already $(((!)goal).status) — its completion review is closed");
|
|
@"The goal is already $(((!)goal).status) — its completion review is closed");
|
|
|
}
|
|
}
|
|
|
// Idempotent on the pending report (see submit_plan).
|
|
// Idempotent on the pending report (see submit_plan).
|
|
|
- foreach (var existing in yield list_blocks(goal_id)) {
|
|
|
|
|
- if (existing.block_type == GOAL_BLOCK_COMPLETION
|
|
|
|
|
- && existing.answer == null) {
|
|
|
|
|
- return existing;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ TaskBlock? raised = yield raise_run_block(((!)goal), "Goal completion report",
|
|
|
|
|
+ GOAL_BLOCK_COMPLETION, null, report, true);
|
|
|
|
|
+ if (raised == null) {
|
|
|
|
|
+ return null;
|
|
|
}
|
|
}
|
|
|
((!)goal).status = GOAL_COMPLETION_REVIEW;
|
|
((!)goal).status = GOAL_COMPLETION_REVIEW;
|
|
|
((!)goal).completion_report = report;
|
|
((!)goal).completion_report = report;
|
|
|
- yield db.update_async<Goal>((!)goal);
|
|
|
|
|
- var block = new GoalBlock() {
|
|
|
|
|
- goal_id = goal_id,
|
|
|
|
|
- question = "Goal completion report",
|
|
|
|
|
- block_type = GOAL_BLOCK_COMPLETION,
|
|
|
|
|
- body = report,
|
|
|
|
|
- created_at = new DateTime.now_local()
|
|
|
|
|
- };
|
|
|
|
|
- yield db.insert_async<GoalBlock>(block);
|
|
|
|
|
|
|
+ yield db.update_async<Goal>(((!)goal));
|
|
|
yield record_event(goal_id, "completion-submitted",
|
|
yield record_event(goal_id, "completion-submitted",
|
|
|
"Completion report submitted for review");
|
|
"Completion report submitted for review");
|
|
|
- yield mirror_block_on_run_task(((!)goal), block);
|
|
|
|
|
- yield pulse.bump(0, "goal completion submitted");
|
|
|
|
|
- push.notify_global(PushService.goal_block_notification(
|
|
|
|
|
- ((!)goal).title, goal_id, GOAL_BLOCK_COMPLETION, block.question));
|
|
|
|
|
- return block;
|
|
|
|
|
|
|
+ return raised;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * The goal agent failed the goal (`saddle failure` in goal
|
|
|
|
|
- * context): status 'failed', the report stored, the goal-run
|
|
|
|
|
- * task failed (its session closes and saddle frees), every
|
|
|
|
|
- * non-terminal task cascades exactly like a cancellation
|
|
|
|
|
- * (plan 13 B3: drafts are deleted — no orphan stays
|
|
|
|
|
- * dispatchable — queued/working/blocked tasks are cancelled)
|
|
|
|
|
- * and the narrative records the cascade. Every user is
|
|
|
|
|
- * notified. Idempotent on terminal goals (plan 13 B2): a
|
|
|
|
|
- * replayed failure push — redelivered OR a fresh entry id
|
|
|
|
|
- * from the honest burst — re-runs no bookkeeping, so exactly
|
|
|
|
|
- * one goal-failed event, one cascade, one notification, ever.
|
|
|
|
|
|
|
+ * Fails the goal (`saddle failure` in goal context): status
|
|
|
|
|
+ * 'failed', the report stored, the goal-run task failed (its
|
|
|
|
|
+ * session closes and saddle frees), every non-terminal task
|
|
|
|
|
+ * cascades exactly like a cancellation (plan 13 B3: drafts
|
|
|
|
|
+ * are deleted — no orphan stays dispatchable — queued/working/
|
|
|
|
|
+ * blocked tasks are cancelled) and the narrative records the
|
|
|
|
|
+ * cascade. Every user is notified. Idempotent on terminal
|
|
|
|
|
+ * goals (plan 13 B2): a replayed failure push — redelivered
|
|
|
|
|
+ * OR a fresh entry id from the honest burst — re-runs no
|
|
|
|
|
+ * bookkeeping, so exactly one goal-failed event, one cascade,
|
|
|
|
|
+ * one notification, ever.
|
|
|
*/
|
|
*/
|
|
|
public async void fail_goal(int64 goal_id, string? report) throws Error {
|
|
public async void fail_goal(int64 goal_id, string? report) throws Error {
|
|
|
var goal = yield require_goal(goal_id);
|
|
var goal = yield require_goal(goal_id);
|
|
@@ -495,7 +464,7 @@ namespace ClankerSaddle.Services {
|
|
|
((!)goal).status = GOAL_FAILED;
|
|
((!)goal).status = GOAL_FAILED;
|
|
|
((!)goal).completed_at = new DateTime.now_local();
|
|
((!)goal).completed_at = new DateTime.now_local();
|
|
|
((!)goal).completion_report = report ?? ((!)goal).completion_report;
|
|
((!)goal).completion_report = report ?? ((!)goal).completion_report;
|
|
|
- yield db.update_async<Goal>((!)goal);
|
|
|
|
|
|
|
+ yield db.update_async<Goal>(((!)goal));
|
|
|
|
|
|
|
|
var run = yield goal_run_task(goal_id);
|
|
var run = yield goal_run_task(goal_id);
|
|
|
if (run != null && ((!)run).status != STATUS_DONE
|
|
if (run != null && ((!)run).status != STATUS_DONE
|
|
@@ -533,15 +502,17 @@ namespace ClankerSaddle.Services {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * Answers a goal block (the goal page's single verdict path):
|
|
|
|
|
- * the answer lands on the goal row AND on EVERY unanswered
|
|
|
|
|
- * mirror of it on the goal-run task — both halves of the
|
|
|
|
|
- * dual-rowed pair settle before any observer pulse fires
|
|
|
|
|
- * (plan 14 B3's mirror-sync: a one-sided pair is exactly the
|
|
|
|
|
- * staleness the dashboard pile suffered); the parked-resume
|
|
|
|
|
- * machinery then wakes the agent with the verdict as its
|
|
|
|
|
- * resume_notice. Plan and completion blocks apply their
|
|
|
|
|
- * goal-state transitions:
|
|
|
|
|
|
|
+ * Answers a goal's block (plan 15 C1: THE single answer path
|
|
|
|
|
+ * every surface calls — the goal page's verdict forms, the
|
|
|
|
|
+ * task-page deep link on a goal-run block, the dashboard's
|
|
|
|
|
+ * link-through destination). The block is the goal-run task's
|
|
|
|
|
+ * OWN row: answering it in place settles everything an
|
|
|
|
|
+ * observer can read before the first pulse fires, so the
|
|
|
|
|
+ * completion-review-never-leaves-the-dashboard bug class is
|
|
|
|
|
+ * structurally impossible — no dual rows exist to fall out of
|
|
|
|
|
+ * sync. The parked-resume machinery then wakes the agent with
|
|
|
|
|
+ * the verdict as its resume_notice. Plan and completion
|
|
|
|
|
+ * blocks apply their goal-state transitions:
|
|
|
*
|
|
*
|
|
|
* - plan + {@link NOTICE_PLAN_APPROVED} → the submission
|
|
* - plan + {@link NOTICE_PLAN_APPROVED} → the submission
|
|
|
* becomes goal.plan, status 'active';
|
|
* becomes goal.plan, status 'active';
|
|
@@ -551,9 +522,9 @@ namespace ClankerSaddle.Services {
|
|
|
* - completion + feedback → status 'active' (the agent keeps
|
|
* - completion + feedback → status 'active' (the agent keeps
|
|
|
* working).
|
|
* working).
|
|
|
*/
|
|
*/
|
|
|
- public async void answer_goal_block(int64 block_id, string answer,
|
|
|
|
|
|
|
+ public async void answer_block(int64 block_id, string answer,
|
|
|
string? answered_by = null) throws Error {
|
|
string? answered_by = null) throws Error {
|
|
|
- var block = yield db.query<GoalBlock>()
|
|
|
|
|
|
|
+ var block = yield db.query<TaskBlock>()
|
|
|
.where(expr("id == $0", elem<int64?>(block_id)))
|
|
.where(expr("id == $0", elem<int64?>(block_id)))
|
|
|
.first_async();
|
|
.first_async();
|
|
|
if (block == null) {
|
|
if (block == null) {
|
|
@@ -563,25 +534,26 @@ namespace ClankerSaddle.Services {
|
|
|
if (((!)block).answer != null) {
|
|
if (((!)block).answer != null) {
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
- var goal = yield get_goal(((!)block).goal_id);
|
|
|
|
|
|
|
+ var task = yield db.query<TaskEntity>()
|
|
|
|
|
+ .where(expr("id == $0", elem<int64?>(((!)block).task_id)))
|
|
|
|
|
+ .first_async();
|
|
|
|
|
+ if (task == null || ((!)task).kind != TASK_KIND_GOAL_RUN
|
|
|
|
|
+ || ((!)task).goal_id <= 0) {
|
|
|
|
|
+ // Not a goal-run block: the plain task answer path.
|
|
|
|
|
+ yield tasks.answer_block(block_id, answer, answered_by);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ var goal = yield get_goal(((!)task).goal_id);
|
|
|
if (goal == null) {
|
|
if (goal == null) {
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- ((!)block).answer = answer;
|
|
|
|
|
- ((!)block).answered_at = new DateTime.now_local();
|
|
|
|
|
- ((!)block).answered_by = answered_by;
|
|
|
|
|
- yield db.update_async<GoalBlock>((!)block);
|
|
|
|
|
-
|
|
|
|
|
- // Plan 14 B3 mirror-sync ordering: the verdict's events
|
|
|
|
|
- // record FIRST, then both halves of the dual-rowed pair
|
|
|
|
|
- // settle (goal row above, every unanswered mirror below)
|
|
|
|
|
- // before the first observer pulse fires — a one-sided
|
|
|
|
|
- // pair is exactly the staleness the dashboard pile
|
|
|
|
|
- // suffered, and an event-less resume is a lost
|
|
|
|
|
- // context_update. The dispatcher's periodic tick can
|
|
|
|
|
- // strike anywhere in between; by the time either row
|
|
|
|
|
- // reads answered, the events are already committed.
|
|
|
|
|
|
|
+ // The verdict's events record FIRST, then the row settles
|
|
|
|
|
+ // (below) before the first observer pulse fires — an
|
|
|
|
|
+ // event-less resume is a lost context_update, and an
|
|
|
|
|
+ // unanswered row past the pulse is exactly the stale-pile
|
|
|
|
|
+ // shape plan 14 B3 defended against with mirror sweeps.
|
|
|
|
|
+ // With one row there is nothing left to sweep.
|
|
|
if (((!)block).block_type == GOAL_BLOCK_PLAN) {
|
|
if (((!)block).block_type == GOAL_BLOCK_PLAN) {
|
|
|
if (answer == NOTICE_PLAN_APPROVED) {
|
|
if (answer == NOTICE_PLAN_APPROVED) {
|
|
|
if (((!)block).body != null && ((!)block).body.length > 0) {
|
|
if (((!)block).body != null && ((!)block).body.length > 0) {
|
|
@@ -607,26 +579,16 @@ namespace ClankerSaddle.Services {
|
|
|
"Completion report rejected with feedback — the agent continues");
|
|
"Completion report rejected with feedback — the agent continues");
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
- yield db.update_async<Goal>((!)goal);
|
|
|
|
|
|
|
+ yield db.update_async<Goal>(((!)goal));
|
|
|
|
|
+
|
|
|
|
|
+ yield tasks.answer_block(block_id, answer, answered_by);
|
|
|
|
|
|
|
|
- var run = yield goal_run_task(((!)goal).id);
|
|
|
|
|
- if (run != null) {
|
|
|
|
|
- foreach (var task_block in yield tasks.list_blocks(((!)run).id)) {
|
|
|
|
|
- if (task_block.question == ((!)block).question
|
|
|
|
|
- && task_block.answer == null) {
|
|
|
|
|
- yield tasks.answer_block(task_block.id, answer, answered_by);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- if (((!)block).block_type == GOAL_BLOCK_COMPLETION
|
|
|
|
|
- && answer == NOTICE_REVIEW_APPROVED) {
|
|
|
|
|
- // Approving the completion closes the goal agent's
|
|
|
|
|
- // own task: the run is finished, its saddle frees.
|
|
|
|
|
- yield tasks.complete_task(((!)run).id,
|
|
|
|
|
- ((!)goal).completion_report, null, "done");
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
if (((!)block).block_type == GOAL_BLOCK_COMPLETION
|
|
if (((!)block).block_type == GOAL_BLOCK_COMPLETION
|
|
|
&& answer == NOTICE_REVIEW_APPROVED) {
|
|
&& answer == NOTICE_REVIEW_APPROVED) {
|
|
|
|
|
+ // Approving the completion closes the goal agent's
|
|
|
|
|
+ // own task: the run is finished, its saddle frees.
|
|
|
|
|
+ yield tasks.complete_task(((!)task).id,
|
|
|
|
|
+ ((!)goal).completion_report, null, "done");
|
|
|
push.notify_global(PushService.goal_terminal_notification(
|
|
push.notify_global(PushService.goal_terminal_notification(
|
|
|
((!)goal).title, ((!)goal).id, true));
|
|
((!)goal).title, ((!)goal).id, true));
|
|
|
}
|
|
}
|
|
@@ -634,28 +596,44 @@ namespace ClankerSaddle.Services {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * Raises the mirror of a goal block on the goal-run task: the
|
|
|
|
|
- * parked-resume machinery (answered blocks ride the resuming
|
|
|
|
|
- * allocation) works untouched, and the goal-run task parks as
|
|
|
|
|
- * 'blocked' waiting for the human verdict. A no-op when the
|
|
|
|
|
- * run task does not exist or already carries the question.
|
|
|
|
|
- * The mirror is QUIET — the goal-level block already pushed
|
|
|
|
|
- * its notification (goal title, /goals/<id> URL); the mirror
|
|
|
|
|
- * must not double-notify with the run task's title.
|
|
|
|
|
|
|
+ * Raises the goal's ONE block on the goal-run task (plan 15
|
|
|
|
|
+ * C1): the parked-resume machinery (answered blocks ride the
|
|
|
|
|
+ * resuming allocation) works untouched, and the goal-run task
|
|
|
|
|
+ * parks as 'blocked' waiting for the human verdict. The raise
|
|
|
|
|
+ * is QUIET on the task side — the goal-level notification
|
|
|
|
|
+ * (goal title, /goals/<id> URL) fires here, once, and must
|
|
|
|
|
+ * not double-notify with the run task's title.
|
|
|
|
|
+ *
|
|
|
|
|
+ * Deduplication (`dedupe_on_type`): a pending row of the same
|
|
|
|
|
+ * question — or, for the review kinds whose question text is
|
|
|
|
|
+ * fixed ("Plan submitted for review", "Goal completion
|
|
|
|
|
+ * report"), of the same TYPE — means the push is a
|
|
|
|
|
+ * redelivery and the existing row is returned unwritten. A
|
|
|
|
|
+ * missing or terminal run task raises nothing (null).
|
|
|
*/
|
|
*/
|
|
|
- private async void mirror_block_on_run_task(Goal goal, GoalBlock block) throws Error {
|
|
|
|
|
|
|
+ private async TaskBlock? raise_run_block(Goal goal, string question,
|
|
|
|
|
+ string block_type, string[]? options, string? body,
|
|
|
|
|
+ bool dedupe_on_type) throws Error {
|
|
|
var run = yield goal_run_task(goal.id);
|
|
var run = yield goal_run_task(goal.id);
|
|
|
if (run == null || ((!)run).status == STATUS_DONE
|
|
if (run == null || ((!)run).status == STATUS_DONE
|
|
|
|| ((!)run).status == STATUS_FAILED) {
|
|
|| ((!)run).status == STATUS_FAILED) {
|
|
|
- return;
|
|
|
|
|
|
|
+ warning("[ClankerSaddle] Goal %s block '%s' raised without a live run task",
|
|
|
|
|
+ goal.id.to_string(), question);
|
|
|
|
|
+ return null;
|
|
|
}
|
|
}
|
|
|
foreach (var existing in yield tasks.list_blocks(((!)run).id)) {
|
|
foreach (var existing in yield tasks.list_blocks(((!)run).id)) {
|
|
|
- if (existing.question == block.question && existing.answer == null) {
|
|
|
|
|
- return;
|
|
|
|
|
|
|
+ bool same = dedupe_on_type
|
|
|
|
|
+ ? existing.block_type == block_type : existing.question == question;
|
|
|
|
|
+ if (same && existing.answer == null) {
|
|
|
|
|
+ return existing;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
- yield tasks.add_block(((!)run).id, block.question, block.block_type,
|
|
|
|
|
- TaskService.parse_block_options(block.options), block.body, true);
|
|
|
|
|
|
|
+ var raised = yield tasks.add_block(((!)run).id, question, block_type,
|
|
|
|
|
+ options, body, true);
|
|
|
|
|
+ yield pulse.bump(0, "goal block raised");
|
|
|
|
|
+ push.notify_global(PushService.goal_block_notification(
|
|
|
|
|
+ goal.title, goal.id, block_type, question));
|
|
|
|
|
+ return raised;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// ------------------------------------------------------------------
|
|
// ------------------------------------------------------------------
|
|
@@ -685,12 +663,18 @@ namespace ClankerSaddle.Services {
|
|
|
.materialise_async();
|
|
.materialise_async();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /** A goal's blocks, oldest first. */
|
|
|
|
|
- public async ImmutableLot<GoalBlock> list_blocks(int64 goal_id) throws Error {
|
|
|
|
|
- return yield db.query<GoalBlock>()
|
|
|
|
|
- .where(expr("goal_id == $0", elem<int64?>(goal_id)))
|
|
|
|
|
- .order_by(expr("id"))
|
|
|
|
|
- .materialise_async();
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * A goal's blocks (plan 15 C1): the goal-run task's
|
|
|
|
|
+ * `task_blocks` rows, oldest first — the run task is the
|
|
|
|
|
+ * single home of the goal agent's questions, plans and
|
|
|
|
|
+ * completion reports. Empty while the goal has no run task.
|
|
|
|
|
+ */
|
|
|
|
|
+ public async ImmutableLot<TaskBlock> list_blocks(int64 goal_id) throws Error {
|
|
|
|
|
+ var run = yield goal_run_task(goal_id);
|
|
|
|
|
+ if (run == null) {
|
|
|
|
|
+ return new Vector<TaskBlock>().to_immutable_buffer();
|
|
|
|
|
+ }
|
|
|
|
|
+ return yield tasks.list_blocks(((!)run).id);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/** A goal's narrative events, oldest first. */
|
|
/** A goal's narrative events, oldest first. */
|