From 3ece00521a1f7010fe27120b3207d8313a4e9437 Mon Sep 17 00:00:00 2001 From: dj-oyu <68707227+dj-oyu@users.noreply.github.com> Date: Sun, 22 Feb 2026 00:59:20 +0900 Subject: [PATCH] fix: trigger LLM execution after /plan start approval /plan start was a fast-path command that changed MEMORY.md status to "executing" but never enqueued a message for the LLM worker, so the agent never actually started working on the plan. Add planStartPending flag that causes Run() to enqueue a synthetic message after the fast-path response, triggering an LLM iteration. Co-Authored-By: Claude Opus 4.6 --- pkg/agent/loop.go | 25 ++++++++++++++++++++++--- pkg/agent/loop_test.go | 22 +++++++++++++++++++++- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index 5bc1cae34..b863a991a 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -38,9 +38,10 @@ type AgentLoop struct { stats *stats.Tracker // nil when --stats not passed running atomic.Bool summarizing sync.Map - fallback *providers.FallbackChain - channelManager *channels.Manager - providerCache map[string]providers.LLMProvider + fallback *providers.FallbackChain + channelManager *channels.Manager + providerCache map[string]providers.LLMProvider + planStartPending bool // set by /plan start to trigger LLM execution } // processOptions configures how a message is processed @@ -190,6 +191,23 @@ func (al *AgentLoop) Run(ctx context.Context) error { SkipPlaceholder: true, }) } + // /plan start sets the flag — enqueue a synthetic message so + // the LLM worker actually begins executing the plan. + if al.planStartPending { + al.planStartPending = false + select { + case llmQueue <- bus.InboundMessage{ + Channel: msg.Channel, + ChatID: msg.ChatID, + SenderID: msg.SenderID, + SessionKey: msg.SessionKey, + Content: "The plan has been approved. Begin executing.", + Metadata: msg.Metadata, + }: + case <-ctx.Done(): + return nil + } + } continue } @@ -1575,6 +1593,7 @@ func (al *AgentLoop) handlePlanCommand(args []string) (string, bool) { if err := agent.ContextBuilder.SetPlanStatus("executing"); err != nil { return fmt.Sprintf("Error: %v", err), true } + al.planStartPending = true return "Plan approved. Executing.", true case "next": diff --git a/pkg/agent/loop_test.go b/pkg/agent/loop_test.go index 3b01f92ea..dc8f53527 100644 --- a/pkg/agent/loop_test.go +++ b/pkg/agent/loop_test.go @@ -1041,6 +1041,11 @@ func TestPlanCommand_Start(t *testing.T) { if status := agent.ContextBuilder.GetPlanStatus(); status != "executing" { t.Errorf("expected 'executing', got %q", status) } + + // planStartPending must be set so Run() enqueues an LLM trigger + if !al.planStartPending { + t.Error("expected planStartPending to be true after /plan start") + } } func TestPlanCommand_StartFromReview(t *testing.T) { @@ -1062,6 +1067,10 @@ func TestPlanCommand_StartFromReview(t *testing.T) { if status := agent.ContextBuilder.GetPlanStatus(); status != "executing" { t.Errorf("expected 'executing', got %q", status) } + + if !al.planStartPending { + t.Error("expected planStartPending to be true after /plan start from review") + } } func TestPlanCommand_StartNoPhases(t *testing.T) { @@ -1081,6 +1090,10 @@ func TestPlanCommand_StartNoPhases(t *testing.T) { if status := agent.ContextBuilder.GetPlanStatus(); status != "interviewing" { t.Errorf("expected status to remain 'interviewing', got %q", status) } + + if al.planStartPending { + t.Error("planStartPending must not be set when start is rejected (no phases)") + } } func TestPlanCommand_StartAlreadyExecuting(t *testing.T) { @@ -1094,11 +1107,18 @@ func TestPlanCommand_StartAlreadyExecuting(t *testing.T) { _ = agent.ContextBuilder.WriteMemory(plan) al.handleCommand(context.Background(), bus.InboundMessage{Content: "/plan start"}) - // Try start again + // Clear the flag from the first call (simulating Run() consuming it) + al.planStartPending = false + + // Try start again — should be rejected response, _ := al.handleCommand(context.Background(), bus.InboundMessage{Content: "/plan start"}) if !strings.Contains(response, "already executing") { t.Errorf("expected 'already executing', got %q", response) } + + if al.planStartPending { + t.Error("planStartPending must not be set when plan is already executing") + } } func TestPlanCommand_Done(t *testing.T) {