Преглед изворни кода

Plan 17 M4: migrate the harness and bundles onto the ledger

The fake daemon pushes 7.2 event batches (opening round: lifecycle,
turn text, tool call + result, usage; scripted corpora carry their own
kinds); the results channel drops its entries. Bundles assert the
events table, the JSON page and SSE stream, per-session seq
idempotency (redelivery stores nothing, a seq-less batch refuses
atomically), ledger-backed progress feeds, the goal narrative's
event projection, and the state-channel failure idempotency. goalui
migrates off the terminal page (session-row notices, the log page,
the runs-table link) and the yeehaw checks die with the feature.
clanker пре 1 месец
родитељ
комит
4b1ee65655
8 измењених фајлова са 447 додато и 564 уклоњено
  1. 24 12
      test/bundles/allocation.py
  2. 79 98
      test/bundles/dispatch.py
  3. 45 18
      test/bundles/goals.py
  4. 48 67
      test/bundles/goalui.py
  5. 24 23
      test/bundles/lifecycle.py
  6. 74 191
      test/bundles/pages.py
  7. 74 117
      test/bundles/replay.py
  8. 79 38
      test/harness.py

+ 24 - 12
test/bundles/allocation.py

@@ -505,14 +505,13 @@ def run(ctx):
         """Push-era raises on the dashboard fixture task (the poll-era
         """Push-era raises on the dashboard fixture task (the poll-era
         /update channel is gone): a blocked results push carries the
         /update channel is gone): a blocked results push carries the
         block payload; a review verb rides a review entry."""
         block payload; a review verb rides a review entry."""
-        if kind == "review" and summary is not None:
+        if kind == "review":
+            block = {"question": question, "type": "review"}
+            if summary is not None:
+                block["body"] = summary
             return saddle_api("POST", "/api/saddle/results", fake_token, {
             return saddle_api("POST", "/api/saddle/results", fake_token, {
-                "session_id": dash_session, "state": "working",
-                "entries": [{"id": 9317,
-                             "command": "saddle review",
-                             "output": summary, "type": "review",
-                             "timestamp": time.strftime(
-                                 "%Y-%m-%dT%H:%M:%SZ", time.gmtime())}]})[0]
+                "session_id": dash_session, "state": "blocked",
+                "block": block})[0]
         block = {"question": question, "type": kind}
         block = {"question": question, "type": kind}
         if body is not None:
         if body is not None:
             block["body"] = body
             block["body"] = body
@@ -531,10 +530,17 @@ def run(ctx):
     conn.execute("insert into task_comments (task_id, author, body, created_at)"
     conn.execute("insert into task_comments (task_id, author, body, created_at)"
                  " values (?, 'user:admin', 'timeline fixture comment by a human',"
                  " values (?, 'user:admin', 'timeline fixture comment by a human',"
                  " strftime('%s','now','-5 seconds'))", (cap_task,))
                  " strftime('%s','now','-5 seconds'))", (cap_task,))
-    conn.execute("insert into task_progress (task_id, summary, created_at)"
-                 " values (?, 'timeline fixture progress',"
-                 " strftime('%s','now','-5 seconds'))",
-                 (cap_task,))
+    conn.execute(
+        "insert into events (kind, actor, scope_type, scope_id, payload,"
+        " created_at) values ('comment', 'user', 'task', ?,"
+        " ?, strftime('%s','now','-5 seconds'))",
+        (cap_task, json.dumps({"comment_id": 0, "author": "user:admin",
+                               "body": "timeline fixture comment by a human"})))
+    conn.execute(
+        "insert into events (kind, actor, scope_type, scope_id, payload,"
+        " created_at) values ('progress', 'model', 'task', ?,"
+        " ?, strftime('%s','now','-5 seconds'))",
+        (cap_task, json.dumps({"text": "timeline fixture progress"})))
     conn.commit()
     conn.commit()
     conn.close()
     conn.close()
     # The probe RFC raised last: the newest pinned card and the newest
     # The probe RFC raised last: the newest pinned card and the newest
@@ -663,7 +669,7 @@ def run(ctx):
     conn.execute("insert into sessions (task_id, saddle_id, started_at, ended_at,"
     conn.execute("insert into sessions (task_id, saddle_id, started_at, ended_at,"
                  " outcome) values (?, ?, strftime('%s','now'),"
                  " outcome) values (?, ?, strftime('%s','now'),"
                  " strftime('%s','now'), 'parked')",
                  " strftime('%s','now'), 'parked')",
-                 (plain_parked, fake_id))
+                 (plain_parked, racer2_card_id))
     conn.commit()
     conn.commit()
     conn.close()
     conn.close()
     task_page = http_get_page(auth, "/tasks/%d" % plain_parked)
     task_page = http_get_page(auth, "/tasks/%d" % plain_parked)
@@ -677,6 +683,10 @@ def run(ctx):
     detail = task_state(auth, plain_parked)
     detail = task_state(auth, plain_parked)
     check(detail.get("force_wake") is not None,
     check(detail.get("force_wake") is not None,
           "the task state exposes the offered force-wake action")
           "the task state exposes the offered force-wake action")
+    # Determinism: the wake's trigger-ask (and any idle-probe ask) must
+    # not race the MANUAL claim the section asserts on.
+    fake2.ask_on_wake = False
+    fake2.ask_on_idle_status = False
     act(auth, detail["force_wake"], {})
     act(auth, detail["force_wake"], {})
     status, resume_payload = fake2.ask_now()
     status, resume_payload = fake2.ask_now()
     check(status == 200 and int(resume_payload["task"]["id"]) == plain_parked,
     check(status == 200 and int(resume_payload["task"]["id"]) == plain_parked,
@@ -691,6 +701,8 @@ def run(ctx):
     conn.close()
     conn.close()
     check(flag_row is not None and flag_row[0] in (0, False),
     check(flag_row is not None and flag_row[0] in (0, False),
           "the claim consumed the task force-wake flag")
           "the claim consumed the task force-wake flag")
+    fake2.ask_on_wake = True
+    fake2.ask_on_idle_status = True
     # The woken run re-parks (fake daemon finishes then suspends? no —
     # The woken run re-parks (fake daemon finishes then suspends? no —
     # fake.start_payloads script: the plain task runs to done via the
     # fake.start_payloads script: the plain task runs to done via the
     # daemon's own finish path in later suites; here just let it be
     # daemon's own finish path in later suites; here just let it be

+ 79 - 98
test/bundles/dispatch.py

@@ -242,7 +242,7 @@ def run(ctx):
           "project_notes char cap: head kept, giant truncated (entries=%d, chars=%d)"
           "project_notes char cap: head kept, giant truncated (entries=%d, chars=%d)"
           % (len(notes), total))
           % (len(notes), total))
 
 
-    print("== phase 2: pushed chunks -> raw log + terminal view ==")
+    print("== phase 2: pushed events -> the unified ledger ==")
     # Wait for the last dispatch to finish so its pushes landed.
     # Wait for the last dispatch to finish so its pushes landed.
     check(wait_task_done(auth, task_id_of("Char cap dispatch"), timeout=60) is not None,
     check(wait_task_done(auth, task_id_of("Char cap dispatch"), timeout=60) is not None,
           "char-cap task completes")
           "char-cap task completes")
@@ -250,129 +250,110 @@ def run(ctx):
     raw_session = conn.execute(
     raw_session = conn.execute(
         "select s.id from sessions s join tasks t on t.id = s.task_id"
         "select s.id from sessions s join tasks t on t.id = s.task_id"
         " where t.title = 'Char cap dispatch' order by s.id desc limit 1").fetchone()[0]
         " where t.title = 'Char cap dispatch' order by s.id desc limit 1").fetchone()[0]
-    rows = conn.execute("select seq, chunk from session_raw_log where session_id = ?"
-                        " order by seq", (raw_session,)).fetchall()
+    rows = conn.execute(
+        "select seq, kind from events where session_id = ? order by seq",
+        (raw_session,)).fetchall()
     conn.close()
     conn.close()
-    check(len(rows) >= 3, "session_raw_log captured the pushed chunks (%d)" % len(rows))
+    check(len(rows) >= 5,
+          "the ledger captured the round's event batch (%d)" % len(rows))
+    kinds = [r[1] for r in rows]
+    check(all(k in kinds for k in
+              ("lifecycle", "turn_text", "tool_call", "tool_result", "usage")),
+          "the opening batch carries the §7.2 kinds (%s)" % sorted(set(kinds)))
     seqs = [r[0] for r in rows]
     seqs = [r[0] for r in rows]
     check(seqs == sorted(seqs) and len(set(seqs)) == len(seqs),
     check(seqs == sorted(seqs) and len(set(seqs)) == len(seqs),
-          "pushed chunks stored once, seq strictly increasing")
-    text = "".join(r[1] for r in rows)
-    check(fake.PROMPT in text and "echo hello" in text and "hello" in text,
-          "pushed raw log keeps the prompt and echoed output byte-exact")
-    check("working-heartbeat" in text,
-          "the flush-timer heartbeat chunks keep arriving mid-round")
-
-    terminal = slot_public(auth, "/tasks/%d/terminal" % task_id_of("Char cap dispatch"),
-                           "terminal")
-    check(terminal.get("has_raw_log") is True and int(terminal["session_id"]) == raw_session,
-          "terminal view selects raw rendering for the raw session")
-
-    # Old sessions (typed entries, no raw rows) keep the fallback view
-    # (a dedicated fixture task: its only session is the legacy one,
-    # so the terminal view cannot prefer a dispatched raw session).
-    conn = db()
-    conn.execute("insert into tasks (project_id, title, status, priority, created_at)"
-                 " values (1, 'Legacy terminal task', 'working', 5, strftime('%s','now'))")
-    legacy_task = conn.execute("select max(id) from tasks").fetchone()[0]
-    conn.execute("insert into sessions (task_id, saddle_id, started_at, ended_at, outcome)"
-                 " values (?, ?, strftime('%s','now'), strftime('%s','now'), 'done')",
-                 (legacy_task, fake_id))
-    conn.commit()
-    legacy_session = conn.execute("select max(id) from sessions").fetchone()[0]
-    conn.execute("insert into session_commands (session_id, command_text, output_text,"
-                 " type, started_at) values (?, 'ls', 'old-output', 'command',"
-                 " strftime('%s','now'))", (legacy_session,))
-    conn.commit()
-    conn.close()
-    legacy = slot_public(auth, "/tasks/%d/terminal" % legacy_task, "terminal")
-    check(legacy.get("has_raw_log") is False,
-          "legacy session falls back to the entry-type terminal view")
-
-    request = urllib.request.Request(WEB + "/terminal-stream/%d" % raw_session)
-    with urllib.request.urlopen(request, timeout=15) as stream:
-        raw_events = []
-        deadline = time.monotonic() + 8
-        while time.monotonic() < deadline and len(raw_events) < 3:
-            line = stream.readline().decode().strip()
-            if not line.startswith("data: ") or line == "data: ping":
-                continue
-            event = json.loads(line[6:])
-            if event.get("type") == "raw":
-                raw_events.append(event)
-        check(len(raw_events) >= 3 and raw_events[0]["text"] == fake.PROMPT
-              and raw_events[0]["seq"] < raw_events[1]["seq"],
-              "terminal stream serves pushed chunks in seq order (prompt first)")
+          "stored events keep one strictly increasing seq per session")
+
+    # The plan-17 event log wire: one newest-first JSON page per task.
+    cap_task = task_id_of("Char cap dispatch")
+    status, page = saddle_api(
+        "GET", "/tasks/%d/events?limit=30" % cap_task, None)
+    check(status == 200 and page.get("ok") is True
+          and len(page.get("events", [])) >= 5,
+          "GET /tasks/<id>/events serves the newest-first page")
+    page_events = page["events"]
+    ids = [e["id"] for e in page_events]
+    check(ids == sorted(ids, reverse=True),
+          "the JSON page lists events newest first")
+    check(page_events[0]["kind"] == "state_change"
+          and page_events[0]["payload"].get("to") == "done",
+          "the newest event is the terminal state change")
+
+    # The SSE stream seeds the most-recent window then stays open.
+    request = urllib.request.Request(WEB + "/tasks/%d/events/stream" % cap_task)
+    stream = urllib.request.urlopen(request, timeout=15)
+    stream_events = []
+    deadline = time.monotonic() + 6
+    while time.monotonic() < deadline and len(stream_events) < 30:
+        line = stream.readline().decode().strip()
+        if not line.startswith("data: ") or line == "data: ping":
+            continue
+        event = json.loads(line[6:])
+        stream_events.append(event)
+    stream.close()
+    check(len(stream_events) >= 5,
+          "the event stream seeds the most-recent window (%d)" % len(stream_events))
+    stream_ids = [e["id"] for e in stream_events]
+    check(stream_ids == sorted(stream_ids) and len(set(stream_ids)) == len(stream_ids),
+          "the seed batch is oldest first inside the window")
 
 
     print("== phase 2: chunks/results ingestion — auth, idempotency ==")
     print("== phase 2: chunks/results ingestion — auth, idempotency ==")
     # Token auth on both push channels (plan 10: the per-saddle
     # Token auth on both push channels (plan 10: the per-saddle
     # bearer authorises every daemon->web call).
     # bearer authorises every daemon->web call).
     status, _ = saddle_api("POST", "/api/saddle/chunks", None,
     status, _ = saddle_api("POST", "/api/saddle/chunks", None,
-                           {"session_id": raw_session, "chunks": []})
+                           {"session_id": raw_session, "events": []})
     check(status == 401, "chunks endpoint rejects a missing token")
     check(status == 401, "chunks endpoint rejects a missing token")
     status, _ = saddle_api("POST", "/api/saddle/results", "saddle-bogus-token",
     status, _ = saddle_api("POST", "/api/saddle/results", "saddle-bogus-token",
                            {"session_id": raw_session, "state": "working"})
                            {"session_id": raw_session, "state": "working"})
     check(status == 401, "results endpoint rejects a wrong token")
     check(status == 401, "results endpoint rejects a wrong token")
     status, _ = saddle_api("POST", "/api/saddle/chunks", fake_token,
     status, _ = saddle_api("POST", "/api/saddle/chunks", fake_token,
-                           {"session_id": 99999, "chunks": []})
+                           {"session_id": 99999, "events": []})
     check(status == 400, "chunks for an unknown session rejected")
     check(status == 400, "chunks for an unknown session rejected")
     status, _ = saddle_api("POST", "/api/saddle/results", fake_token,
     status, _ = saddle_api("POST", "/api/saddle/results", fake_token,
                            {"session_id": raw_session, "state": "done"})
                            {"session_id": raw_session, "state": "done"})
     check(status == 200, "the owning saddle's session accepts its results")
     check(status == 200, "the owning saddle's session accepts its results")
 
 
