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 <noreply@anthropic.com>
This commit is contained in:
dj-oyu 2026-02-22 19:17:58 +09:00
parent 4e52b1cb75
commit 57a75b281b
4 changed files with 40 additions and 13 deletions

View file

@ -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()

View file

@ -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,7 +862,11 @@ 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()
// 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,
@ -871,6 +875,7 @@ func (al *AgentLoop) runAgentLoop(ctx context.Context, agent *AgentInstance, opt
SkipPlaceholder: true,
})
}
}
} else if agent.ContextBuilder.IsCurrentPhaseComplete() {
prev := agent.ContextBuilder.GetCurrentPhase()
_ = agent.ContextBuilder.AdvancePhase()

View file

@ -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)
}
}

View file

@ -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()