Переглянути джерело

s4: terminal-text single representation — the failure path stops writing the duplicate agent comment and retry stops copying summary→comment (tasks.summary is the terminal cache, the narrative renders summary_lead, the task page renders the card); human comments keep their ONE writer (the web's user-comment row — tests pin the single-row invariant; the daemon-side USER_COMMENT echo suppression is D's item).

clanker 1 місяць тому
батько
коміт
2b251e6521

+ 18 - 16
src/Services/TaskService.vala

@@ -1189,13 +1189,16 @@ namespace ClankerSaddle.Services {
 
         /**
          * Records a saddle-reported failure: status 'failed' with the
-         * daemon's message as summary, the explanation stored again as a
-         * agent comment (so the thread carries it), the open session
-         * closed with outcome 'failed' and its saddle freed. Terminal
-         * transitions project ONCE (plan 13 B2): the live failure's
-         * honest burst — a dozen-plus distinct `saddle failure` entry
-         * ids in one session — still fails the task exactly once, with
-         * one comment and one goal event.
+         * daemon's message as summary, the open session closed with
+         * outcome 'failed' and its saddle freed. Plan 16 S4
+         * (terminal-text single representation): `tasks.summary` IS
+         * the terminal cache — no duplicate agent comment is written
+         * (the narrative renders `summary_lead`; the task page renders
+         * the summary card). Terminal transitions project ONCE (plan
+         * 13 B2): the live failure's honest burst — a dozen-plus
+         * distinct `saddle failure` entry ids in one session — still
+         * fails the task exactly once, with one summary write and one
+         * goal event.
          */
         public async void fail_task(int64 task_id, string? summary) throws Error {
             var task = yield db.query<TaskEntity>()
@@ -1213,7 +1216,6 @@ namespace ClankerSaddle.Services {
             ((!)task).completed_at = new DateTime.now_local();
             if (summary != null && summary.length > 0) {
                 ((!)task).summary = summary;
-                yield add_comment(task_id, "agent", summary);
             } else if (((!)task).summary == null || ((!)task).summary.length == 0) {
                 ((!)task).summary = "Saddle reported a failure";
             }
@@ -1399,11 +1401,14 @@ namespace ClankerSaddle.Services {
         }
 
         /**
-         * Returns a failed task to the queue for another attempt. The
-         * failure summary moves into an agent comment (so the history keeps
-         * it) before being cleared from the task, `completed_at` is
-         * withdrawn, any open session closes and its saddle frees — the
-         * same bookkeeping as {@link requeue_task}, plus the audit
+         * Returns a failed task to the queue for another attempt.
+         * Plan 16 S4: the failure summary is the terminal cache of the
+         * attempt that earned it — retry clears it and copies it
+         * NOWHERE (the narrative rendered its lead, the page rendered
+         * its card; the next attempt writes a fresh one), so no
+         * summary→comment duplicate joins the thread. `completed_at`
+         * is withdrawn, any open session closes and its saddle frees —
+         * the same bookkeeping as {@link requeue_task}, plus the audit
          * comment "Task retried by user".
          */
         public async void retry_task(int64 task_id) throws Error {
@@ -1415,9 +1420,6 @@ namespace ClankerSaddle.Services {
                     @"No task exists with id $(task_id)");
             }
 
-            if (((!)task).summary != null && ((!)task).summary.length > 0) {
-                yield add_comment(task_id, "agent", (!)(((!)task).summary));
-            }
             ((!)task).status = STATUS_QUEUED;
             ((!)task).summary = null;
             ((!)task).completed_at = null;

+ 3 - 2
src/actions/GoalActions.vala

@@ -189,8 +189,9 @@ namespace ClankerSaddle {
      * Re-offers a failed goal's supervision (plan 14 B1, the goal
      * page's parity with the task page's Retry): the goal revives to
      * 'active' and its run task re-queues for another attempt — the
-     * failure summary moves into the run's comment history exactly
-     * like {@link RetryTaskAction}.
+     * failure summary clears with the attempt (plan 16 S4: the
+     * terminal cache is never copied to the thread), exactly like
+     * {@link RetryTaskAction}.
      */
     public class RetryGoalRunAction : SealedGoalAction {
 

+ 4 - 2
src/actions/TaskActions.vala

@@ -439,8 +439,10 @@ namespace ClankerSaddle {
 
     /**
      * Retries a failed task: the requeue path with the failure summary
-     * moved into a comment (and cleared), all history kept. The next
-     * dispatch resumes the daemon-side workspace when it still exists.
+     * cleared (plan 16 S4: the terminal cache dies with its attempt —
+     * nothing derived is copied to the thread), all other history
+     * kept. The next dispatch resumes the daemon-side workspace when
+     * it still exists.
      */
     public class RetryTaskAction : SealedTaskAction {
 

+ 39 - 10
test/web_stack_test.py

@@ -2652,21 +2652,21 @@ def bundle_dispatch(ctx):
     check("bash -n rejected" in (detail.get("summary") or ""),
           "failure summary shows the daemon's explanation")
 
-    # Retry: summary moves into a comment and clears, task requeues,
-    # history stays, and the re-dispatch restores the workspace.
+    # Retry: the terminal summary dies with its attempt (plan 16 S4 —
+    # it is NOT copied to the thread), the task requeues, history
+    # stays, and the re-dispatch restores the workspace.
     comments_before = len(detail.get("comments", []))
     progress_before = len(detail.get("progress", []))
     act(auth, detail["retry"], {})
     detail = task_state(auth, flaky)
     check(detail["status"] == "queued" and (detail.get("summary") or "") == "",
           "retry requeues the task and clears the stale summary")
-    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(not any("bash -n rejected" in c["body"] for c in detail["comments"]),
+          "no summary→comment copy joins the thread (the terminal cache dies with its attempt, S4)")
     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,
+    check(len(detail["comments"]) >= comments_before,
           "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])
@@ -2688,6 +2688,20 @@ def bundle_dispatch(ctx):
     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)")
+    # Plan 16 S4: terminal text has ONE representation — the failed
+    # attempt's summary lived in tasks.summary alone (asserted above:
+    # no agent comment), and a HUMAN comment lands as exactly one
+    # user-authored row (the web is the single writer; the daemon's
+    # terminal echo is raw-log only).
+    act(auth, task_state(auth, flaky)["comment"],
+        {"body": "The retry landed — nice recovery"})
+    flaky_comments = task_state(auth, flaky)["comments"]
+    check(len([c for c in flaky_comments
+               if c["body"] == "The retry landed — nice recovery"]) == 1
+          and all(c["author"] != "agent"
+                  for c in flaky_comments
+                  if c["body"] == "The retry landed — nice recovery"),
+          "a human comment lands as exactly ONE user row (single writer, S4)")
 
     # Manual resume: a task stuck working on an offline saddle.
     task_id_tmp = create_queued(auth, {
@@ -6096,6 +6110,17 @@ def bundle_goals(ctx):
     conn.close()
     check(failure_report == "the foundation crumbled",
           "fail_goal's failure report keeps its goal-side home (S1)")
+    # Plan 16 S4: the narrative renders the task-failed rows from the
+    # terminal summary's lead — the run thread carries NO duplicate
+    # agent comment of the goal failure text.
+    failed_rows = [e for e in state["events"] if e["kind"] == "task-failed"]
+    check(failed_rows
+          and all(": Cancelled by user" in e["text"] for e in failed_rows),
+          "task-failed narrative rows render the summary lead (S4: summary is the terminal cache)")
+    run_comments = [c for c in task_state(auth, run3)["comments"]
+                    if c["author"] == "agent"]
+    check(not any("the foundation crumbled" in c["body"] for c in run_comments),
+          "the goal failure text never duplicates as an agent comment (S4)")
     fake.hold_state = False
 
     print("== plan 12: auto-enqueue-after for human tasks ==")
@@ -6390,8 +6415,10 @@ def bundle_replay(ctx):
     check(status == 200
           and task_state(auth, burst_task)["status"] == "failed",
           "the failure entry + state pair fails the task")
-    check(agent_comments(burst_task, "bash -n rejected the script") == 1,
-          "the entry path and the state path together leave one failure comment")
+    check(agent_comments(burst_task, "bash -n rejected the script") == 0
+          and "bash -n rejected the script"
+          in (task_state(auth, burst_task).get("summary") or ""),
+          "the failure explanation lives ONCE — in tasks.summary, the terminal cache (S4: no duplicate agent comment)")
 
     # The burst: a DISTINCT failure entry id in the same session, plus
     # both ids redelivered under a churned session — nothing re-runs.
@@ -6408,8 +6435,10 @@ def bundle_replay(ctx):
               "the burst push under %s is consumed, not rejected" % (
                   "its session" if target == burst_session else "a churned session"))
     check(task_state(auth, burst_task)["status"] == "failed"
-          and agent_comments(burst_task) == 1,
-          "duplicate failed entries leave one transition and one comment")
+          and agent_comments(burst_task) == 0
+          and "bash -n rejected the script"
+          in (task_state(auth, burst_task).get("summary") or ""),
+          "duplicate failed entries leave one transition, one summary, zero agent comments (S4)")
 
     print("== plan 13 B2: goal failure projects once ==")
     # Goal context: `saddle failure` fails the goal ONCE — the 115