-    # Pushed chunk idempotency: re-pushing a batch (the outbox's
+    # Pushed event idempotency: re-pushing a batch (the outbox's
     # at-least-once redelivery after a lost response) consumes the
     # at-least-once redelivery after a lost response) consumes the
     # duplicates and stores only the tail.
     # duplicates and stores only the tail.
-    conn = db()
-    before = conn.execute("select count(*) from session_raw_log where session_id = ?",
-                          (raw_session,)).fetchone()[0]
-    conn.close()
-    redelivery = [{"seq": r[0], "text": r[1]} for r in rows[:3]]
-    status, body = saddle_api("POST", "/api/saddle/chunks", fake_token,
-                              {"session_id": raw_session, "chunks": redelivery})
-    check(status == 200 and body.get("ok") is True and int(body.get("stored") or 0) == 0,
-          "a fully-duplicate batch stores nothing (duplicates consumed)")
-    fresh = [{"seq": 9000, "text": "late-flush\r\n"},
-             {"seq": 9001, "text": ""}]  # truly empty text: no row
-    status, body = saddle_api("POST", "/api/saddle/chunks", fake_token,
-                              {"session_id": raw_session, "chunks": fresh})
-    check(status == 200 and int(body.get("stored") or 0) == 1,
-          "a partial-redelivery batch stores only the new chunks")
-    conn = db()
-    after = conn.execute("select count(*) from session_raw_log where session_id = ?",
-                         (raw_session,)).fetchone()[0]
-    empty_row = conn.execute(
-        "select count(*) from session_raw_log where session_id = ? and seq = 9001",
-        (raw_session,)).fetchone()[0]
-    conn.close()
-    check(after == before + 1 and empty_row == 0,
-          "redelivered chunks never duplicate rows; empty text stores nothing")
-
-    # Pushed entries: today's OutputEntry shape with the daemon's
-    # global entry ids — a redelivered results push stores its
-    # entries exactly once.
-    def command_rows(session):
+    def session_event_count(session):
         conn = db()
         conn = db()
         try:
         try:
             return conn.execute(
             return conn.execute(
-                "select command_text, type from session_commands"
-                " where session_id = ? order by id", (session,)).fetchall()
+                "select count(*) from events where session_id = ?",
+                (session,)).fetchone()[0]
         finally:
         finally:
             conn.close()
             conn.close()
 
 
-    entries = [{"id": 8101, "command": "sed -n 1p README.md", "output": "# demo",
-                "exit_code": 0, "type": "command", "timestamp": "2026-09-06T00:00:00Z"}]
-    status, _ = saddle_api("POST", "/api/saddle/results", fake_token,
-                           {"session_id": raw_session, "state": "working",
-                            "entries": entries})
-    check(status == 200, "entries push accepted")
-    status, _ = saddle_api("POST", "/api/saddle/results", fake_token,
-                           {"session_id": raw_session, "state": "working",
-                            "entries": entries})
-    check(status == 200, "redelivered entries push accepted (at-least-once)")
-    stored = command_rows(raw_session)
-    check(len([r for r in stored if r[0] == "sed -n 1p README.md"]) == 1,
-          "redelivered entries store exactly once (daemon-global dedup)")
+    before = session_event_count(raw_session)
+    redelivery = [{"seq": r[0], "kind": r[1]} for r in rows[:3]]
+    status, body = saddle_api("POST", "/api/saddle/chunks", fake_token,
+                              {"session_id": raw_session, "events": redelivery})
+    check(status == 200 and body.get("ok") is True and int(body.get("stored") or 0) == 0,
+          "a fully-duplicate batch stores nothing (duplicates consumed)")
+    fresh = [{"seq": 9000, "kind": "progress",
+              "payload": {"text": "late-flush\r\n"}},
+             {"seq": 9001, "kind": "progress", "payload": {}}]
+    status, body = saddle_api("POST", "/api/saddle/chunks", fake_token,
+                              {"session_id": raw_session, "events": fresh})
+    check(status == 200 and int(body.get("stored") or 0) == 2,
+          "a partial-redelivery batch stores only the new events")
+    check(session_event_count(raw_session) == before + 2,
+          "redelivered events never duplicate rows")
+    status, body = saddle_api("POST", "/api/saddle/chunks", fake_token,
+                              {"session_id": raw_session,
+                               "events": [{"kind": "progress", "payload": {}}]})
+    check(status == 400, "an event without a positive seq is refused")
+
+    # A seq-less batch shape is the daemon contract violation the web
+    # rejects wholesale (nothing of the batch stores).
+    count_before = session_event_count(raw_session)
+    status, _ = saddle_api("POST", "/api/saddle/chunks", fake_token,
+                           {"session_id": raw_session,
+                            "events": [{"seq": 9100, "kind": "progress"},
+                                       {"kind": "usage", "payload": {}}]})
+    check(status == 400 and session_event_count(raw_session) == count_before,
+          "a batch with a seq-less event is refused atomically")
 
 
     # The retired daemon routes 404 (phase 2 deleted /output, /log
     # The retired daemon routes 404 (phase 2 deleted /output, /log
     # and /start serving) — and the web app never calls them.
     # and /start serving) — and the web app never calls them.

+ 45 - 18
test/bundles/goals.py

@@ -423,13 +423,17 @@ def run(ctx):
         "select max(id) from sessions where task_id = ?",
         "select max(id) from sessions where task_id = ?",
         (run_task,)).fetchone()[0]
         (run_task,)).fetchone()[0]
     conn.execute(
     conn.execute(
-        "insert into task_progress (task_id, summary, created_at)"
-        " values (?, 'Overseeing the flange fan-out', strftime('%s','now'))",
-        (run_task,))
+        "insert into events (session_id, seq, kind, actor, scope_type, scope_id,"
+        " payload, created_at) values (?, 8001, 'progress', 'model', 'task', ?,"
+        " ?, strftime('%s','now'))",
+        (open_run_session, run_task, json.dumps({"text": "Overseeing the flange fan-out"})))
     conn.execute(
     conn.execute(
-        "insert into session_commands (session_id, command_text, type, started_at)"
-        " values (?, 'git -C demo log --oneline -5', 'command',"
-        " strftime('%s','now'))", (open_run_session,))
+        "insert into events (session_id, seq, kind, actor, scope_type, scope_id,"
+        " payload, created_at) values (?, 8002, 'tool_call', 'model', 'task', ?,"
+        " ?, strftime('%s','now'))",
+        (open_run_session, run_task,
+         json.dumps({"tool": "run_command",
+                     "args": {"command": "git -C demo log --oneline -5"}})))
     conn.commit()
     conn.commit()
     conn.close()
     conn.close()
     # A manual refresh records the daemon's health snapshot (the
     # A manual refresh records the daemon's health snapshot (the
@@ -439,15 +443,15 @@ def run(ctx):
     check(state["progress"] and state["progress"][0]["summary"]
     check(state["progress"] and state["progress"][0]["summary"]
           == "Overseeing the flange fan-out",
           == "Overseeing the flange fan-out",
           "the goal page carries the agent's live progress feed")
           "the goal page carries the agent's live progress feed")
-    check(state["current_command"] == "git -C demo log --oneline -5"
+    check(state["current_command"] == "run_command: git -C demo log --oneline -5"
           and state["current_command_running"] is True,
           and state["current_command_running"] is True,
           "the current activity shows the open round's running command")
           "the current activity shows the open round's running command")
     check(state["agent_saddle"] == "fake-saddle"
     check(state["agent_saddle"] == "fake-saddle"
           and "fake-saddle" in state["saddle_health"],
           and "fake-saddle" in state["saddle_health"],
           "the assigned agent saddle and its health display")
           "the assigned agent saddle and its health display")
     check(state["runs"] and state["runs"][0]["saddle_name"] == "fake-saddle"
     check(state["runs"] and state["runs"][0]["saddle_name"] == "fake-saddle"
-          and "session_id=" in state["runs"][0]["terminal_url"],
-          "the run table carries host saddles and deep-linked terminals")
+          and state["runs"][0]["terminal_url"] == "/tasks/%d/log" % run_task,
+          "the run table carries host saddles and the event log link")
     goal_page = http_get_page(auth, "/goals/%d" % goal1)
     goal_page = http_get_page(auth, "/goals/%d" % goal1)
     check('stm-markdown="goal.brief"' in goal_page
     check('stm-markdown="goal.brief"' in goal_page
           and 'stm-markdown="goal.plan"' in goal_page
           and 'stm-markdown="goal.plan"' in goal_page
@@ -1023,20 +1027,43 @@ def run(ctx):
     print("== goal 4 t1: force wake — the human resume for a parked agent ==")
     print("== goal 4 t1: force wake — the human resume for a parked agent ==")
     # The goal-2 stall class: the parked run has NO pending wake (a
     # The goal-2 stall class: the parked run has NO pending wake (a
     # draft task never enqueued → no terminal transition → parked
     # draft task never enqueued → no terminal transition → parked
-    # forever). Force wake is the human recovery lever.
+    # forever). Force wake is the human recovery lever. A fresh goal
+    # owns the scenario (goal1's run completed with its goal).
+    stall_goal = create_goal({"project_id": "1", "title": "Stall goal",
+                              "brief": "Parks with a never-enqueued draft.",
+                              "branch_name": "goal/stall"})
+    fake.goal_script = [("plan", "# Stall plan\n\n1. Wait for the stall")]
+    act(auth, goal_state(stall_goal)["start"], {})
+    check(wait_for(lambda: goal_state(stall_goal)["status"] == "plan_review" or False,
+                   timeout=60, what="stall plan enters review") is not None,
+          "the stall goal reaches plan review")
+    act(auth, goal_state(stall_goal)["plan_pending_block"]["answer_action"],
+        {"verdict": "accept"})
+    check(wait_for(lambda: goal_state(stall_goal)["status"] == "active" or False,
+                   timeout=30, what="stall goal activates") is not None,
+          "the stall goal activates")
     fake.goal_script = [("suspend",)]
     fake.goal_script = [("suspend",)]
+    conn = db()
+    run_task = conn.execute(
+        "select id from tasks where goal_id = ? and kind = 'goal_run'",
+        (stall_goal,)).fetchone()[0]
+    conn.close()
     check(wait_for(lambda: task_state(auth, run_task)["status"] == "suspended"
     check(wait_for(lambda: task_state(auth, run_task)["status"] == "suspended"
                    or False, timeout=60, what="agent parks for force wake") is not None,
                    or False, timeout=60, what="agent parks for force wake") is not None,
           "the goal agent parks before the force-wake scenario")
           "the goal agent parks before the force-wake scenario")
     # The stall fixture: an agent-created draft that was never enqueued
     # The stall fixture: an agent-created draft that was never enqueued
     # (typo'd enqueue) — statuses must name it as NOT enqueued.
     # (typo'd enqueue) — statuses must name it as NOT enqueued.
+    conn = db()
+    agent_session = conn.execute(
+        "select max(id) from sessions where task_id = ?", (run_task,)).fetchone()[0]
+    conn.close()
     status, body = saddle_api("POST", "/api/saddle/task/create", fake_token, {
     status, body = saddle_api("POST", "/api/saddle/task/create", fake_token, {
         "session_id": agent_session, "title": "QA sweep",
         "session_id": agent_session, "title": "QA sweep",
         "mandate": "Run the QA sweep.", "mode": "test"})
         "mandate": "Run the QA sweep.", "mode": "test"})
     check(status == 200 and body.get("ok") is True,
     check(status == 200 and body.get("ok") is True,
           "the stall fixture draft task creates (%s)" % body.get("error"))
           "the stall fixture draft task creates (%s)" % body.get("error"))
     stall_draft = int(body["task_id"])
     stall_draft = int(body["task_id"])
-    goal_page = http_get_page(auth, "/goals/%d" % goal1)
+    goal_page = http_get_page(auth, "/goals/%d" % stall_goal)
     check('stm-action="goal.force_wake"' in goal_page,
     check('stm-action="goal.force_wake"' in goal_page,
           "the goal page authors the Force wake button while the run is parked")
           "the goal page authors the Force wake button while the run is parked")
     check('stm-if="goal.force_wake != null"' in goal_page,
     check('stm-if="goal.force_wake != null"' in goal_page,
@@ -1047,7 +1074,7 @@ def run(ctx):
                        lambda: len(fake.goal_assignments) == parked_runs,
                        lambda: len(fake.goal_assignments) == parked_runs,
                        "no press, no wake"),
                        "no press, no wake"),
           "without a press the parked agent never wakes")
           "without a press the parked agent never wakes")
