Selaa lähdekoodia

feat: /start gains the parent object for subtask dispatches — id, title, description, plan (clamped ~4000 chars), status; absent for root tasks

clanker 1 kuukausi sitten
vanhempi
sitoutus
04b636e258
2 muutettua tiedostoa jossa 72 lisäystä ja 0 poistoa
  1. 29 0
      src/Services/SaddleService.vala
  2. 43 0
      test/web_stack_test.py

+ 29 - 0
src/Services/SaddleService.vala

@@ -581,6 +581,35 @@ namespace ClankerSaddle.Services {
             }
             payload.end_array();
 
+            // The wire contract's `parent` addition (subtask dispatches
+            // only): the daemon's system prompt renders the lineage —
+            // the subtask agent sees its parent's title, description,
+            // status and plan (clamped, plans run long).
+            if (task.parent_task_id > 0) {
+                var parent_row = yield db.query<TaskEntity>()
+                    .where(expr("id == $0", elem<int64?>(task.parent_task_id)))
+                    .first_async();
+                if (parent_row != null) {
+                    string parent_plan = ((!)parent_row).plan ?? "";
+                    if (parent_plan.length > 4000) {
+                        parent_plan = parent_plan.substring(0, 4000);
+                    }
+                    payload.set_member_name("parent");
+                    payload.begin_object();
+                    payload.set_member_name("id");
+                    payload.add_int_value(((!)parent_row).id);
+                    payload.set_member_name("title");
+                    payload.add_string_value(((!)parent_row).title);
+                    payload.set_member_name("description");
+                    payload.add_string_value(((!)parent_row).description ?? "");
+                    payload.set_member_name("plan");
+                    payload.add_string_value(parent_plan);
+                    payload.set_member_name("status");
+                    payload.add_string_value(((!)parent_row).status);
+                    payload.end_object();
+                }
+            }
+
             // The wire contract's `resume_notice` addition: on a
             // workspace-adopting start (a suspended parent's resume), the
             // daemon injects it as the next user turn instead of the

+ 43 - 0
test/web_stack_test.py

@@ -1671,6 +1671,49 @@ def main():
         check(wait_task_done(auth, offline_task, timeout=60) is not None,
               "requeued task completes again on the healthy daemon")
 
+
+        print("== R5 B1: /start parent context ==")
+        # A fresh subtask dispatch carries the parent object (plan clamped).
+        conn = db()
+        conn.execute("update tasks set plan = ? where id = ?",
+                     ("# Parent plan\n\n" + "step. " * 1200, auto_parent))
+        conn.commit()
+        conn.close()
+        act(auth, slot_public(auth, "/tasks", "tasks")["create"], {
+            "project_id": "1",
+            "title": "Lineage subtask",
+            "priority": "3",
+        })
+        lineage_task = int([t["id"] for t in slot_public(auth, "/tasks", "tasks")["tasks"]
+                            if t["title"] == "Lineage subtask"][0])
+        conn = db()
+        conn.execute("update tasks set parent_task_id = ?, description = 'torque to spec'"
+                     " where id = ?", (auto_parent, lineage_task))
+        conn.commit()
+        conn.close()
+        check(wait_for(lambda: any(int(p.get("task", {}).get("id") or 0) == lineage_task
+                                   for p in fake.start_payloads) or False,
+                       timeout=60, what="lineage subtask dispatches") is not None,
+              "lineage subtask dispatches")
+        lineage_push = [p for p in fake.start_payloads
+                        if int(p.get("task", {}).get("id") or 0) == lineage_task][-1]
+        parent_obj = lineage_push.get("parent")
+        check(parent_obj is not None
+              and int(parent_obj.get("id") or 0) == auto_parent
+              and parent_obj.get("title") == "Auto parent"
+              and parent_obj.get("status") in ("suspended", "working", "done"),
+              "subtask /start carries the parent object")
+        check(parent_obj.get("description") == "torque to spec"
+              or "Parent plan" in (parent_obj.get("plan") or ""),
+              "parent description/plan content flows through")
+        check(len(parent_obj.get("plan") or "") <= 4000,
+              "parent plan clamped to ~4000 chars (%d)"
+              % len(parent_obj.get("plan") or ""))
+        root_pushes = [p for p in fake.start_payloads
+                       if int(p.get("task", {}).get("id") or 0) == offline_task]
+        check(all("parent" not in p for p in root_pushes),
+              "root task /start carries no parent object")
+
     finally:
         # Exit-code diagnostic: distinguishes a crashed app (negative
         # returncode, e.g. -11 SIGSEGV) from a clean shutdown. When