From a8ca5128b829d8bab5f0ba3e117c27e56c13ccec Mon Sep 17 00:00:00 2001 From: Pepijn Date: Wed, 13 May 2026 16:26:49 +0200 Subject: [PATCH] fix(annotate): re-emit plan at every subtask boundary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously only emitted a plan at t=0 and on interjections, so the active plan rendered into training carried "done" subtasks until the next interjection. With the new "plan = remaining subtasks" summariser this meant the plan was stale between boundaries. Emit a fresh plan row at every subtask start. ``active_at(t)`` then returns a plan that contains exactly the subtasks whose start ≥ the current span's start — completed subtasks fall off the plan the moment the next subtask begins. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../modules/plan_subtasks_memory.py | 33 ++++++++++++------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/src/lerobot/annotations/steerable_pipeline/modules/plan_subtasks_memory.py b/src/lerobot/annotations/steerable_pipeline/modules/plan_subtasks_memory.py index bf04eddfd..15ad5d287 100644 --- a/src/lerobot/annotations/steerable_pipeline/modules/plan_subtasks_memory.py +++ b/src/lerobot/annotations/steerable_pipeline/modules/plan_subtasks_memory.py @@ -116,18 +116,29 @@ class PlanSubtasksMemoryModule: "tool_calls": None, } ) - # plan row at t=0 - plan_text = self._generate_plan(record, subtask_spans, task=effective_task) - if plan_text is not None: - rows.append( - { - "role": "assistant", - "content": plan_text, - "style": "plan", - "timestamp": float(t0), - "tool_calls": None, - } + # Plan rows at every subtask boundary — including t=0 (start of + # the first subtask). Because the plan is just a numbered list + # of *still-todo* subtasks, re-emitting at each boundary makes + # the active plan shrink as work progresses: at frame t the + # rendered ``${plan}`` is the most recent emission, which + # contains exactly the subtasks that started at or after the + # current span. Saves the runtime from having to derive + # "what's still left" at inference time. + for span in subtask_spans: + boundary_t = _snap_to_frame(span["start"], record.frame_timestamps) + plan_text = self._generate_plan( + record, subtask_spans, refresh_t=boundary_t, task=effective_task ) + if plan_text is not None: + rows.append( + { + "role": "assistant", + "content": plan_text, + "style": "plan", + "timestamp": float(boundary_t), + "tool_calls": None, + } + ) # memory rows at every subtask boundary except the very first start prior_memory = "" for i, span in enumerate(subtask_spans[1:], start=1):