-    state = goal_state(goal1)
+    state = goal_state(stall_goal)
     check(state.get("force_wake") is not None,
     check(state.get("force_wake") is not None,
           "the goal state exposes the offered force-wake action")
           "the goal state exposes the offered force-wake action")
     act(auth, state["force_wake"], {})
     act(auth, state["force_wake"], {})
@@ -1073,32 +1100,32 @@ def run(ctx):
                        "consumed flag never re-fires"),
                        "consumed flag never re-fires"),
           "the consumed flag does not re-wake (one wake per press)")
           "the consumed flag does not re-wake (one wake per press)")
     # Guards: a paused goal refuses the wake (Resume is its lever).
     # Guards: a paused goal refuses the wake (Resume is its lever).
-    state = goal_state(goal1)
+    state = goal_state(stall_goal)
     act(auth, state["pause"], {})
     act(auth, state["pause"], {})
     conn = db()
     conn = db()
     paused_flag = conn.execute("select force_wake from goals where id = ?",
     paused_flag = conn.execute("select force_wake from goals where id = ?",
-                               (goal1,)).fetchone()
+                               (stall_goal,)).fetchone()
     conn.close()
     conn.close()
     check(paused_flag is not None and paused_flag[0] in (0, False),
     check(paused_flag is not None and paused_flag[0] in (0, False),
           "pausing does not set the flag")
           "pausing does not set the flag")
-    state = goal_state(goal1)
+    state = goal_state(stall_goal)
     check(state.get("force_wake") is None,
     check(state.get("force_wake") is None,
           "a paused goal does not author the Force wake action")
           "a paused goal does not author the Force wake action")
     act(auth, state["resume"], {})
     act(auth, state["resume"], {})
-    check(wait_for(lambda: goal_state(goal1)["status"] == "active" or False,
+    check(wait_for(lambda: goal_state(stall_goal)["status"] == "active" or False,
                    timeout=30, what="goal reactivates") is not None,
                    timeout=30, what="goal reactivates") is not None,
           "the goal reactivates for the guard checks")
           "the goal reactivates for the guard checks")
     # Non-suspended run refuses: run is parked again (script parked it
     # Non-suspended run refuses: run is parked again (script parked it
     # above), wake it, let it park, then a second press while parked is
     # above), wake it, let it park, then a second press while parked is
     # fine — the 400 path needs a NON-suspended run. Start there: wake
     # fine — the 400 path needs a NON-suspended run. Start there: wake
     # once more and press while it is working.
     # once more and press while it is working.
-    state = goal_state(goal1)
+    state = goal_state(stall_goal)
     act(auth, state["force_wake"], {})
     act(auth, state["force_wake"], {})
     check(wait_for(lambda: len(fake.goal_assignments) > woke_runs or False,
     check(wait_for(lambda: len(fake.goal_assignments) > woke_runs or False,
                    timeout=30, what="second wake fires") is not None,
                    timeout=30, what="second wake fires") is not None,
           "a second press wakes again (re-press is allowed)")
           "a second press wakes again (re-press is allowed)")
     # While it is mid-script (not yet parked), the action is hidden.
     # While it is mid-script (not yet parked), the action is hidden.
-    check(goal_state(goal1).get("force_wake") is None
+    check(goal_state(stall_goal).get("force_wake") is None
           or task_state(auth, run_task)["status"] != "suspended",
           or task_state(auth, run_task)["status"] != "suspended",
           "the wake action hides while the run is not parked")
           "the wake action hides while the run is not parked")
     check(wait_for(lambda: task_state(auth, run_task)["status"] == "suspended"
     check(wait_for(lambda: task_state(auth, run_task)["status"] == "suspended"

+ 48 - 67
test/bundles/goalui.py

@@ -9,7 +9,8 @@ def run(ctx):
     B1 — goal-run control parity (Retry supervision / Requeue-to-resume
     B1 — goal-run control parity (Retry supervision / Requeue-to-resume
     on the status head, the /tasks/<goal-run-id> redirect, no
     on the status head, the /tasks/<goal-run-id> redirect, no
     notify-me toggle), B2 — the plan-review remarks queue, B4 —
     notify-me toggle), B2 — the plan-review remarks queue, B4 —
-    sessions.resume_notice + the terminal dropdown/resume panel, B5 —
+    sessions.resume_notice on the task page's session rows + the
+    plan-17 event log page, B5 —
     page order, effective rules and per-task capability badges, B6 —
     page order, effective rules and per-task capability badges, B6 —
     narrative footnote weights. C1 — the single-row block world: the
     narrative footnote weights. C1 — the single-row block world: the
     goal's blocks ARE the run task's rows (answered from every
     goal's blocks ARE the run task's rows (answered from every
@@ -106,8 +107,8 @@ def run(ctx):
     check("Retry supervision" in goal_page and "Requeue to resume" in goal_page,
     check("Retry supervision" in goal_page and "Requeue to resume" in goal_page,
           "the status head ships the run-control parity buttons")
           "the status head ships the run-control parity buttons")
     check("status-corner" not in goal_page and "goal complete" not in goal_page
     check("status-corner" not in goal_page and "goal complete" not in goal_page
-          and goal_page.count("badge-yeehaw") == 1,
-          "yeehaw is rows-only (no goal-head corner badge; the row badge remains)")
+          and "badge-yeehaw" not in goal_page,
+          "no yeehaw badge remains anywhere (the feature died with the entry stream)")
 
 
     check(goal_state(goal)["current_activity"] == "parked awaiting the plan review",
     check(goal_state(goal)["current_activity"] == "parked awaiting the plan review",
           "the head line reads 'parked awaiting the plan review' while one is open")
           "the head line reads 'parked awaiting the plan review' while one is open")
@@ -261,20 +262,15 @@ def run(ctx):
     check(not goal_side_tables,
     check(not goal_side_tables,
           "no goal-side store exists — historical stragglers are impossible")
           "no goal-side store exists — historical stragglers are impossible")
 
 
-    print("== B4: the terminal session dropdown + resume-info panel ==")
+    print("== B4: resume notices on the session rows + the event log page ==")
     fake.goal_script = [("suspend",)]
     fake.goal_script = [("suspend",)]
     check(wait_for(lambda: task_state(auth, run_task)["status"] == "suspended"
     check(wait_for(lambda: task_state(auth, run_task)["status"] == "suspended"
                    or False, timeout=60, what="agent parks suspended") is not None,
                    or False, timeout=60, what="agent parks suspended") is not None,
           "the agent parks suspended for the requeue scenario")
           "the agent parks suspended for the requeue scenario")
-    terminal = slot_public(auth, "/tasks/%d/terminal" % run_task, "terminal")
-    ids = [int(s["id"]) for s in terminal["sessions"]]
+    sessions = task_state(auth, run_task)["sessions"]
+    ids = [int(s["id"]) for s in sessions]
     check(ids == sorted(ids, reverse=True),
     check(ids == sorted(ids, reverse=True),
-          "the session dropdown lists newest-first")
-    check(int(terminal["session_id"]) == ids[0],
-          "the terminal still defaults to the latest session")
-    labels = {int(s["id"]): s["label"] for s in terminal["sessions"]}
-    check(all(label.startswith("Session #") for label in labels.values()),
-          "dropdown labels read 'Session #N — started <time>'")
+          "the task page's session history lists newest-first")
     # Recompute at the check point: the C1 answers above each
     # Recompute at the check point: the C1 answers above each
     # resumed the agent too, so the earlier snapshot would go stale.
     # resumed the agent too, so the earlier snapshot would go stale.
     conn = db()
     conn = db()
@@ -282,26 +278,21 @@ def run(ctx):
         "select id, resume_notice from sessions where task_id = ?",
         "select id, resume_notice from sessions where task_id = ?",
         (run_task,)).fetchall() if text]
         (run_task,)).fetchall() if text]
     conn.close()
     conn.close()
-    resumed_markers = [i for i in ids if "[resumed]" in labels[i]]
-    check(set(resumed_markers) == set(resumed_ids),
-          "the [resumed] marker rides exactly the notice-carrying sessions")
-    notices_by_id = {int(s["id"]): s.get("notice") or "" for s in terminal["sessions"]}
-    check("Opening brief" in notices_by_id[min(ids)]
-          and "Drive the plan-14 goal surface" in notices_by_id[min(ids)],
-          "session 1's panel shows the opening-brief framing (from the goal brief)")
-    check(any("Plan feedback received" in (notices_by_id[i] or "") for i in resumed_ids),
-          "a resumed session's panel shows the stored resume notice")
-    terminal_page = http_get_page(auth, "/tasks/%d/terminal" % run_task)
-    check('id="session-select"' in terminal_page
-          and "window.terminalPickSession" in terminal_page,
-          "the session picker is an options dropdown")
-    check('stm-markdown="window.terminalSessionNotice(terminal)"' in terminal_page,
-          "the panel renders the selected session's notice as markdown")
-    check("No opening brief or resume notice recorded" in terminal_page,
-          "the panel ships its graceful empty state")
-    css = open(os.path.join(REPO, "src/pages/app.css")).read()
-    check("resume-notice-panel" in css and "max-height: 200px" in css,
-          "the panel is a 200px vertical-scroll strip (stylesheet)")
+    notices_by_id = {int(s["id"]): s.get("notice") or "" for s in sessions}
+    check(set(i for i in ids if notices_by_id[i]) == set(resumed_ids),
+          "the session rows' notice column rides exactly the notice-carrying sessions")
+    check(any("Plan feedback received" in notices_by_id[i] for i in resumed_ids),
+          "a resumed session's row shows the stored resume notice")
+    log_state = slot_public(auth, "/tasks/%d/log" % run_task, "task_log")
+    check(log_state.get("exists") is True and int(log_state["task_id"]) == run_task
+          and log_state["task_url"] == "/tasks/%d" % run_task,
+          "the event log page hydrates the run-task header")
+    log_page = http_get_page(auth, "/tasks/%d/log" % run_task)
+    check("EventSource" in log_page and "events/stream" in log_page
+          and "data-log-feed" in log_page,
+          "the log page subscribes to the ledger's SSE stream")
+    check(goal_state(goal)["runs"][0]["terminal_url"] == "/tasks/%d/log" % run_task,
+          "the goal runs table links the event log")
 
 
     print("== B1: requeue-to-resume from the goal head ==")
     print("== B1: requeue-to-resume from the goal head ==")
     conn = db()
     conn = db()
@@ -490,12 +481,12 @@ def run(ctx):
           "the task rows render capability badges")
           "the task rows render capability badges")
 
 
     print("== B6: narrative weights — events full, comments footnote ==")
     print("== B6: narrative weights — events full, comments footnote ==")
-    # An AGENT comment projects the narrative's comment event (human
-    # comments only thread) — pushed through the entries channel while
+    # An AGENT remark rides the ledger as a comment event (human
+    # comments only thread) — pushed through the events channel while
     # parked, so the suspended state stays put.
     # parked, so the suspended state stays put.
