From 0cafa2f1dc9a7350678644fc0c3161b2cfb09a43 Mon Sep 17 00:00:00 2001 From: dj-oyu <68707227+dj-oyu@users.noreply.github.com> Date: Sun, 22 Feb 2026 19:17:58 +0900 Subject: [PATCH] fix: show correct phase (e.g. 4/4) on plan completion and preserve memory When all plan steps were completed, the post-processing block was skipped because "completed" status wasn't handled, leaving phase stuck at 1. Additionally, ClearMemory() deleted MEMORY.md immediately, preventing users from reviewing completed plans in the Mini App. - Add SetPhase(n) to directly set phase number - Handle "completed" status in post-processing condition - Replace ClearMemory() with SetCurrentPhase(total) + SetPlanStatus("completed") - Update TestAutoCompleteClears to verify new retention behavior Co-Authored-By: Claude Opus 4.6 --- pkg/agent/context.go | 5 +++++ pkg/agent/loop.go | 23 ++++++++++++++--------- pkg/agent/loop_test.go | 14 ++++++++++---- pkg/agent/memory.go | 11 +++++++++++ 4 files changed, 40 insertions(+), 13 deletions(-) diff --git a/pkg/agent/context.go b/pkg/agent/context.go index 67056def3..acd3a630c 100644 --- a/pkg/agent/context.go +++ b/pkg/agent/context.go @@ -396,6 +396,11 @@ func (cb *ContextBuilder) AdvancePhase() error { return cb.memory.AdvancePhase() } +// SetCurrentPhase sets the current phase number to n. +func (cb *ContextBuilder) SetCurrentPhase(n int) error { + return cb.memory.SetPhase(n) +} + // GetCurrentPhase returns the current phase number. func (cb *ContextBuilder) GetCurrentPhase() int { return cb.memory.GetCurrentPhase() diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index 673f91aa1..25f7273c3 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -833,7 +833,7 @@ func (al *AgentLoop) runAgentLoop(ctx context.Context, agent *AgentInstance, opt // 5a. Auto-advance plan phases after LLM iteration postStatus := agent.ContextBuilder.GetPlanStatus() - if agent.ContextBuilder.HasActivePlan() && (postStatus == "executing" || postStatus == "review") { + if agent.ContextBuilder.HasActivePlan() && (postStatus == "executing" || postStatus == "review" || postStatus == "completed") { // Intercept: if AI changed status to executing or review without user approval // (from interviewing or review), validate and hold at "review". if preStatus == "interviewing" || (preStatus == "review" && postStatus == "executing") { @@ -862,14 +862,19 @@ func (al *AgentLoop) runAgentLoop(ctx context.Context, agent *AgentInstance, opt logger.WarnCF("agent", "Reverted plan to interviewing: no phases defined", map[string]interface{}{"agent_id": agent.ID}) } else if agent.ContextBuilder.IsPlanComplete() { - _ = agent.ContextBuilder.ClearMemory() - if !constants.IsInternalChannel(opts.Channel) { - al.bus.PublishOutbound(bus.OutboundMessage{ - Channel: opts.Channel, - ChatID: opts.ChatID, - Content: "\u2705 Plan completed!", - SkipPlaceholder: true, - }) + // Mark plan as completed (keep memory for review; user can /plan clear) + total := agent.ContextBuilder.GetTotalPhases() + _ = agent.ContextBuilder.SetCurrentPhase(total) + if preStatus != "completed" { + _ = agent.ContextBuilder.SetPlanStatus("completed") + if !constants.IsInternalChannel(opts.Channel) { + al.bus.PublishOutbound(bus.OutboundMessage{ + Channel: opts.Channel, + ChatID: opts.ChatID, + Content: "\u2705 Plan completed!", + SkipPlaceholder: true, + }) + } } } else if agent.ContextBuilder.IsCurrentPhaseComplete() { prev := agent.ContextBuilder.GetCurrentPhase() diff --git a/pkg/agent/loop_test.go b/pkg/agent/loop_test.go index 0e42b98ad..43b46e84d 100644 --- a/pkg/agent/loop_test.go +++ b/pkg/agent/loop_test.go @@ -1317,7 +1317,7 @@ Test } } -// TestAutoCompleteClears verifies that plan is cleared when all phases are complete. +// TestAutoCompleteClears verifies that plan is marked completed with correct phase when all phases are complete. func TestAutoCompleteClears(t *testing.T) { tmpDir, err := os.MkdirTemp("", "agent-auto-complete-*") if err != nil { @@ -1369,9 +1369,15 @@ Test t.Fatalf("ProcessDirectWithChannel failed: %v", err) } - // Plan should be cleared - if agent.ContextBuilder.HasActivePlan() { - t.Error("expected plan to be cleared after completion") + // Plan should be kept with status "completed" and phase set to total + if !agent.ContextBuilder.HasActivePlan() { + t.Error("expected plan to be retained after completion") + } + if status := agent.ContextBuilder.GetPlanStatus(); status != "completed" { + t.Errorf("expected plan status 'completed', got %q", status) + } + if phase := agent.ContextBuilder.GetCurrentPhase(); phase != 1 { + t.Errorf("expected phase 1 (total phases), got %d", phase) } } diff --git a/pkg/agent/memory.go b/pkg/agent/memory.go index 4cb825898..8ad8b7502 100644 --- a/pkg/agent/memory.go +++ b/pkg/agent/memory.go @@ -335,6 +335,17 @@ func (ms *MemoryStore) AdvancePhase() error { return ms.WriteLongTerm(content) } +// SetPhase sets the current phase number to n. +func (ms *MemoryStore) SetPhase(n int) error { + content := ms.ReadLongTerm() + m := rePhase.FindString(content) + if m == "" { + return fmt.Errorf("no phase marker found") + } + content = strings.Replace(content, m, fmt.Sprintf("> Phase: %d", n), 1) + return ms.WriteLongTerm(content) +} + // MarkStep marks the nth step (1-based) in the given phase as done [x]. func (ms *MemoryStore) MarkStep(phase, step int) error { content := ms.ReadLongTerm()