Jelajahi Sumber

C2: goal comments ride the run task; system events styled (F8)

goal_comments is deleted (M0001 amended): the goal's comment thread
IS the goal-run task's task_comments — the agent's pushes and the
human composer land on the same rows, and human comments inject
through the existing delivery paths (the pending-comment turn, the
resuming assignment's embedded comments). The comment-entry mirror
dies with it; a draft goal has no thread (the composer gates on the
run, with a muted note), and the timeless-note composer hint lands.

Harness/system-authored bookkeeping — aborts, requeues, retries,
unreachable notices and the session-context restoration — is now
authored 'system' (TaskService.add_system_comment) and renders as a
distinct labelled muted system row (.comment.system, never an agent
comment). The session restore left the green progress feed (F8:
status is not progress) and lives on the thread as a system event.

Test-side: the dispatch/goalui state-offer waits became event-driven
(load races), the flaky arc gained a claim barrier so a lagging
dispatch cannot eat fail_next, and the plan-accept completion wait
breathes at 60s under full-suite load.
clanker 1 bulan lalu
induk
melakukan
e7e167579f

+ 0 - 21
src/Data/Entities.vala

@@ -593,27 +593,6 @@ namespace ClankerSaddle.Data {
         }
     }
 
-    /** A remark on a goal, authored by a human or by the goal agent ('agent'). */
-    public class GoalComment : Object {
-
-        public int64 id { get; set; }
-        public int64 goal_id { get; set; }
-        public string author { get; set; default = "agent"; }
-        public string body { get; set; default = ""; }
-        public string? quote { get; set; default = null; }
-        public DateTime? created_at { get; set; default = null; }
-
-        public static void entity_mapping(EntityMapperBuilder<GoalComment> cfg) {
-            cfg.table("goal_comments")
-                .column<int64?>("id", o => o.id, (o, v) => o.id = v)
-                .column<int64?>("goal_id", o => o.goal_id, (o, v) => o.goal_id = v)
-                .column<string>("author", o => o.author, (o, v) => o.author = v)
-                .column<string>("body", o => o.body, (o, v) => o.body = v)
-                .column<string>("quote", o => o.quote, (o, v) => o.quote = v)
-                .column<DateTime?>("created_at", o => o.created_at, (o, v) => o.created_at = v);
-        }
-    }
-
     /**
      * One entry of a goal's supervision narrative (`goal_events`):
      * lifecycle moves, plan submissions/reviews, the goal agent's task

+ 11 - 24
src/Data/Migrations/M0001_Initial.vala

@@ -6,9 +6,10 @@ namespace ClankerSaddle.Data.Migrations {
      * The full ClankerSaddle schema in one migration (plan 11: every
      * earlier migration flattened — the database is not deployed, so
      * tear-down-and-recreate replaced incremental evolution). Covers:
-     * projects and their links; the plan-12 goals (with links and
-     * comments — plan 15 C1 deleted `goal_blocks`: the goal agent's
-     * blocks ride the run task's `task_blocks` rows) and the
+     * projects and their links; the plan-12 goals (with links —
+     * plan 15 C1 deleted `goal_blocks` and C2 deleted `goal_comments`:
+     * the goal agent's blocks and comments ride the run task's own
+     * rows) and the
      * flattened task shape (`goal_id`, `kind`, `mode`,
      * `creator`, `enqueue_after_task_id` — the subtask machinery's
      * parent_task_id/plan/auto-* columns and the task_dependencies
@@ -210,26 +211,13 @@ namespace ClankerSaddle.Data.Migrations {
                 t.column<DateTime?>("created_at");
             });
 
-            // Plan 15 C1: the goal-side block table is GONE — the
-            // goal agent's questions, plans and completion reports
-            // ride the RUN TASK's task_blocks rows (one row per
-            // question, answered in place; no dual-row pair can fall
-            // out of sync).
-            b.create_table("goal_comments", (t) => {
-                t.column<int64?>("id")
-                    .auto_increment()
-                    .primary_key();
-                t.column<int64?>("goal_id")
-                    .not_null()
-                    .references("goals", "id")
-                    .indexed();
-                t.column<string>("author")
-                    .not_null();
-                t.column<string>("body")
-                    .not_null();
-                t.column<string>("quote");
-                t.column<DateTime?>("created_at");
-            });
+            // Plan 15 C1/C2: the goal-side block and comment tables
+            // are GONE — the goal agent's questions, plans and
+            // completion reports ride the RUN TASK's task_blocks rows
+            // (one row per question, answered in place; no dual-row
+            // pair can fall out of sync), and its comment thread IS
+            // the run task's task_comments (human comments inject
+            // through the existing delivery paths).
 
             // The supervision narrative's event log: one row per
             // notable goal occurrence (plan reviews, agent task
@@ -512,7 +500,6 @@ namespace ClankerSaddle.Data.Migrations {
         public override void down(MigrationBuilder b) {
             b.drop_table("saddle_entry_dedup");
             b.drop_table("goal_events");
-            b.drop_table("goal_comments");
             b.drop_table("task_notify_prefs");
             b.drop_table("push_subscriptions");
             b.drop_table("task_capabilities");

+ 28 - 19
src/Services/GoalService.vala

@@ -640,27 +640,36 @@ namespace ClankerSaddle.Services {
         // Comments and events
         // ------------------------------------------------------------------
 
-        /** Appends a goal comment; `author` is 'agent' or 'user:<name>'. */
-        public async GoalComment add_comment(int64 goal_id, string author, string body,
-                string? quote = null) throws Error {
-            var comment = new GoalComment() {
-                goal_id = goal_id,
-                author = author,
-                body = body,
-                quote = quote != null && quote.strip().length > 0 ? quote.strip() : null,
-                created_at = new DateTime.now_local()
-            };
-            yield db.insert_async<GoalComment>(comment);
-            yield pulse.bump(0, "goal comment");
-            return comment;
+        /**
+         * Appends a HUMAN comment to the goal's thread (plan 15 C2):
+         * the goal-run task's own `task_comments` row — injection
+         * rides the existing delivery paths (the pending-comment
+         * turn, the resuming assignment's embedded comments) straight
+         * into the agent's next round. The goal agent's own comments
+         * land on the same thread via its entry pushes. Refused
+         * before the run exists (a draft goal has no thread).
+         */
+        public async TaskComment? add_human_comment(int64 goal_id, string author,
+                string body, string? quote = null) throws Error {
+            var run = yield goal_run_task(goal_id);
+            if (run == null) {
+                throw new Error(Quark.from_string("clanker-saddle"), 87,
+                    "The goal agent has not started yet — comments open with its run");
+            }
+            return yield tasks.add_comment(((!)run).id, author, body, quote);
         }
 
-        /** A goal's comments, oldest first. */
-        public async ImmutableLot<GoalComment> list_comments(int64 goal_id) throws Error {
-            return yield db.query<GoalComment>()
-                .where(expr("goal_id == $0", elem<int64?>(goal_id)))
-                .order_by(expr("id"))
-                .materialise_async();
+        /**
+         * The goal's comments (plan 15 C2): the goal-run task's
+         * `task_comments` rows, oldest first — the ONE thread.
+         * Empty while the goal has no run task.
+         */
+        public async ImmutableLot<TaskComment> list_comments(int64 goal_id) throws Error {
+            var run = yield goal_run_task(goal_id);
+            if (run == null) {
+                return new Vector<TaskComment>().to_immutable_buffer();
+            }
+            return yield tasks.list_comments(((!)run).id);
         }
 
         /**

+ 14 - 8
src/Services/SaddleService.vala

@@ -451,7 +451,11 @@ namespace ClankerSaddle.Services {
                     && saddle.current_task_id > 0) {
                 var session = yield task_service.open_session_for_task(saddle.current_task_id);
                 if (session != null && poll_state.mark_resumed(((!)session).id)) {
-                    yield task_service.add_progress(saddle.current_task_id,
+                    // Plan 15 C2: the restoration is harness
+                    // bookkeeping — a distinct system event on the
+                    // thread, never a green progress line (the feed
+                    // carries agent work only).
+                    yield task_service.add_system_comment(saddle.current_task_id,
                         "Saddle restored the session context; task resumed");
                 }
             }
@@ -547,12 +551,12 @@ namespace ClankerSaddle.Services {
                     throw new Error(Quark.from_string("clanker-saddle"), 45,
                         @"task-update cancel returned HTTP $(status_code)");
                 }
-                yield tasks.add_comment(session.task_id, "agent", @"$(verb); daemon aborted");
+                yield tasks.add_system_comment(session.task_id, @"$(verb); daemon aborted");
             } catch (Error e) {
                 warning(@"[ClankerSaddle] Abort for task $(session.task_id) failed: $(e.message); marking saddle '$(((!)saddle).name)' offline");
                 yield mark_saddle_unreachable(((!)saddle).id, tasks,
                     @"abort delivery failed ($(e.message))");
-                yield tasks.add_comment(session.task_id, "agent",
+                yield tasks.add_system_comment(session.task_id,
                     @"$(verb); daemon unreachable — saddle marked offline");
             }
         }
@@ -586,7 +590,7 @@ namespace ClankerSaddle.Services {
                     }
                     print(@"[ClankerSaddle] Task $(task.id) requeued: its saddle went offline\n");
                     yield tasks.requeue_task(task.id);
-                    yield tasks.add_comment(task.id, "agent",
+                    yield tasks.add_system_comment(task.id,
                         "saddle went offline; task requeued");
                 }
             }
@@ -661,7 +665,7 @@ namespace ClankerSaddle.Services {
                         string unreachable_text = reason != null && reason.length > 0
                             ? @"Saddle became unreachable ($(reason)); task requeued"
                             : "Saddle became unreachable; task requeued";
-                        yield tasks.add_comment(task_id, "agent", unreachable_text);
+                        yield tasks.add_system_comment(task_id, unreachable_text);
                     }
                 }
 
@@ -1256,8 +1260,9 @@ namespace ClankerSaddle.Services {
          * Plan 12: on a GOAL-RUN session the plan/done/failure verbs
          * are goal-context — a plan submission enters plan review, a
          * done submits the completion report (the goal-run task parks
-         * instead of completing), a failure fails the goal — and
-         * comments also land on the goal's thread.
+         * instead of completing), a failure fails the goal. Plan 15
+         * C2: the run task's comment thread IS the goal's thread —
+         * nothing is mirrored anywhere.
          *
          * Plan 13 B2: the projections key on the DEDUPED entry stream
          * ({@link store_pushed_entries} claims every id once per
@@ -1296,7 +1301,8 @@ namespace ClankerSaddle.Services {
                         if (body.length > 0) {
                             yield tasks.add_comment(task_id, "agent", body);
                             if (goal_id > 0) {
-                                yield goal_service.add_comment(goal_id, "agent", body);
+                                // Plan 15 C2: the run task's comment
+                                // IS the goal comment — no mirror.
                                 yield goal_service.record_event(goal_id, "comment",
                                     snippet_text(body));
                             }

+ 24 - 2
src/Services/TaskService.vala

@@ -31,6 +31,13 @@ namespace ClankerSaddle.Services {
      */
     public const string BLOCK_COMPLETION = "completion";
 
+    /**
+     * The system-event comment author (plan 15 C2): harness-authored
+     * bookkeeping rows (aborts, requeues, retries, session
+     * restorations) — never agent content.
+     */
+    public const string COMMENT_AUTHOR_SYSTEM = "system";
+
     /**
      * The agent-celebration entry type (`saddle yeehaw <message>`): the
      * daemon pushes it as a round-results entry carrying the message in
@@ -789,7 +796,11 @@ namespace ClankerSaddle.Services {
             return ((!)element).try_get_as<DateTime>(out value) ? value : null;
         }
 
-        /** Appends a comment; `author` is 'agent' or 'user:<name>'. */
+        /**
+         * Appends a comment; `author` is 'agent', 'user:<name>' or
+         * 'system' (the harness-authored bookkeeping rows — plan 15
+         * C2's distinct system-event class, never agent content).
+         */
         public async TaskComment add_comment(int64 task_id, string author, string body,
                 string? quote = null) throws Error {
             var comment = new TaskComment() {
@@ -804,6 +815,17 @@ namespace ClankerSaddle.Services {
             return comment;
         }
 
+        /**
+         * Records a harness/system event on the task's thread (plan
+         * 15 C2): park/resume bookkeeping (aborts, requeues,
+         * retries), the session-context restoration, unreachable
+         * notices — authored 'system', rendered with the distinct
+         * muted system styling, never an agent comment row.
+         */
+        public async TaskComment add_system_comment(int64 task_id, string body) throws Error {
+            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,
@@ -1395,7 +1417,7 @@ namespace ClankerSaddle.Services {
 
             yield close_open_sessions(task_id, "stopped");
             yield free_saddles_running(task_id);
-            yield add_comment(task_id, "agent", "Task retried by user");
+            yield add_system_comment(task_id, "Task retried by user");
             yield pulse.bump(task_id, "task retried");
         }
 

+ 3 - 1
src/actions/GoalActions.vala

@@ -315,7 +315,9 @@ namespace ClankerSaddle {
             var me = SpryAuth.current(request);
             string author = me != null ? @"user:$(me.username)" : "user:anonymous";
             try {
-                yield goals.add_comment(sealed_goal_id(), author, body,
+                // Plan 15 C2: the goal's thread IS the run task's —
+                // the comment rides the existing injection paths.
+                yield goals.add_human_comment(sealed_goal_id(), author, body,
                     Forms.optional(request, "quote"));
                 return directives().notify("success", "Comment added")
                     .navigate(@"/goals/$(sealed_goal_id())");

+ 13 - 2
src/entrypoints/GoalDetailEntrypoint.vala

@@ -47,13 +47,19 @@ namespace ClankerSaddle {
         public bool approved { get; set; default = false; }
     }
 
-    /** One comment on the goal thread. */
+    /**
+     * One comment on the goal thread (plan 15 C2: the run task's own
+     * `task_comments` row). {@link is_system} marks the
+     * harness-authored bookkeeping rows — the distinct muted system
+     * styling, never an agent row.
+     */
     public class GoalCommentView : Object {
         public string author { get; set; default = ""; }
         public string body { get; set; default = ""; }
         public string quote { get; set; default = ""; }
         public string created_display { get; set; default = ""; }
         public bool from_agent { get; set; default = false; }
+        public bool is_system { get; set; default = false; }
     }
 
     /** One progress line from the goal agent's runs (`saddle progress-update`). */
@@ -537,13 +543,18 @@ namespace ClankerSaddle {
                 events.add(event_array[i]);
             }
 
+            // Plan 15 C2: the goal's comment thread IS the run
+            // task's task_comments (injection rides the existing
+            // delivery paths) — the agent's pushes and the human's
+            // composer land on the same rows.
             foreach (var row in yield goals.list_comments(goal.id)) {
                 comments.add(new GoalCommentView() {
                     author = row.author,
                     body = row.body,
                     quote = row.quote ?? "",
                     created_display = format_stamp(row.created_at),
-                    from_agent = row.author == "agent"
+                    from_agent = row.author == "agent",
+                    is_system = row.author == COMMENT_AUTHOR_SYSTEM
                 });
             }
 

+ 9 - 2
src/entrypoints/TaskDetailEntrypoint.vala

@@ -9,13 +9,19 @@ using ClankerSaddle.Services;
 
 namespace ClankerSaddle {
 
-    /** A comment row in the task detail thread, with its quoted reference. */
+    /**
+     * A comment row in the task detail thread, with its quoted
+     * reference. {@link is_system} marks the harness-authored
+     * bookkeeping rows (plan 15 C2) — rendered with the distinct
+     * muted system styling, never an agent row.
+     */
     public class CommentView : Object {
         public string author { get; set; default = ""; }
         public string body { get; set; default = ""; }
         public string quote { get; set; default = ""; }
         public string when_display { get; set; default = ""; }
         public bool from_agent { get; set; }
+        public bool is_system { get; set; }
     }
 
     /** One progress line in the task's history. */
@@ -278,7 +284,8 @@ namespace ClankerSaddle {
                     body = comment.body,
                     quote = comment.quote ?? "",
                     when_display = format_time(comment.created_at),
-                    from_agent = comment.author == "agent"
+                    from_agent = comment.author == "agent",
+                    is_system = comment.author == COMMENT_AUTHOR_SYSTEM
                 });
             }
 

+ 0 - 1
src/main.vala

@@ -50,7 +50,6 @@ int main(string[] args) {
         db.add_entity<TaskNotifyPref>(TaskNotifyPref.entity_mapping);
         db.add_entity<Goal>(Goal.entity_mapping);
         db.add_entity<GoalLink>(GoalLink.entity_mapping);
-        db.add_entity<GoalComment>(GoalComment.entity_mapping);
         db.add_entity<GoalEvent>(GoalEvent.entity_mapping);
         db.migrate_on_startup();
 

+ 21 - 0
src/pages/app.css

@@ -467,6 +467,27 @@ nav .nav-goal { font-weight: 600; }
 }
 
 .comment.agent { border-left-color: var(--accent); }
+
+/* Plan 15 C2: harness/system-authored rows (park/resume bookkeeping,
+   abort/requeue/retry notices, session restorations) render as a
+   distinct SYSTEM event — labelled, muted, no agent accent or icon;
+   never an agent comment row. */
+.comment.system {
+    background: var(--surface-sunken);
+    border-left: 3px dashed var(--line);
+    font-size: 0.82rem;
+}
+.comment.system .system-label {
+    border: 1px solid var(--line);
+    border-radius: 999px;
+    color: var(--ink-muted);
+    font-size: 0.7rem;
+    font-weight: 600;
+    letter-spacing: 0.04em;
+    margin-right: 0.35rem;
+    padding: 0.05rem 0.5rem;
+    text-transform: uppercase;
+}
 .comment blockquote {
     border-left: 3px solid var(--line);
     color: var(--ink-muted);

+ 14 - 5
src/pages/goal_detail.html

@@ -604,21 +604,30 @@
                 </div>
             </section>
 
-            <!-- Comments with selection-quoting (markdown bodies). -->
+            <!-- Comments with selection-quoting (markdown bodies).
+                 Plan 15 C2: the thread IS the goal-run task's own
+                 task_comments — the agent's pushes and the human's
+                 composer land on the same rows; harness-authored
+                 bookkeeping renders as distinct muted system rows,
+                 never agent comment rows. The composer opens with
+                 the run (a draft goal has no thread to inject into). -->
             <section class="card">
                 <h3>Comments</h3>
                 <div stm-for-comment-in="goal.comments"
-                     stm-attribute.class="'comment' + (comment.from_agent ? ' agent' : '')">
-                    <strong stm-text="comment.author">author</strong>
+                     stm-attribute.class="'comment' + (comment.from_agent ? ' agent' : '') + (comment.is_system ? ' system' : '')">
+                    <strong stm-if="!comment.is_system" stm-text="comment.author">author</strong>
+                    <span stm-if="comment.is_system" class="system-label">system</span>
                     <span class="muted" stm-text="comment.created_display">when</span>
                     <div class="md" stm-markdown="comment.body">body</div>
                     <blockquote stm-if="comment.quote.length > 0" stm-text="comment.quote">quote</blockquote>
                 </div>
                 <p stm-if="goal.comments.length == 0" class="muted">No comments yet.</p>
-                <form stm-action="goal.comment">
+                <p stm-if="goal.run_task_id == 0" class="muted">The goal agent has not started
+                    yet — comments open with its run.</p>
+                <form stm-if="goal.run_task_id > 0" stm-action="goal.comment">
                     <div class="form-row"><label for="goal-comment-body">Add comment</label>
                         <textarea id="goal-comment-body" name="body" rows="3"
-                                  placeholder="A comment for the goal agent (markdown)"></textarea></div>
+                                  placeholder="A timeless note for a future reader — status belongs in progress updates (markdown)"></textarea></div>
                     <button type="submit" class="primary">Comment</button>
                 </form>
                 <dialog id="quote-comment-dialog">

+ 6 - 2
src/pages/task_detail.html

@@ -507,8 +507,12 @@
             </details>
 
             <h3>Comments</h3>
-            <div stm-for-comment-in="task.comments" stm-attribute.class="'comment' + (comment.from_agent ? ' agent' : '')">
-                <strong stm-text="comment.author">author</strong>
+            <!-- Plan 15 C2: harness-authored bookkeeping (aborts,
+                 requeues, retries, session restorations) renders as a
+                 distinct muted SYSTEM row — never an agent comment. -->
+            <div stm-for-comment-in="task.comments" stm-attribute.class="'comment' + (comment.from_agent ? ' agent' : '') + (comment.is_system ? ' system' : '')">
+                <strong stm-if="!comment.is_system" stm-text="comment.author">author</strong>
+                <span stm-if="comment.is_system" class="system-label">system</span>
                 <span class="muted" stm-text="comment.when_display">when</span>
                 <div stm-text="comment.body">body</div>
                 <blockquote stm-if="comment.quote.length > 0" stm-text="comment.quote">quote</blockquote>

+ 54 - 16
test/web_stack_test.py

@@ -2599,6 +2599,14 @@ def bundle_dispatch(ctx):
     # up (the fake saddle still carries gpu from the routing checks).
     check(queue.get("waiting_for_saddle") is not True,
           "capable saddle available: no wait flag on the new task")
+    # Drain barrier: the cap task must be CLAIMED before the flaky
+    # arc arms fail_next (under parallel-shard load the dispatch tick
+    # can lag task creation; an unclaimed predecessor would consume
+    # the one-shot failure flag instead of the flaky task).
+    check(wait_for(lambda: task_state(auth, cap_create).get("status")
+                   in ("working", "done", "failed") or False,
+                   timeout=60, what="cap task claimed") is not None,
+          "the capable task is claimed before the flaky arc starts")
 
     print("== retry + resume ==")
     # A daemon-side failure fails the task with its explanation.
@@ -2610,7 +2618,7 @@ def bundle_dispatch(ctx):
     })
     flaky = task_id_tmp
     check(wait_for(lambda: task_state(auth, flaky).get("status") == "failed" or False,
-                   timeout=60, what="flaky task fails") is not None,
+                   timeout=90, what="flaky task fails") is not None,
           "daemon failure fails the task")
     detail = task_state(auth, flaky)
     check("bash -n rejected" in (detail.get("summary") or ""),
@@ -2627,15 +2635,16 @@ def bundle_dispatch(ctx):
     check(any("bash -n rejected" in c["body"] and c["author"] == "agent"
               for c in detail["comments"]),
           "failure explanation kept as an agent comment (author 'agent')")
-    check(any(c["body"] == "Task retried by user" for c in detail["comments"]),
-          "retry lands its audit comment")
+    check(any(c["body"] == "Task retried by user" and c["author"] == "system"
+              and c.get("is_system") for c in detail["comments"]),
+          "retry lands its audit comment as a system row (never an agent row)")
     check(len(detail["comments"]) >= comments_before + 1,
           "retry keeps the comment history")
     pushes_before = len([p for p in fake.start_payloads
                          if int(p.get("task", {}).get("id") or 0) == flaky])
     check(wait_for(lambda: len([p for p in fake.start_payloads
                                 if int(p.get("task", {}).get("id") or 0) == flaky])
-                   > pushes_before or False, timeout=60,
+                   > pushes_before or False, timeout=90,
                    what="flaky task re-dispatched") is not None,
           "retried task dispatches again")
     # The resumed arc surfaces on the next /status read — in the
@@ -2644,11 +2653,13 @@ def bundle_dispatch(ctx):
     check(wait_task_done(auth, flaky, timeout=60) is not None,
           "retried task completes on the restored workspace")
     detail = task_state(auth, flaky)
-    check(any("restored the session context" in p["summary"]
-              for p in detail.get("progress", [])),
+    check(any("restored the session context" in c["body"]
+              and c["author"] == "system" and c.get("is_system")
+              for c in detail.get("comments", [])),
           "daemon resume reported on the first status after the restore")
-    check(len(detail["progress"]) > progress_before,
-          "resume note appended to the progress timeline")
+    check(all("restored the session context" not in p["summary"]
+              for p in detail.get("progress", [])),
+          "the resume note stays OUT of the progress feed (F8: status is not progress)")
 
     # Manual resume: a task stuck working on an offline saddle.
     task_id_tmp = create_queued(auth, {
@@ -2685,8 +2696,9 @@ def bundle_dispatch(ctx):
     check(detail["status"] == "queued",
           "manual resume requeues the stuck task")
     check(any(c["body"] == "Saddle became unreachable; task requeued"
+              and c["author"] == "system" and c.get("is_system")
               for c in detail["comments"]),
-          "manual resume lands the interruption comment")
+          "manual resume lands the interruption comment as a system row")
     check(all(s["status"] == "offline"
               for s in slot_public(auth, "/saddles", "saddles")["saddles"]),
           "saddle stays offline after the manual requeue")
@@ -4587,7 +4599,7 @@ def bundle_answers(ctx):
         timeout=30, what="accept resume allocation") is not None,
           "accept resumes the parked daemon with the approval notice")
     planner.push_results("done", summary="plan accepted and implemented")
-    check(wait_task_done(auth, accept_task, timeout=30) is not None,
+    check(wait_task_done(auth, accept_task, timeout=60) is not None,
           "the accepted plan's task completes")
     planner.set_state("idle")
 
@@ -4756,9 +4768,10 @@ def bundle_answers(ctx):
                    timeout=60, what="daemon death -> offline") is not None,
           "vanishing daemon drives the saddle offline (failed staleness probe)")
     check(wait_for(lambda: any("unreachable" in c["body"]
+                               and c.get("is_system")
                                for c in task_state(auth, vanish).get("comments", []))
                    or False, timeout=30, what="unreachable comment") is not None,
-          "the affected task gets an unreachable agent comment")
+          "the affected task gets an unreachable system row (never an agent comment)")
     check(task_state(auth, vanish)["status"] == "queued",
           "the task requeues after the daemon death")
     card = saddle_card(auth, "fake-saddle")
@@ -5240,6 +5253,12 @@ def bundle_goals(ctx):
     check(state.get("start") is not None and state.get("cancel") is not None
           and state.get("pause") is None,
           "draft offers start + cancel only")
+    check(state["run_task_id"] == 0 and state["comments"] == [],
+          "a draft goal has no thread yet (the run task owns it)")
+    draft_page = http_get_page(auth, "/goals/%d" % goal1)
+    check('stm-if="goal.run_task_id &gt; 0" stm-action="goal.comment"' in draft_page
+          and "comments open with its run" in draft_page,
+          "the draft goal page gates the composer on the run (muted note until then)")
     # Branch-name uniqueness: same name, same involved project → refuse.
     duplicate_directives = act(auth, slot_public(auth, "/goals", "goals")["create"], {
         "project_id": "1", "title": "Clash", "brief": "b", "branch_name": "goal/flange"})
@@ -6239,16 +6258,16 @@ def bundle_replay(ctx):
     conn = db()
     task_comment_bodies = [r[0] for r in conn.execute(
         "select body from task_comments where task_id = ?", (comment_run,))]
-    goal_comment_bodies = [r[0] for r in conn.execute(
-        "select body from goal_comments where goal_id = ?", (comment_goal,))]
     event_texts = [r[0] for r in conn.execute(
         "select text from goal_events where goal_id = ? and kind = 'comment'",
         (comment_goal,))]
+    tables = {r[0] for r in conn.execute(
+        "select name from sqlite_master where type = 'table'").fetchall()}
     conn.close()
     check(task_comment_bodies == [comment_body],
-          "the task comment stores the body, not the command echo")
-    check(goal_comment_bodies == [comment_body],
-          "the goal comment stores the body, not the command echo")
+          "the run task's comment stores the body, not the command echo")
+    check("goal_comments" not in tables,
+          "the goal_comments table is gone (the run task's rows are the thread)")
     check(len(event_texts) == 1 and event_texts[0].startswith("The flange weld holds.")
           and "saddle comment" not in event_texts[0] and "EOF" not in event_texts[0],
           "the goal_events comment projection stores the body text")
@@ -6711,6 +6730,25 @@ def bundle_goalui(ctx):
     check(".goal-event.footnote" in css and ".footnote {" in css,
           "the stylesheet ships the footnote row weight beside the utility")
 
+    print("== C2: system rows render distinctly on the ONE thread ==")
+    run_comments = task_state(auth, run_task)["comments"]
+    system_rows = [c for c in run_comments if c.get("is_system")]
+    check(system_rows and all(c["author"] == "system" for c in system_rows)
+          and not any(c.get("from_agent") for c in system_rows),
+          "the harness bookkeeping rows are authored 'system' (never agent)")
+    goal_thread = goal_state(goal)["comments"]
+    check(any(c.get("is_system") for c in goal_thread),
+          "the goal page reads the same thread (the system rows included)")
+    css = open(os.path.join(REPO, "src/pages/app.css")).read()
+    check(".comment.system" in css and ".system-label" in css,
+          "the stylesheet ships the distinct system-event styling")
+    page = http_get_page(auth, "/goals/%d" % goal)
+    check("comment.is_system" in page and "system-label" in page,
+          "the goal page renders system rows with the labelled muted style")
+    task_page = http_get_page(auth, "/tasks/%d" % worker)
+    check("comment.is_system" in task_page and "system-label" in task_page,
+          "the task page renders system rows with the same style")
+
     print("== C1: one review block per verdict path (F6) ==")
     # The daemon's honest round pushes the ENTRY and the STATE
     # together — the done pair must raise exactly ONE completion