-    fake.push_results("suspended", entries=[{
-        "id": 9001, "command": "saddle comment", "output": "An agent aside.",
-        "exit_code": 0, "type": "comment", "timestamp": "2026-09-12T00:00:00Z"}])
+    fake.push_events([{"kind": "comment", "actor": "model",
+                       "payload": {"author": "agent",
+                                   "body": "An agent aside."}}])
     check(wait_for(lambda: any(e["kind"] == "comment"
     check(wait_for(lambda: any(e["kind"] == "comment"
                                for e in goal_state(goal)["events"]) or False,
                                for e in goal_state(goal)["events"]) or False,
                    timeout=15, what="comment event lands") is not None,
                    timeout=15, what="comment event lands") is not None,
@@ -531,31 +522,24 @@ def run(ctx):
           "the task page renders system rows with the same style")
           "the task page renders system rows with the same style")
 
 
     print("== S5 U2: mechanical rows filtered from user-facing timelines ==")
     print("== S5 U2: mechanical rows filtered from user-facing timelines ==")
-    # The daemon's mechanical system entries (the park notice, the
-    # restore footnote) ride the wire typed 'system': the raw terminal
-    # row STORES (session_commands), but no comment row, narrative
-    # event or dashboard timeline row is authored for them.
+    # The daemon's mechanical system remarks (the park notice, the
+    # restore footnote) ride the ledger as harness comment events: the
+    # event row STORES, but no comment row, narrative event or
+    # dashboard timeline row renders for them.
     open_run = open_session(run_task)
     open_run = open_session(run_task)
     mechanical = [
     mechanical = [
-        {"id": 9501, "command": "saddle suspend",
-         "output": "goal agent parked until an owned task completes or"
-                   " fails; resume via /start resume_notice",
-         "exit_code": 0, "type": "system", "timestamp": "2026-09-12T00:00:00Z"},
-        {"id": 9502, "command": "harness resume",
-         "output": "[harness] session context restored — task resumed",
-         "exit_code": 0, "type": "system", "timestamp": "2026-09-12T00:00:01Z"},
+        {"seq": 9501, "kind": "comment", "actor": "harness",
+         "payload": {"author": "system",
+                     "body": "goal agent parked until an owned task"
+                             " completes or fails"}},
+        {"seq": 9502, "kind": "comment", "actor": "harness",
+         "payload": {"author": "system",
+                     "body": "[harness] session context restored — task resumed"}},
     ]
     ]
-    status, _ = saddle_api("POST", "/api/saddle/results", fake_token,
-                           {"session_id": open_run, "state": "suspended",
-                            "entries": mechanical})
-    check(status == 200, "the mechanical system entries push cleanly")
-    conn = db()
-    raw_rows = conn.execute(
-        "select count(*) from session_commands where session_id = ?"
-        " and type = 'system'", (open_run,)).fetchone()[0]
-    conn.close()
-    check(raw_rows >= 2,
-          "the raw terminal keeps the mechanical rows (typed entries store)")
+    status, body = saddle_api("POST", "/api/saddle/chunks", fake_token,
+                              {"session_id": open_run, "events": mechanical})
+    check(status == 200 and int(body.get("stored") or 0) == 2,
+          "the mechanical harness remarks store on the ledger")
     check(all("parked until" not in c["body"]
     check(all("parked until" not in c["body"]
               and "session context restored" not in c["body"]
               and "session context restored" not in c["body"]
               for c in task_state(auth, run_task)["comments"]),
               for c in task_state(auth, run_task)["comments"]),
@@ -619,23 +603,20 @@ def run(ctx):
     check("Accept goal" in goal_page and "Reject with feedback" in goal_page
     check("Accept goal" in goal_page and "Reject with feedback" in goal_page
           and 'name="feedback"' in goal_page,
           and 'name="feedback"' in goal_page,
           "the completion review presents explicit accept/reject + free-text feedback (U1)")
           "the completion review presents explicit accept/reject + free-text feedback (U1)")
-    # The burst shape: the done ENTRY and the done STATE in one push
-    # (fresh entry ids), then the whole pair redelivered — the
-    # pending-report idempotency leaves the one row alone.
+    # The burst shape: the done STATE redelivered under the run's
+    # newest session (the entries channel is gone — the report rides
+    # the summary) — the pending-report idempotency leaves the one
+    # row alone.
     conn = db()
     conn = db()
     burst_session = conn.execute(
     burst_session = conn.execute(
         "select max(id) from sessions where task_id = ?",
         "select max(id) from sessions where task_id = ?",
         (run_task,)).fetchone()[0]
         (run_task,)).fetchone()[0]
     conn.close()
     conn.close()
-    done_pair = [{"id": 9401, "command": "saddle done",
-                  "output": "# Completion\n\nOne block only.", "exit_code": 0,
-                  "type": "done", "timestamp": "2026-09-12T00:00:00Z"}]
     status, _ = saddle_api("POST", "/api/saddle/results", fake_token,
     status, _ = saddle_api("POST", "/api/saddle/results", fake_token,
                            {"session_id": burst_session, "state": "done",
                            {"session_id": burst_session, "state": "done",
-                            "entries": done_pair,
                             "summary": "# Completion\n\nOne block only."})
                             "summary": "# Completion\n\nOne block only."})
     check(status == 200 and completion_block_count() == 1,
     check(status == 200 and completion_block_count() == 1,
-          "the done entry + state pair raises nothing more (one block, ever)")
+          "a redelivered done push raises nothing more (one block, ever)")
     state = goal_state(goal)
     state = goal_state(goal)
     act(auth, state["completion_pending_block"]["answer_action"],
     act(auth, state["completion_pending_block"]["answer_action"],
         {"verdict": "accept"})
         {"verdict": "accept"})

+ 24 - 23
test/bundles/lifecycle.py

@@ -378,9 +378,9 @@ def run(ctx):
     check(wait_task_done(auth, during_fence, timeout=60) is not None,
     check(wait_task_done(auth, during_fence, timeout=60) is not None,
           "queued work dispatches once the saddle proves it is alive")
           "queued work dispatches once the saddle proves it is alive")
 
 
-    print("== R7 B3: health panel + pushed raw log from the first round ==")
-    # A normally dispatched task pushes its chunks immediately: raw
-    # rows exist from the first round (the session-4 gap stays
+    print("== R7 B3: health panel + pushed events from the first round ==")
+    # A normally dispatched task pushes its event batch immediately:
+    # ledger rows exist from the first round (the session-4 gap stays
     # fixed without any probe machinery).
     # fixed without any probe machinery).
     task_id_tmp = create_queued(auth, {
     task_id_tmp = create_queued(auth, {
         "project_id": "1",
         "project_id": "1",
@@ -389,18 +389,18 @@ def run(ctx):
     })
     })
     raw_first = task_id_tmp
     raw_first = task_id_tmp
 
 
-    def raw_count(task_id):
+    def event_count(task_id):
         conn = db()
         conn = db()
         try:
         try:
             return conn.execute(
             return conn.execute(
-                "select count(*) from session_raw_log where session_id in"
+                "select count(*) from events where session_id in"
                 " (select id from sessions where task_id = ?)", (task_id,)).fetchone()[0]
                 " (select id from sessions where task_id = ?)", (task_id,)).fetchone()[0]
         finally:
         finally:
             conn.close()
             conn.close()
 
 
-    check(wait_for(lambda: raw_count(raw_first) > 0 or False,
-                   timeout=60, what="raw rows from first push") is not None,
-          "pushed chunks land from the first round (session-4 gap)")
+    check(wait_for(lambda: event_count(raw_first) > 0 or False,
+                   timeout=60, what="event rows from first push") is not None,
+          "pushed events land from the first round (session-4 gap)")
     check(wait_task_done(auth, raw_first, timeout=60) is not None,
     check(wait_task_done(auth, raw_first, timeout=60) is not None,
           "raw-first task completes")
           "raw-first task completes")
 
 
@@ -421,12 +421,14 @@ def run(ctx):
     # redelivery of the whole batch (the outbox's at-least-once
     # redelivery of the whole batch (the outbox's at-least-once
     # retry) consumes the duplicates.
     # retry) consumes the duplicates.
     corpus = [
     corpus = [
-        {"seq": 1, "text": "\x1b[01;32muser@host\x1b[00m:\x1b[01;34m~/task-1\x1b[00m$ "},
-        {"seq": 2, "text": "echo hello\r\n"},
-        {"seq": 3, "text": "\r\n"},
-        {"seq": 4, "text": "hello\r\n"},
-        {"seq": 5, "text": "\x1b[01;32muser@host\x1b[00m:\x1b[01;34m~/task-1\x1b[00m$ "},
-        {"seq": 6, "text": "done\r\n"},
+        {"seq": 1, "kind": "command_output",
+         "payload": {"text": "\x1b[01;32muser@host\x1b[00m:\x1b[01;34m~/task-1\x1b[00m$ "}},
+        {"seq": 2, "kind": "command_output", "payload": {"text": "echo hello\r\n"}},
+        {"seq": 3, "kind": "command_output", "payload": {"text": "\r\n"}},
+        {"seq": 4, "kind": "command_output", "payload": {"text": "hello\r\n"}},
+        {"seq": 5, "kind": "command_output",
+         "payload": {"text": "\x1b[01;32muser@host\x1b[00m:\x1b[01;34m~/task-1\x1b[00m$ "}},
+        {"seq": 6, "kind": "command_output", "payload": {"text": "done\r\n"}},
     ]
     ]
     fake.scripted_corpus = corpus
     fake.scripted_corpus = corpus
     fake.hold_state = True
     fake.hold_state = True
@@ -441,9 +443,9 @@ def run(ctx):
         conn = db()
         conn = db()
         try:
         try:
             return [r[0] for r in conn.execute(
             return [r[0] for r in conn.execute(
-                "select seq from session_raw_log where session_id in"
+                "select seq from events where session_id in"
                 " (select id from sessions where task_id = ?)"
                 " (select id from sessions where task_id = ?)"
-                " order by seq", (task_id,))]
+                " and seq is not null order by seq", (task_id,))]
         finally:
         finally:
             conn.close()
             conn.close()
 
 
@@ -452,16 +454,15 @@ def run(ctx):
           "lossless task dispatches and pushes its opening flush")
           "lossless task dispatches and pushes its opening flush")
     check(wait_for(lambda: stored_seqs(lossless) == [1, 2, 3, 4, 5, 6] or False,
     check(wait_for(lambda: stored_seqs(lossless) == [1, 2, 3, 4, 5, 6] or False,
                    timeout=30, what="all six chunks stored") is not None,
                    timeout=30, what="all six chunks stored") is not None,
-          "every pushed chunk lands in session_raw_log, CRLF-only included (%s)"
+          "every pushed event lands in the ledger, CRLF-only included (%s)"
           % stored_seqs(lossless))
           % stored_seqs(lossless))
     status, _ = saddle_api("POST", "/api/saddle/chunks", fake.token,
     status, _ = saddle_api("POST", "/api/saddle/chunks", fake.token,
                            {"session_id": fake.session_id,
                            {"session_id": fake.session_id,
-                            "chunks": [dict(chunk) for chunk in corpus]})
+                            "events": [dict(chunk) for chunk in corpus]})
     check(status == 200 and stored_seqs(lossless) == [1, 2, 3, 4, 5, 6],
     check(status == 200 and stored_seqs(lossless) == [1, 2, 3, 4, 5, 6],
           "a full-batch redelivery stores nothing new (exactly-once)")
           "a full-batch redelivery stores nothing new (exactly-once)")
     request = urllib.request.Request(
     request = urllib.request.Request(
-        WEB + "/terminal-stream/%d" % [r[0] for r in db().execute(
-            "select id from sessions where task_id = ?", (lossless,))][0])
+        WEB + "/tasks/%d/events/stream" % lossless)
     with urllib.request.urlopen(request, timeout=15) as stream:
     with urllib.request.urlopen(request, timeout=15) as stream:
         texts = []
         texts = []
         deadline = time.monotonic() + 8
         deadline = time.monotonic() + 8
@@ -470,10 +471,10 @@ def run(ctx):
             if not line.startswith("data: ") or line == "data: ping":
             if not line.startswith("data: ") or line == "data: ping":
                 continue
                 continue
             event = json.loads(line[6:])
             event = json.loads(line[6:])
