fix(tasks): plan panel showed only the latest step, not the full plan
Root cause: proposePlan unconditionally deleted and replaced the whole
session_plan_steps list on every call. The model isn't strictly held to
"call propose_plan once with the full list" — nothing stopped it (and
production evidence + live testing showed it happening) from calling
propose_plan once per step as it worked. Each such call wiped every
already-completed step, so the operator only ever saw the model's latest
single step ("1/1") instead of the real, growing plan.
Fix, two layers:
- store.go: proposePlan now only does a destructive replace when no step
has left 'pending' yet (a genuine pre-execution revision). Once any step
has started, a new call APPENDS after the current max seq instead of
wiping — so the panel accumulates the full history regardless of how the
model chooses to call the tool. plan.proposed now carries `appended` so
the frontend knows whether to replace or append.
- workspace.ts: plan.proposed handler respects `appended` (update vs set).
- tasks.go / SOUL.md: strengthened the propose_plan description and task-
loop guidance to call it ONCE with the complete step list end-to-end,
using update_plan_step (not re-calling propose_plan) to advance — fixing
the root behavioral cause, with the store-side append as a safety net
that holds even if the model still calls it incrementally.
Verified: forced the exact incremental-call pattern (propose_plan with 1
step, mark it running, propose_plan again with 1 more step) — the second
call appended at seq 2 instead of erasing seq 1, and its plan.proposed
event carried appended=true.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -101,12 +101,15 @@ function applyEvent(ev: { type: string; correlation_id?: string | null; data?: u
|
||||
switch (ev.type) {
|
||||
case 'plan.proposed':
|
||||
if (Array.isArray(data.steps)) {
|
||||
planSteps.set(
|
||||
data.steps.map((s: any) => ({
|
||||
id: s.id, seq: s.seq, title: s.title, detail: s.detail ?? '',
|
||||
status: 'pending', target_slug: s.target_slug || undefined
|
||||
}))
|
||||
)
|
||||
const incoming = data.steps.map((s: any) => ({
|
||||
id: s.id, seq: s.seq, title: s.title, detail: s.detail ?? '',
|
||||
status: 'pending' as const, target_slug: s.target_slug || undefined
|
||||
}))
|
||||
// The server appends rather than replaces once any step has started
|
||||
// (see store.go proposePlan) — mirror that here so a model that calls
|
||||
// propose_plan once per step still shows the FULL running history in
|
||||
// the panel, not just its latest call's single step.
|
||||
planSteps.update((existing) => (data.appended ? [...existing, ...incoming] : incoming))
|
||||
}
|
||||
break
|
||||
case 'plan.step.started':
|
||||
|
||||
Reference in New Issue
Block a user