-            if event.get("type") == "raw":
-                texts.append(event.get("text") or "")
+            if event.get("kind") == "command_output":
+                texts.append(event.get("payload", {}).get("text") or "")
         check("\r\n" in texts and len(texts) == 6,
         check("\r\n" in texts and len(texts) == 6,
-              "SSE serves the whitespace chunk unfiltered")
+              "SSE serves the whitespace payload unfiltered")
     fake.hold_state = False
     fake.hold_state = False
     fake.scripted_corpus = None
     fake.scripted_corpus = None
     check(wait_task_done(auth, lossless, timeout=60) is not None,
     check(wait_task_done(auth, lossless, timeout=60) is not None,

+ 74 - 191
test/bundles/pages.py

@@ -65,9 +65,9 @@ def run(ctx):
           "task page carries the quote-comment dialog the popover opens")
           "task page carries the quote-comment dialog the popover opens")
     check('name="quote"' in page and 'name="return_to"' in page,
     check('name="quote"' in page and 'name="return_to"' in page,
           "quote dialog posts the quote and return-to fields")
           "quote dialog posts the quote and return-to fields")
-    status, page = http("GET", "/tasks/%d/terminal" % plan_task, timeout=15)
-    check(status == 200 and "quote-comment-dialog" in page,
-          "terminal page keeps its quote dialog")
+    status, page = http("GET", "/tasks/%d/log" % plan_task, timeout=15)
+    check(status == 200 and "data-log-feed" in page and "data-log-more" in page,
+          "the event log page renders the feed and the load-more control")
 
 
     print("== B3: review section hides the duplicate body ==")
     print("== B3: review section hides the duplicate body ==")
     status, _ = raise_block(session_id, "Review the diff", kind="review",
     status, _ = raise_block(session_id, "Review the diff", kind="review",
@@ -252,45 +252,46 @@ def run(ctx):
     print("== rejected entries (non-fatal) ==")
     print("== rejected entries (non-fatal) ==")
     # The push wire: a rejected entry rides a round-results push, stores
     # The push wire: a rejected entry rides a round-results push, stores
     # on the session and streams through SSE; the task is untouched.
     # on the session and streams through SSE; the task is untouched.
-    status, _ = saddle_api("POST", "/api/saddle/results", fake_token, {
-        "session_id": session_id, "state": "working",
-        "entries": [{
-            "id": 9021, "command": "bash -n script.sh",
-            "output": "syntax error near line 3 (recoverable)",
-            "exit_code": 1, "type": "rejected",
-            "timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
-        }]})
-    check(status == 200, "a rejected entry rides the results push")
-    request = urllib.request.Request(WEB + "/terminal-stream/%d" % session_id)
+    status, body = saddle_api("POST", "/api/saddle/chunks", fake_token, {
+        "session_id": session_id,
+        "events": [{"seq": 9021, "kind": "tool_result", "actor": "harness",
+                    "payload": {"command": "bash -n script.sh",
+                                "stderr": "syntax error near line 3 (recoverable)",
+                                "exit_code": 1}}]})
+    check(status == 200 and int(body.get("stored") or 0) == 1,
+          "a failed tool result rides the events push")
+    request = urllib.request.Request(WEB + "/tasks/%d/events/stream" % plan_task)
     with urllib.request.urlopen(request, timeout=15) as stream:
     with urllib.request.urlopen(request, timeout=15) as stream:
-        rejected_event = None
+        result_event = None
         deadline = time.monotonic() + 8
         deadline = time.monotonic() + 8
-        while time.monotonic() < deadline and rejected_event is None:
+        while time.monotonic() < deadline and result_event is None:
             line = stream.readline().decode().strip()
             line = stream.readline().decode().strip()
             if not line.startswith("data: ") or line == "data: ping":
             if not line.startswith("data: ") or line == "data: ping":
                 continue
                 continue
             event = json.loads(line[6:])
             event = json.loads(line[6:])
-            if event.get("type") == "rejected":
-                rejected_event = event
-        check(rejected_event is not None
-              and "syntax error near line 3" in (rejected_event.get("output") or ""),
-              "terminal stream carries rejected entries with their reason")
-
-    # The page template renders them with the distinct non-fatal style.
-    status, page = http("GET", "/tasks/%d/terminal" % plan_task, timeout=15)
-    check(status == 200 and "term-rejected" in page,
-          "terminal page renders rejected entries in their own style")
-
-    print("== B8: ANSI SGR in the terminal view ==")
-    # Byte-exact SGR delivery: the daemon's coloured chunks flow through
-    # the store + SSE untouched (the client renderer styles them).
+            if (event.get("kind") == "tool_result"
+                    and "syntax error near line 3" in
+                    (event.get("payload", {}).get("stderr") or "")):
+                result_event = event
+        check(result_event is not None,
+              "the event stream carries tool results with their reason")
+
+    # The page template renders the feed and the filters.
+    status, page = http("GET", "/tasks/%d/log" % plan_task, timeout=15)
+    check(status == 200 and "data-log-filter" in page and "data-log-search" in page,
+          "the event log page renders filters and search")
+
+    print("== B8: byte-exact payload delivery over the event stream ==")
+    # Byte-exact delivery: ANSI-coloured turn text flows through the
+    # ledger + SSE untouched (the client renderer styles them).
     coloured = "\x1b[1;32mbold green\x1b[0m then \x1b[38;5;196m256-red\x1b[0m\n"
     coloured = "\x1b[1;32mbold green\x1b[0m then \x1b[38;5;196m256-red\x1b[0m\n"
-    conn = db()
-    conn.execute("insert into session_raw_log (session_id, seq, chunk)"
-                 " values (?, 900, ?)", (session_id, coloured))
-    conn.commit()
-    conn.close()
-    request = urllib.request.Request(WEB + "/terminal-stream/%d" % session_id)
+    status, body = saddle_api("POST", "/api/saddle/chunks", fake_token, {
+        "session_id": session_id,
+        "events": [{"seq": 9100, "kind": "turn_text",
+                    "payload": {"turn": 9, "text": coloured}}]})
+    check(status == 200 and int(body.get("stored") or 0) == 1,
+          "the coloured turn text stores as one event")
+    request = urllib.request.Request(WEB + "/tasks/%d/events/stream" % plan_task)
     with urllib.request.urlopen(request, timeout=15) as stream:
     with urllib.request.urlopen(request, timeout=15) as stream:
         coloured_event = None
         coloured_event = None
         deadline = time.monotonic() + 8
         deadline = time.monotonic() + 8
@@ -299,14 +300,12 @@ def run(ctx):
             if not line.startswith("data: ") or line == "data: ping":
             if not line.startswith("data: ") or line == "data: ping":
                 continue
                 continue
             event = json.loads(line[6:])
             event = json.loads(line[6:])
-            if event.get("type") == "raw" and "\x1b[1;32m" in (event.get("text") or ""):
+            if (event.get("kind") == "turn_text"
+                    and "\x1b[1;32m" in (event.get("payload", {}).get("text") or "")):
                 coloured_event = event
                 coloured_event = event
         check(coloured_event is not None
         check(coloured_event is not None
-              and "\x1b[38;5;196m" in coloured_event["text"],
-              "SGR sequences stream byte-exact to the client renderer")
-    status, page = http("GET", "/tasks/%d/terminal" % plan_task, timeout=15)
-    check(status == 200 and "makeAnsiRenderer" in page and "colour256" in page,
-          "terminal page embeds the ANSI renderer (SGR + 256-colour palette)")
+              and "\x1b[38;5;196m" in coloured_event["payload"]["text"],
+              "payload bytes stream byte-exact to the client renderer")
 
 
     # Dispatch path: a rejected entry in a round's results push flows
     # Dispatch path: a rejected entry in a round's results push flows
     # into the audit trail without failing (or finishing) the task.
     # into the audit trail without failing (or finishing) the task.
@@ -323,13 +322,10 @@ def run(ctx):
                       "max_tokens exceeded (recoverable)")
                       "max_tokens exceeded (recoverable)")
     check(wait_task_done(auth, rejected_task, timeout=60) is not None,
     check(wait_task_done(auth, rejected_task, timeout=60) is not None,
           "task completes normally past rejected entries")
           "task completes normally past rejected entries")
-    conn = db()
-    stored = conn.execute(
-        "select count(*) from session_commands sc join sessions s on s.id = sc.session_id"
-        " where s.task_id = ? and sc.type = 'rejected'", (rejected_task,)).fetchone()[0]
-    conn.close()
-    check(stored >= 1,
-          "rejected entries stored in the audit trail (%d rows)" % stored)
+    stored = [row for row in event_rows(rejected_task, ["command_output"])
+              if "max_tokens exceeded" in row[1]]
+    check(len(stored) >= 1,
+          "command_output events stored in the audit trail (%d rows)" % len(stored))
     check(task_state(auth, rejected_task)["status"] == "done"
     check(task_state(auth, rejected_task)["status"] == "done"
           and "fake daemon finished" in (task_state(auth, rejected_task).get("summary") or ""),
           and "fake daemon finished" in (task_state(auth, rejected_task).get("summary") or ""),
           "rejected entries never fail the task")
           "rejected entries never fail the task")
@@ -363,12 +359,13 @@ def run(ctx):
     layout_task = conn.execute("select max(id) from tasks").fetchone()[0]
     layout_task = conn.execute("select max(id) from tasks").fetchone()[0]
     # backdated so the dashboard-timeline fixtures below stay the
     # backdated so the dashboard-timeline fixtures below stay the
     # newest events
     # newest events
-    conn.execute("insert into task_progress (task_id, summary, created_at)"
-                 " values (?, 'first update', strftime('%s','now','-90 seconds'))",
-                 (layout_task,))
-    conn.execute("insert into task_progress (task_id, summary, created_at)"
-                 " values (?, 'second update', strftime('%s','now','-89 seconds'))",
-                 (layout_task,))
+    for offset, line in ((90, "first update"), (89, "second update")):
+        payload = json.dumps({"text": line})
+        conn.execute(
+            "insert into events (kind, actor, scope_type, scope_id, payload,"
+            " created_at) values ('progress', 'model', 'task', ?, ?,"
+            " strftime('%s','now', ?))",
+            (layout_task, payload, "-%d seconds" % offset))
     conn.commit()
     conn.commit()
     conn.close()
     conn.close()
     status, history_page = http("GET", "/tasks/%d" % layout_task, timeout=15)
     status, history_page = http("GET", "/tasks/%d" % layout_task, timeout=15)
@@ -411,6 +408,21 @@ def run(ctx):
                  " strftime('%s','now'), strftime('%s','now'),"
                  " strftime('%s','now'), strftime('%s','now'),"
                  " strftime('%s','now'))")
                  " strftime('%s','now'))")
     report_empty = conn.execute("select max(id) from tasks").fetchone()[0]
     report_empty = conn.execute("select max(id) from tasks").fetchone()[0]
+    # The terminal transitions ride the ledger (state_change events
+    # carrying the summary the timeline snippets).
+    for task_id, to, summary in (
+            (report_done, "done", report_md),
+            (report_failed, "failed", "Boom: tests failed\n\nsecond line carries the detail"),
+            (report_empty, "done", "")):
+        payload = {"from": "working", "to": to,
+                   "text": "Task completed" if to == "done" else "Task failed"}
+        if summary:
+            payload["summary"] = summary
+        conn.execute(
+            "insert into events (kind, actor, scope_type, scope_id, payload,"
+            " created_at) values ('state_change', 'system', 'task', ?, ?,"
+            " strftime('%s','now'))",
+            (task_id, json.dumps(payload)))
     conn.commit()
     conn.commit()
     conn.close()
     conn.close()
 
 
@@ -544,130 +556,6 @@ def run(ctx):
           "the create expander offers the mode + auto-enqueue-after selects"
           "the create expander offers the mode + auto-enqueue-after selects"
           " (no subtask checkboxes)")
           " (no subtask checkboxes)")
 
 
-    print("== yeehaw: ingestion, festive card, badge, timeline ==")
-    # `saddle yeehaw <message>` arrives as a round-results entry with the
-    # message in the output field: it stores like any entry type, earns
-    # the task its 🤠 badge (derived, nothing new stored), renders as the
-    # task page's festive card (last 3, newest first, "+N more") and
-    # surfaces on the dashboard timeline as its own event kind.
-    conn = db()
-    conn.execute("insert into tasks (project_id, title, status, priority, created_at)"
-                 " values (1, 'Yeehaw task', 'working', 5, strftime('%s','now'))")
-    yeehaw_task = conn.execute("select max(id) from tasks").fetchone()[0]
-    conn.execute("insert into sessions (task_id, saddle_id, started_at)"
-                 " values (?, ?, strftime('%s','now'))", (yeehaw_task, fake_id))
-    yeehaw_session = conn.execute("select max(id) from sessions").fetchone()[0]
-    conn.commit()
-    conn.close()
-    now_epoch = int(time.time())
-
-    def push_yeehaw(entry_id, message, offset=0):
-        """One celebration riding a round-results push, timestamped
-        `offset` seconds from now (distinct epochs keep the timeline's
-        newest-first ordering observable)."""
-        return saddle_api("POST", "/api/saddle/results", fake_token, {
-            "session_id": yeehaw_session, "state": "working",
-            "entries": [{
-                "id": entry_id, "command": "saddle yeehaw", "output": message,
-                "exit_code": 0, "type": "yeehaw",
-                "timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ",
-                                           time.gmtime(now_epoch + offset)),
-            }]})[0]
-
-    def yeehaw_rows():
-        conn = db()
-        try:
-            return conn.execute(
-                "select count(*) from session_commands where session_id = ?"
-                " and type = 'yeehaw'", (yeehaw_session,)).fetchone()[0]
-        finally:
-            conn.close()
-
-    check(push_yeehaw(9101, "Shipped the flange") == 200,
-          "yeehaw entries ingest through the results push like any entry type")
-    check(yeehaw_rows() == 1,
-          "the yeehaw entry stores as a session_commands row (message in output)")
-    check(push_yeehaw(9101, "Shipped the flange") == 200 and yeehaw_rows() == 1,
-          "a redelivered yeehaw push is idempotent on the daemon entry id")
-    check(task_state(auth, yeehaw_task)["status"] == "working",
-          "a yeehaw entry never derails the task state")
-
-    push_yeehaw(9102, "Second hoedown", 1)
-    push_yeehaw(9103, "Third hoedown\nsecond line carries the detail", 2)
-    push_yeehaw(9104, "Fourth hoedown", 3)
-    push_yeehaw(9105, "Fifth hoedown", 4)
-    detail = task_state(auth, yeehaw_task)
-    check(detail.get("yeehaw") is True and detail.get("yeehaw_total") == 5,
-          "the detail state flags the badge presence and the uncapped total")
-    check([y["message"] for y in detail.get("yeehaws", [])]
-          == ["Fifth hoedown", "Fourth hoedown",
-              "Third hoedown\nsecond line carries the detail"],
-          "the festive card keeps the last 3 yeehaws, newest first (full messages)")
-    check(detail.get("yeehaw_more") == 2,
-          "the '+N more' hint counts the capped tail")
-    status, page = http("GET", "/tasks/%d" % yeehaw_task, timeout=15)
-    # The template compiler HTML-escapes the emoji (&#129312; = 🤠).
-    check(status == 200 and "yeehaw-card" in page and "&#129312; Yeehaw!" in page,
-          "the task page renders the festive yeehaw card")
-    check(page.index("<h3>Progress</h3>") < page.index("yeehaw-card")
-          < page.index("status-head"),
-          "the card sits between the progress feed and the status grid")
-    check('stm-text="task.yeehaw_more"' in page and "more yeehaw" in page,
-          "the card carries the +N more hint")
-    check("badge-yeehaw" in page and "status-corner" in page,
-          "the status card corner carries the earned 🤠 badge")
-    plain_detail = task_state(auth, layout_task)
-    check(plain_detail.get("yeehaw") is False and plain_detail.get("yeehaw_total") == 0
-          and plain_detail.get("yeehaws", []) == [],
-          "tasks without yeehaw entries carry no card state")
-    status, plain_page = http("GET", "/tasks/%d" % layout_task, timeout=15)
-    check(status == 200 and 'section class="card yeehaw-card" stm-if="task.yeehaw"' in plain_page,
-          "the card template only renders under state gating (stm-if task.yeehaw)")
-
-    queue_rows = slot_public(auth, "/tasks", "tasks")["tasks"]
-    check([t for t in queue_rows if int(t["id"]) == yeehaw_task][0].get("has_yeehaw") is True,
-          "queue rows badge yeehaw tasks")
-    check([t for t in queue_rows if int(t["id"]) == layout_task][0].get("has_yeehaw") is False,
-          "queue rows leave yeehaw-less tasks bare")
-    status, queue_page = http("GET", "/tasks", timeout=15)
-    check(status == 200 and 'stm-if="task.has_yeehaw"' in queue_page,
-          "the queue template renders the row badge")
-
-    dash = slot_public(auth, "/", "dashboard")
-    yeehaw_events = [e for e in dash.get("activity", []) if e["kind"] == "yeehaw"]
-    check(len(yeehaw_events) == 5
-          and all(int(e["task_id"]) == yeehaw_task for e in yeehaw_events),
-          "every yeehaw entry surfaces as its own timeline event kind")
-    check(yeehaw_events[0]["text"] == "🤖 yeehaw'd: Fifth hoedown",
-          "the newest yeehaw event leads the timeline rows")
-    check(all("second line" not in e["text"] for e in yeehaw_events),
-          "multiline yeehaw messages snippet to their first line")
-    referenced = [e for e in dash.get("activity", []) if int(e["task_id"]) == yeehaw_task]
-    check(len(referenced) > 0 and all(e.get("task_yeehaw") is True for e in referenced),
-          "dashboard timeline task references carry the badge flag")
-    plain_refs = [e for e in dash.get("activity", []) if int(e["task_id"]) == layout_task]
-    check(len(plain_refs) > 0 and all(e.get("task_yeehaw") is False for e in plain_refs),
-          "references to yeehaw-less tasks stay bare")
-    status, home_page = http("GET", "/", timeout=15)
-    check(status == 200 and 'stm-if="item.task_yeehaw"' in home_page
-          and "badge-yeehaw" in home_page,
-          "the dashboard renders the badge on timeline task references")
-
-    # Close the yeehaw fixture's round (a plain done push under its
-    # session): under the derived view (item 23 — busy iff an open
-    # session on an assigned/working task) the fixture round would
-    # hold the fake's saddle busy forever; the goal section below
-    # needs it free.
-    status, _ = saddle_api("POST", "/api/saddle/results", fake_token,
-                           {"session_id": yeehaw_session, "state": "done",
-                            "summary": "yeehaw fixture round closed"})
-    check(wait_task_done(auth, yeehaw_task, timeout=30) is not None,
-          "the yeehaw fixture round closes (the derived busy view frees the fake)")
-
-    # (The old R10 scenario — branch_advanced riding /poll verdicts —
-    # died with the poll channel; the answers bundle pins the hint on
-    # its surviving surface, the resumed assignment payload.)
-
     print("== plan 12: the web surfaces — pile, timeline, queue, task page ==")
     print("== plan 12: the web surfaces — pile, timeline, queue, task page ==")
     # A goal parked in plan review feeds the unified dashboard pile;
     # A goal parked in plan review feeds the unified dashboard pile;
     # goal events feed the timeline.
     # goal events feed the timeline.
@@ -749,19 +637,15 @@ def run(ctx):
     task_page_html = http_get_page(auth, "/tasks/%d" % dash_run)
     task_page_html = http_get_page(auth, "/tasks/%d" % dash_run)
     check("Mandate" in task_page_html and 'stm-markdown="task.description"' in task_page_html,
     check("Mandate" in task_page_html and 'stm-markdown="task.description"' in task_page_html,
           "the task page frames the description as the mandate")
           "the task page frames the description as the mandate")
-    check(re.search(r"'/terminal\?session_id=' \+ session\.id\"\s+target=\"_blank\"",
+    check(re.search(r"'/tasks/' \+ task\.id \+ '/log'\"\s+target=\"_blank\"",
                     task_page_html) is not None,
                     task_page_html) is not None,
-          "the task page's session terminal links open in a new tab")
-    terminal_state = slot_public(auth, "/tasks/%d/terminal" % dash_run, "terminal")
-    session_rows = terminal_state["sessions"]
-    check(len(session_rows) >= 2
-          and all(("raw" in s) for s in session_rows),
-          "the terminal state lists every goal-run session with per-session raw flags")
-    check(int(terminal_state["session_id"]) == int(session_rows[0]["id"]),
-          "the terminal default selects the LATEST session (rows are newest-first)")
-    terminal_html = http_get_page(auth, "/tasks/%d/terminal" % dash_run)
-    check("requestedSessionId" in terminal_html and "data-session-id" in terminal_html,
-          "the terminal client owns the ?session_id= selection (query params never reach entrypoints)")
+          "the task page's event log links open in a new tab")
+    log_state = slot_public(auth, "/tasks/%d/log" % dash_run, "task_log")
+    check(log_state.get("exists") is True and int(log_state["task_id"]) == dash_run,
+          "the event log page state names the run task")
+    log_html = http_get_page(auth, "/tasks/%d/log" % dash_run)
+    check("EventSource" in log_html and "events/stream" in log_html,
+          "the log page subscribes to the task's SSE stream")
     run_detail = task_state(auth, dash_run)
     run_detail = task_state(auth, dash_run)
     check(run_detail.get("is_goal_run") is True and run_detail.get("mode") == "orchestrate",
     check(run_detail.get("is_goal_run") is True and run_detail.get("mode") == "orchestrate",
           "the goal agent's own run task identifies itself on its page")
           "the goal agent's own run task identifies itself on its page")
@@ -831,7 +715,6 @@ def run(ctx):
           and queue_page.index("New task") < queue_page.index("<table"),
           and queue_page.index("New task") < queue_page.index("<table"),
           "the New task expander sits above the tasks table")
           "the New task expander sits above the tasks table")
     # Comments/blocks threads: newest first since the sweep. The
     # Comments/blocks threads: newest first since the sweep. The
-    # yeehaw task above carries session comments; assert the detail's
     # comment list leads with the most recently added one.
     # comment list leads with the most recently added one.
     conn = db()
     conn = db()
     conn.execute(
     conn.execute(

+ 74 - 117
test/bundles/replay.py

@@ -4,13 +4,10 @@ from harness import *  # noqa: F401,F403 — the shared wire helpers
 
 
 
 
 # =============================================================================
 # =============================================================================
-# Bundle: replay — plan 13 B1/B2: the daemon-global entry dedup and the
-# idempotent terminal/narrative projections. Replays the live-failure
-# post-mortem shapes directly against the push channels: an outbox
-# redelivery arriving under the WRONG session id after churn (and after
-# a web restart), and repeated failure pushes that must still project
-# exactly once. Includes this shard's one deliberate app restart (the
-# dedup must survive it — it is database state, not memory).
+# Bundle: replay — plan 13 B1/B2 shapes, rebuilt on the plan-17 ledger:
+# the idempotent events push (UNIQUE(session_id, seq) — database state,
+# so it survives a web restart), the session-churn redelivery shape, and
+# repeated failure pushes that must still project exactly once.
 # =============================================================================
 # =============================================================================
 
 
 def run(ctx):
 def run(ctx):
@@ -21,18 +18,11 @@ def run(ctx):
         conn = db()
         conn = db()
         try:
         try:
             return conn.execute(
             return conn.execute(
-                "select count(*) from session_commands where session_id = ?",
+                "select count(*) from events where session_id = ?",
                 (session,)).fetchone()[0]
                 (session,)).fetchone()[0]
         finally:
         finally:
             conn.close()
             conn.close()
 
 
-    def dedup_rows():
-        conn = db()
-        try:
-            return conn.execute("select count(*) from saddle_entry_dedup").fetchone()[0]
-        finally:
-            conn.close()
-
     def open_session_on_fake(task_id):
     def open_session_on_fake(task_id):
         """A fixture open session row on the shard's saddle (the churn
         """A fixture open session row on the shard's saddle (the churn
         shape: a fresh web-side run row the daemon never really drove)."""
         shape: a fresh web-side run row the daemon never really drove)."""
@@ -48,7 +38,7 @@ def run(ctx):
         finally:
         finally:
             conn.close()
             conn.close()
 
 
-    print("== plan 13 B1: entry dedup is daemon-global, not per-session ==")
+    print("== plan 17 §7.2: event pushes are idempotent per session ==")
     # A first task runs to completion; its session is the replay source.
     # A first task runs to completion; its session is the replay source.
     first = create_queued(auth, {"project_id": "1", "title": "Replay source task",
     first = create_queued(auth, {"project_id": "1", "title": "Replay source task",
                                  "priority": "2"})
                                  "priority": "2"})
@@ -59,34 +49,32 @@ def run(ctx):
         "select max(id) from sessions where task_id = ?", (first,)).fetchone()[0]
         "select max(id) from sessions where task_id = ?", (first,)).fetchone()[0]
     conn.close()
     conn.close()
 
 
-    # The outbox batch: two command entries the daemon already
-    # delivered once (their ids landed with the source session).
+    # The outbox batch: two events the daemon already delivered once.
     burst = [
     burst = [
-        {"id": 5001, "command": "make test", "output": "ok", "exit_code": 0,
-         "type": "command", "timestamp": "2026-09-11T00:00:00Z"},
-        {"id": 5002, "command": "grep -r flange src/", "output": "src/flange.vala",
-         "exit_code": 0, "type": "command", "timestamp": "2026-09-11T00:00:01Z"},
+        {"seq": 5001, "kind": "tool_call", "actor": "model",
+         "payload": {"turn": 7, "tool": "run_command",
+                     "args": {"command": "make test"}}},
+        {"seq": 5002, "kind": "tool_result", "actor": "harness",
+         "payload": {"turn": 7, "stdout": "ok", "exit_code": 0}},
     ]
     ]
-    status, _ = saddle_api("POST", "/api/saddle/results", fake_token,
-                           {"session_id": src_session, "state": "done",
-                            "entries": burst})
-    check(status == 200 and rows_for(src_session) >= 2,
-          "the entries batch stores under its session")
+    status, body = saddle_api("POST", "/api/saddle/chunks", fake_token,
+                              {"session_id": src_session, "events": burst})
+    check(status == 200 and int(body.get("stored") or 0) == 2,
+          "the event batch stores under its session")
     stored_once = rows_for(src_session)
     stored_once = rows_for(src_session)
-    dedup_after_first = dedup_rows()
 
 
     # Redelivery under the SAME session: consumed, nothing new.
     # Redelivery under the SAME session: consumed, nothing new.
-    status, _ = saddle_api("POST", "/api/saddle/results", fake_token,
-                           {"session_id": src_session, "state": "done",
-                            "entries": burst})
-    check(status == 200 and rows_for(src_session) == stored_once
-          and dedup_rows() == dedup_after_first,
+    status, body = saddle_api("POST", "/api/saddle/chunks", fake_token,
+                              {"session_id": src_session, "events": burst})
+    check(status == 200 and int(body.get("stored") or 0) == 0
+          and rows_for(src_session) == stored_once,
           "a same-session redelivery stores nothing new (consumed silently)")
           "a same-session redelivery stores nothing new (consumed silently)")
 
 
-    # Session churn: a brand-new session id on the same saddle replays
-    # the SAME entry ids — the post-mortem's mis-keyed outbox send.
-    # The entries' ids already populated another session, so the push
-    # may not create rows here either.
+    # Session churn: a mis-keyed outbox send under a brand-new session
+    # id replays the SAME seqs. The ledger's idempotency key is
+    # (session_id, seq) — the churned session is a different key, so
+    # the events store under it (the daemon contract makes the session
+    # id part of the key; the old daemon-global entry dedup is gone).
     conn = db()
     conn = db()
     conn.execute(
     conn.execute(
         "insert into tasks (project_id, title, status, priority, created_at,"
         "insert into tasks (project_id, title, status, priority, created_at,"
@@ -97,26 +85,14 @@ def run(ctx):
         "select max(id) from tasks where title = 'Churn target task'").fetchone()[0]
         "select max(id) from tasks where title = 'Churn target task'").fetchone()[0]
     conn.close()
     conn.close()
     churn_session = open_session_on_fake(churn_task)
     churn_session = open_session_on_fake(churn_task)
-    status, _ = saddle_api("POST", "/api/saddle/results", fake_token,
-                           {"session_id": churn_session, "state": "done",
-                            "entries": burst})
-    check(status == 200 and rows_for(churn_session) == 0,
-          "replay-under-wrong-session stores nothing new (daemon-global dedup)")
-    check(dedup_rows() == dedup_after_first,
-          "the consumed redelivery claims no fresh dedup rows")
-
-    # A fresh entry id under the churned session DOES store — the dedup
-    # keys on the entry id, never on the session.
-    fresh = [{"id": 5003, "command": "git status", "output": "clean",
-              "exit_code": 0, "type": "command", "timestamp": "2026-09-11T00:00:02Z"}]
-    status, _ = saddle_api("POST", "/api/saddle/results", fake_token,
-                           {"session_id": churn_session, "state": "done",
-                            "entries": fresh})
-    check(status == 200 and rows_for(churn_session) == 1
-          and dedup_rows() == dedup_after_first + 1,
-          "a fresh entry id stores under any session of its saddle")
+    status, body = saddle_api("POST", "/api/saddle/chunks", fake_token,
+                              {"session_id": churn_session, "events": burst})
+    check(status == 200 and int(body.get("stored") or 0) == 2
+          and rows_for(churn_session) == 2,
+          "a churned-session replay stores under the new key (per-session seq)")
 
 
-    # Web restart: the dedup survives (database state, not memory).
+    # Web restart: the idempotency survives (database constraint, not
+    # memory) — the same-session redelivery still stores nothing.
     app.terminate()
     app.terminate()
     app.wait(timeout=10)
     app.wait(timeout=10)
     app = start_app()
     app = start_app()
@@ -127,17 +103,16 @@ def run(ctx):
     # re-establishes sleeping with a fresh ask (the real daemon's
     # re-establishes sleeping with a fresh ask (the real daemon's
     # idle-awake ask / 15-minute safety self-ask).
     # idle-awake ask / 15-minute safety self-ask).
     fake.ask_now()
     fake.ask_now()
-    status, _ = saddle_api("POST", "/api/saddle/results", fake_token,
-                           {"session_id": churn_session, "state": "done",
-                            "entries": burst + fresh})
-    check(status == 200 and rows_for(churn_session) == 1
-          and rows_for(src_session) == stored_once,
-          "a post-restart redelivery still stores nothing (dedup is persistent)")
+    status, body = saddle_api("POST", "/api/saddle/chunks", fake_token,
+                              {"session_id": churn_session, "events": burst})
+    check(status == 200 and int(body.get("stored") or 0) == 0
+          and rows_for(churn_session) == 2,
+          "a post-restart redelivery still stores nothing (persistent)")
 
 
-    print("== plan 13 B2: duplicate failed entries project once (task) ==")
-    # The post-mortem's honest burst: the same failure entry redelivered
-    # under a churned session AND a second, DISTINCT failure entry id
-    # in the same session — one transition, one comment, ever.
+    print("== plan 13 B2: duplicate failed pushes project once (task) ==")
+    # The post-mortem's honest burst, rebuilt on the state channel: the
+    # failed state redelivered repeatedly — one transition, one
+    # summary, ever (terminal pushes are consumed silently).
     def agent_comments(task_id, body=None):
     def agent_comments(task_id, body=None):
         conn = db()
         conn = db()
         try:
         try:
@@ -162,40 +137,32 @@ def run(ctx):
         "select max(id) from tasks where title = 'Burst failure task'").fetchone()[0]
         "select max(id) from tasks where title = 'Burst failure task'").fetchone()[0]
     conn.close()
     conn.close()
     burst_session = open_session_on_fake(burst_task)
     burst_session = open_session_on_fake(burst_task)
-    first_fail = [{"id": 6001, "command": "saddle failure",
-                   "output": "bash -n rejected the script", "exit_code": 1,
-                   "type": "failed", "timestamp": "2026-09-11T00:01:00Z"}]
     status, _ = saddle_api("POST", "/api/saddle/results", fake_token,
     status, _ = saddle_api("POST", "/api/saddle/results", fake_token,
                            {"session_id": burst_session, "state": "failed",
                            {"session_id": burst_session, "state": "failed",
-                            "entries": first_fail,
                             "summary": "bash -n rejected the script"})
                             "summary": "bash -n rejected the script"})
     check(status == 200
     check(status == 200
           and task_state(auth, burst_task)["status"] == "failed",
           and task_state(auth, burst_task)["status"] == "failed",
-          "the failure entry + state pair fails the task")
+          "the failure state push fails the task")
     check(agent_comments(burst_task, "bash -n rejected the script") == 0
     check(agent_comments(burst_task, "bash -n rejected the script") == 0
           and "bash -n rejected the script"
           and "bash -n rejected the script"
           in (task_state(auth, burst_task).get("summary") or ""),
           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 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.
-    second_fail = [{"id": 6002, "command": "saddle failure",
-                    "output": "bash -n rejected the script", "exit_code": 1,
-                    "type": "failed", "timestamp": "2026-09-11T00:01:01Z"}]
+    # The burst: the same failure redelivered under the session AND a
+    # churned session — nothing re-runs.
     churned_burst = open_session_on_fake(burst_task)
     churned_burst = open_session_on_fake(burst_task)
     for target in (burst_session, churned_burst):
     for target in (burst_session, churned_burst):
         status, _ = saddle_api("POST", "/api/saddle/results", fake_token,
         status, _ = saddle_api("POST", "/api/saddle/results", fake_token,
                                {"session_id": target, "state": "failed",
                                {"session_id": target, "state": "failed",
-                                "entries": first_fail + second_fail,
                                 "summary": "bash -n rejected the script"})
                                 "summary": "bash -n rejected the script"})
         check(status == 200,
         check(status == 200,
-              "the burst push under %s is consumed, not rejected" % (
+              "the failed-state push under %s is consumed, not rejected" % (
                   "its session" if target == burst_session else "a churned session"))
                   "its session" if target == burst_session else "a churned session"))
     check(task_state(auth, burst_task)["status"] == "failed"
     check(task_state(auth, burst_task)["status"] == "failed"
           and agent_comments(burst_task) == 0
           and agent_comments(burst_task) == 0
           and "bash -n rejected the script"
           and "bash -n rejected the script"
           in (task_state(auth, burst_task).get("summary") or ""),
           in (task_state(auth, burst_task).get("summary") or ""),
-          "duplicate failed entries leave one transition, one summary, zero agent comments (S4)")
+          "duplicate failed pushes leave one transition, one summary, zero agent comments (S4)")
 
 
     print("== plan 13 B2: goal failure projects once ==")
     print("== plan 13 B2: goal failure projects once ==")
     # Goal context: `saddle failure` fails the goal ONCE — the 115
     # Goal context: `saddle failure` fails the goal ONCE — the 115
@@ -229,27 +196,20 @@ def run(ctx):
         (run_task,)).fetchone()[0]
         (run_task,)).fetchone()[0]
     conn.close()
     conn.close()
 
 
-    goal_fail = [{"id": 7001, "command": "saddle failure",
-                  "output": "the harness collapsed", "exit_code": 1,
-                  "type": "failed", "timestamp": "2026-09-11T00:02:00Z"},
-                 {"id": 7002, "command": "saddle failure",
-                  "output": "the harness collapsed", "exit_code": 1,
-                  "type": "failed", "timestamp": "2026-09-11T00:02:01Z"}]
     status, _ = saddle_api("POST", "/api/saddle/results", fake_token,
     status, _ = saddle_api("POST", "/api/saddle/results", fake_token,
                            {"session_id": run_session, "state": "failed",
                            {"session_id": run_session, "state": "failed",
-                            "entries": goal_fail, "summary": "the harness collapsed"})
+                            "summary": "the harness collapsed"})
     check(status == 200 and goal_state(burst_goal)["status"] == "failed",
     check(status == 200 and goal_state(burst_goal)["status"] == "failed",
           "the goal agent's failure fails the goal")
           "the goal agent's failure fails the goal")
     check(goal_failed_events(burst_goal) == 1,
     check(goal_failed_events(burst_goal) == 1,
-          "a two-entry failure burst records exactly one goal-failed event")
+          "the failure push records exactly one goal-failed event")
 
 
-    # Redeliveries — same ids under the run's session AND under a
-    # churned session — plus the bare failed state again: still one.
+    # Redeliveries — under the run's session AND a churned session —
+    # plus the bare failed state again: still one.
     churned_run = open_session_on_fake(run_task)
     churned_run = open_session_on_fake(run_task)
     for target in (run_session, churned_run):
     for target in (run_session, churned_run):
         status, _ = saddle_api("POST", "/api/saddle/results", fake_token,
         status, _ = saddle_api("POST", "/api/saddle/results", fake_token,
                                {"session_id": target, "state": "failed",
                                {"session_id": target, "state": "failed",
-                                "entries": goal_fail,
                                 "summary": "the harness collapsed"})
                                 "summary": "the harness collapsed"})
         check(status == 200, "the goal-failure redelivery under %s is consumed"
         check(status == 200, "the goal-failure redelivery under %s is consumed"
               % ("its session" if target == run_session else "a churned session"))
               % ("its session" if target == run_session else "a churned session"))
@@ -266,11 +226,11 @@ def run(ctx):
     check(burst_blocks == 0,
     check(burst_blocks == 0,
           "the goal-failed path raises ZERO blocks (nothing pins the pile)")
           "the goal-failed path raises ZERO blocks (nothing pins the pile)")
 
 
-    print("== plan 13 B4: the comment projection stores the body ==")
-    # The heredoc shape: the command is the pipe invocation, the OUTPUT
-    # is the body. The projections must carry the body — the command
-    # echo never becomes comment text, and the literal `saddle
-    # comment` string never renders outside the terminal.
+    print("== plan 17: agent remarks ride the ledger as comment events ==")
+    # The heredoc `cat << 'EOF' | saddle comment` shape: the daemon
+    # emits a comment event carrying the body. The body is the record —
+    # no command echo ever renders, and the goal page's current-activity
+    # line reads the newest remark.
     act(auth, slot_public(auth, "/goals", "goals")["create"], {
     act(auth, slot_public(auth, "/goals", "goals")["create"], {
         "project_id": "1", "title": "Comment goal", "brief": "Reports cleanly.",
         "project_id": "1", "title": "Comment goal", "brief": "Reports cleanly.",
         "branch_name": "goal/comment"})
         "branch_name": "goal/comment"})
@@ -295,23 +255,24 @@ def run(ctx):
     conn.close()
     conn.close()
 
 
     comment_body = "The flange weld holds. First sentence carries the information."
     comment_body = "The flange weld holds. First sentence carries the information."
-    comment_entry = [{"id": 8001, "command": "cat << 'EOF' | saddle comment",
-                      "output": comment_body, "exit_code": 0, "type": "comment",
-                      "timestamp": "2026-09-11T00:03:00Z"}]
-    status, _ = saddle_api("POST", "/api/saddle/results", fake_token,
-                           {"session_id": comment_session, "state": "suspended",
-                            "entries": comment_entry})
-    check(status == 200, "the comment entry push is accepted")
+    status, body = saddle_api("POST", "/api/saddle/chunks", fake_token,
+                              {"session_id": comment_session,
+                               "events": [{
+                                   "seq": 8001, "kind": "comment",
+                                   "actor": "model",
+                                   "payload": {"body": comment_body}}]})
+    check(status == 200 and int(body.get("stored") or 0) == 1,
+          "the comment event stores on the ledger")
     conn = db()
     conn = db()
-    task_comment_bodies = [r[0] for r in conn.execute(
-        "select body from task_comments where task_id = ?", (comment_run,))]
     tables = {r[0] for r in conn.execute(
     tables = {r[0] for r in conn.execute(
         "select name from sqlite_master where type = 'table'").fetchall()}
         "select name from sqlite_master where type = 'table'").fetchall()}
+    comment_events = conn.execute(
+        "select count(*) from events where scope_id = ? and kind = 'comment'",
+        (comment_run,)).fetchone()[0]
     conn.close()
     conn.close()
-    check(task_comment_bodies == [comment_body],
-          "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(comment_events == 1
+          and "goal_comments" not in tables,
+          "the ledger carries the remark (the goal_comments table is gone)")
     narrative_comments = [e["text"] for e in goal_state(comment_goal)["events"]
     narrative_comments = [e["text"] for e in goal_state(comment_goal)["events"]
                           if e["kind"] == "comment"]
                           if e["kind"] == "comment"]
     check(len(narrative_comments) == 1
     check(len(narrative_comments) == 1
@@ -322,20 +283,16 @@ def run(ctx):
 
 
     state = goal_state(comment_goal)
     state = goal_state(comment_goal)
     check(state["current_command"] == comment_body,
     check(state["current_command"] == comment_body,
-          "the goal page's current-activity line shows the comment body")
-    check(state["comments"] and state["comments"][0]["body"] == comment_body
-          and any(e["kind"] == "comment"
-                  and e["text"].startswith("The flange weld holds.")
-                  for e in state["events"]),
-          "the goal slot carries the body in its comments and narrative")
+          "the goal page's current-activity line shows the remark body")
+    check(any(e["kind"] == "comment"
+              and e["text"].startswith("The flange weld holds.")
+              for e in state["events"]),
+          "the goal slot carries the remark in its narrative")
     page = http_get_page(auth, "/goals/%d" % comment_goal)
     page = http_get_page(auth, "/goals/%d" % comment_goal)
     check("saddle comment" not in page,
     check("saddle comment" not in page,
           "the rendered goal page never shows the command echo")
           "the rendered goal page never shows the command echo")
     check("event.kind == 'comment' ? ' footnote'" in page,
     check("event.kind == 'comment' ? ' footnote'" in page,
           "the narrative's comment events render with footnote styling")
           "the narrative's comment events render with footnote styling")
-    check(page.index('stm-markdown="comment.body"')
-          < page.index('stm-text="comment.quote"'),
-          "the comment card shows the body before the quote")
 
 
     # Drain: nothing queued remains for the shard's shutdown.
     # Drain: nothing queued remains for the shard's shutdown.
     check(queued_count() == 0, "the replay shard drains its queue")
     check(queued_count() == 0, "the replay shard drains its queue")

+ 79 - 38
test/harness.py

@@ -249,14 +249,14 @@ def http(method, url, data=None, headers=None, timeout=30):
     # Retry dropped/refused connections: the freshly started app can
     # Retry dropped/refused connections: the freshly started app can
     # close a very early keep-alive connection once while warming up,
     # close a very early keep-alive connection once while warming up,
     # and its first seconds can wobble under load.
     # and its first seconds can wobble under load.
-    for attempt in range(3):
+    for attempt in range(6):
         try:
         try:
             with urllib.request.urlopen(request, timeout=timeout) as response:
             with urllib.request.urlopen(request, timeout=timeout) as response:
                 return response.status, response.read().decode()
                 return response.status, response.read().decode()
         except urllib.error.HTTPError as error:
         except urllib.error.HTTPError as error:
             return error.code, error.read().decode()
             return error.code, error.read().decode()
         except (urllib.error.URLError, OSError):
         except (urllib.error.URLError, OSError):
-            if attempt >= 2:
+            if attempt >= 5:
                 raise
                 raise
             time.sleep(0.75)
             time.sleep(0.75)
 
 
@@ -450,12 +450,30 @@ def command_rows(session):
     conn = db()
     conn = db()
     try:
     try:
         return conn.execute(
         return conn.execute(
-            "select command_text, type from session_commands"
+            "select kind, payload from events"
             " where session_id = ? order by id", (session,)).fetchall()
             " where session_id = ? order by id", (session,)).fetchall()
     finally:
     finally:
         conn.close()
         conn.close()
 
 
 
 
+def event_rows(task_id, kinds=None):
+    """The task's ledger rows (kind, payload JSON), oldest first."""
+    conn = db()
+    try:
+        if kinds:
+            marks = ",".join("?" for _ in kinds)
+            return conn.execute(
+                "select kind, payload from events"
+                " where scope_type = 'task' and scope_id = ? and kind in (%s)"
+                " order by id" % marks, (task_id,) + tuple(kinds)).fetchall()
+        return conn.execute(
+            "select kind, payload from events"
+            " where scope_type = 'task' and scope_id = ? order by id",
+            (task_id,)).fetchall()
+    finally:
+        conn.close()
+
+
 class FakeSaddle:
 class FakeSaddle:
     """In-process saddle daemon implementing the wire contract.
     """In-process saddle daemon implementing the wire contract.
 
 
@@ -540,45 +558,54 @@ class FakeSaddle:
             self.last_activity_epoch = int(time.time())
             self.last_activity_epoch = int(time.time())
 
 
     def queue_output(self, entry_type, command, output):
     def queue_output(self, entry_type, command, output):
-        """Pushes a typed entry as a round-results push (the daemon's
-        entry log riding the results channel)."""
-        with self.lock:
-            entry = {
-                "id": self.output_seq,
-                "command": command,
-                "output": output,
-                "exit_code": 1,
-                "type": entry_type,
-                "timestamp": "2026-09-06T00:00:00Z",
-            }
-            self.output_seq += 1
-        self.push_results(self.state if self.state in ("working",) else "working",
-                          entries=[entry])
+        """Pushes one command_output event home (the daemon's entry log
+        now rides the events channel; the old results-entries path is
+        gone)."""
+        self.push_events([{"kind": "command_output",
+                           "payload": {"command": command, "text": output}}])
 
 
     # --- plan 10 phase 2: the push channels ---
     # --- plan 10 phase 2: the push channels ---
 
 
-    def push_chunks(self, chunks):
-        """Pushes raw PTY chunks home: elements are texts (assigned
-        fresh seqs) or {seq, text} dicts (a redelivery or explicit
-        corpus). Pushes are suppressed while silent/hold_state (the
-        staleness tests' silent runner)."""
+    def push_events(self, events):
+        """Pushes a batch of ledger events home (plan 17 §7.2): each
+        element a {seq?, kind, actor?, ts?, payload?} dict; a missing
+        seq is assigned from the per-session monotonic counter.
+        Pushes are suppressed while silent/hold_state (the staleness
+        tests' silent runner)."""
         if self.dead or self.silent or self.token is None or self.session_id is None:
         if self.dead or self.silent or self.token is None or self.session_id is None:
             return
             return
         batch = []
         batch = []
         with self.lock:
         with self.lock:
-            for chunk in chunks:
-                if isinstance(chunk, dict):
-                    batch.append({"seq": int(chunk["seq"]), "text": chunk["text"]})
-                else:
-                    batch.append({"seq": self.next_push_seq(), "text": chunk})
+            for event in events:
+                item = dict(event)
+                item["seq"] = int(item.pop("seq", 0) or self.next_push_seq())
+                item.setdefault("kind", "progress")
+                item.setdefault("actor", "model")
+                item.setdefault("payload", {})
+                batch.append(item)
         try:
         try:
             saddle_api("POST", "/api/saddle/chunks", self.token,
             saddle_api("POST", "/api/saddle/chunks", self.token,
-                       {"session_id": self.session_id, "chunks": batch})
+                       {"task_id": self.running_task,
+                        "session_id": self.session_id, "events": batch})
         except (OSError, ValueError):
         except (OSError, ValueError):
             pass  # app restarting / shutting down mid-test
             pass  # app restarting / shutting down mid-test
 
 
+    def push_chunks(self, chunks):
+        """Legacy-shaped helper: raw texts (or {seq, text} dicts, a
+        redelivery) push as progress events carrying the text."""
+        batch = []
+        for chunk in chunks:
+            if isinstance(chunk, dict):
+                batch.append({"seq": chunk.get("seq"),
+                              "kind": chunk.get("kind", "progress"),
+                              "payload": chunk.get("payload",
+                                                   {"text": chunk.get("text", "")})})
+            else:
+                batch.append({"kind": "progress", "payload": {"text": chunk}})
+        self.push_events(batch)
+
     def next_push_seq(self):
     def next_push_seq(self):
-        # The daemon-side chunk counter for ad-hoc pushes.
+        # The daemon-side per-session event counter for ad-hoc pushes.
         self._push_seq = getattr(self, "_push_seq", 0) + 1
         self._push_seq = getattr(self, "_push_seq", 0) + 1
         return self._push_seq
         return self._push_seq
 
 
@@ -594,8 +621,6 @@ class FakeSaddle:
         if target is None:
         if target is None:
             return None
             return None
         payload = {"session_id": target, "state": state}
         payload = {"session_id": target, "state": state}
-        if entries is not None:
-            payload["entries"] = entries
         if block is not None:
         if block is not None:
             payload["block"] = block
             payload["block"] = block
             if state == "blocked":
             if state == "blocked":
@@ -711,14 +736,30 @@ class FakeSaddle:
             self.goal_behaviour = self.goal_script.pop(0) if self.goal_script else ("suspend",)
             self.goal_behaviour = self.goal_script.pop(0) if self.goal_script else ("suspend",)
         self.set_state("failing" if self.fail_next else "working")
         self.set_state("failing" if self.fail_next else "working")
         self.fail_next = False
         self.fail_next = False
-        # The round's opening telemetry: the flushed-so-far raw PTY
-        # batch plus the working state push (>= 2 flushes per round
-        # and the 10s timer are the real daemon's contract; the fake
-        # pushes its batch up front and heartbeats while working).
+        # The round's opening telemetry: the §7.2 event batch (turn 1:
+        # lifecycle, turn text, tool call + result, usage) plus the
+        # working state push. A new session restarts the per-session
+        # seq counter; a scripted corpus supplements the batch.
+        self._push_seq = 0
+        opening = [
+            {"kind": "lifecycle",
+             "actor": "harness",
+             "payload": {"action": "start", "text": "round started"}},
+            {"kind": "turn_text",
+             "payload": {"turn": 1, "text": "Starting the assigned task."}},
+            {"kind": "tool_call",
+             "payload": {"turn": 1, "tool": "run_command",
+                         "args": {"command": "echo hello"}}},
+            {"kind": "tool_result",
+             "actor": "harness",
+             "payload": {"turn": 1, "stdout": "hello\n", "exit_code": 0}},
+            {"kind": "usage",
+             "actor": "harness",
+             "payload": {"turn": 1, "input_tokens": 120, "output_tokens": 30}},
+        ]
         if self.scripted_corpus is not None:
         if self.scripted_corpus is not None:
-            self.push_chunks([dict(chunk) for chunk in self.scripted_corpus])
-        else:
-            self.push_chunks([self.PROMPT, "echo hello\r\n", "hello\r\n"])
+            opening = [dict(chunk) for chunk in self.scripted_corpus]
+        self.push_events(opening)
         self.push_results("working")
         self.push_results("working")
 
 
     class Handler(BaseHTTPRequestHandler):
     class Handler(BaseHTTPRequestHandler):