Skip to content

Commit 6b8dcb3

Browse files
ccchowclaude
andcommitted
fix: remove lastIterationHandledMessage flag, reorder prompt to action-first
The deferred exit flag caused send_message loops — the LLM kept replying without taking action. With "acknowledge last" ordering, unacknowledged messages naturally prevent auto-exit (pendingMessages > 0). The flag is no longer needed. Guidelines now: take action first, acknowledge last, send_message only for questions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 57aa65f commit 6b8dcb3

File tree

2 files changed

+8
-19
lines changed

2 files changed

+8
-19
lines changed

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ Full lists: [`docs/CODING-GOTCHAS.md`](docs/CODING-GOTCHAS.md), [`docs/TESTING-G
9999
- **No auto-completion of blueprints**: Blueprints do NOT auto-transition to `"done"` when all nodes finish. The LLM must explicitly call `complete()` (or the user must take action). This applies everywhere: `runAutopilotLoop`, `maybeFinalizeBlueprint`, and `executeNextNode`. The `maybeFinalizeBlueprint` helper only resets stuck "running" blueprints back to "approved" — it never sets "done". The `complete` action in `executeDecision` has a structural guard: it rejects with `"active_nodes"` error if any nodes are still `"running"` or `"queued"`, and logs a warning if unacknowledged messages exist.
100100
- **Autopilot test mock ordering with reflections**: `mockRunSession` is shared between decision calls and reflection calls (`reflectAndUpdateMemory`). Reflections happen every `REFLECT_EVERY_N` (5) iterations, on pause, and on error. Tests with 5+ loop iterations must insert reflection response mocks at the right positions. For persistent `mockImplementation`, filter on prompt content: reflections contain `"reflecting"`, global memory contains `"global autopilot"` or `"global strategy"`.
101101
- **AI operations in plan-operations.ts**: `enrichNodeInternal`, `reevaluateNodeInternal`, `splitNodeInternal`, `smartDepsInternal`, `reevaluateAllInternal` are extracted from route handlers. Called by `plan-routes.ts` in manual mode only. In autopilot/FSD mode, these endpoints create a user message via `createAutopilotMessage` and call `triggerAutopilotIfNeeded` instead — the autopilot loop handles the request via its tool palette. `runWithRelatedSessionDetection` helper also lives here.
102-
- **Autopilot tool palette**: `autopilot.ts` uses read tools (`get_node_titles`, `get_node_details`, `get_node_handoff`), message tools (`read_user_messages`, `acknowledge_message`, `send_message`) instead of sub-agent AI operations. `send_message(content)` creates an "assistant"-role message visible in BlueprintChat. `AutopilotNodeState` is lightweight (no `description` or `suggestions` — fetched on-demand). Unacknowledged user messages are also injected directly into the prompt at each iteration (via `buildAutopilotPrompt`'s `userMessages` param) so the LLM sees them even without calling `read_user_messages`. The auto-exit condition (`allNodesDone && !pendingMessages`) is deferred for one iteration after message-related actions (`acknowledge_message`, `send_message`, `read_user_messages`) so the LLM can act on message content before the loop exits.
102+
- **Autopilot tool palette**: `autopilot.ts` uses read tools (`get_node_titles`, `get_node_details`, `get_node_handoff`), message tools (`read_user_messages`, `acknowledge_message`, `send_message`) instead of sub-agent AI operations. `send_message(content)` creates an "assistant"-role message visible in BlueprintChat. `AutopilotNodeState` is lightweight (no `description` or `suggestions` — fetched on-demand). Unacknowledged user messages are injected directly into the prompt at each iteration (via `buildAutopilotPrompt`'s `userMessages` param) so the LLM sees them even without calling `read_user_messages`. The prompt instructs the LLM to take action first, then `acknowledge_message` last — the unacknowledged message naturally prevents auto-exit (`pendingMessages.length > 0`) and keeps context in the prompt until the LLM has acted.
103103
- **triggerAutopilotIfNeeded helper**: `plan-routes.ts` has a `triggerAutopilotIfNeeded(blueprintId)` helper that checks if blueprint is in autopilot/FSD mode and no loop is running, then enqueues `runAutopilotLoop`. Used by message endpoint and AI operation endpoints to wake the autopilot when needed.
104104
- **Autopilot pause/resume flow**: When resuming from a safeguard pause, the resume handler must clear `pauseReason` and set `status: "running"` (both via API and optimistically in local state). `runAutopilotLoop` also clears `pauseReason` on start. The PUT endpoint's `switchingToAutopilot` only fires when `executionMode` changes FROM non-autopilot, so re-entering autopilot from a paused-autopilot state uses `runAllNodes` instead. The pause/resume UI is now inside `BlueprintChat` (not standalone `PauseBanner`).
105105
- **BlueprintChat replaces generator section**: The blueprint detail page uses `BlueprintChat` component instead of the old generator textarea + action buttons. It also subsumes `PauseBanner` (inline pause messages with Resume button) and `AutopilotLog` (interleaved log entries). The standalone `PauseBanner` and `AutopilotLog` components still exist for potential reuse but are no longer rendered on the blueprint detail page.

backend/src/autopilot.ts

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -762,10 +762,10 @@ Do NOT call complete() while unacknowledged messages exist — the user may be r
762762
${userMessages.map((m) => `- [${m.id}] ${m.content}`).join("\n")}
763763
764764
**Required**: For each message, follow this order:
765-
1. send_message(content) — reply to the user first: confirm what you understood and what you plan to do.
766-
2. Take action: create_node/batch_create_nodes for feature requests, run_node for tasks, etc.
767-
3. acknowledge_message(messageId) — mark as handled ONLY AFTER you have taken action.
768-
IMPORTANT: Do NOT acknowledge before acting — the message stays visible in your prompt until acknowledged, so you keep context about what the user asked for.
765+
1. Take action FIRST: create_node/batch_create_nodes for feature requests, run_node for tasks, etc.
766+
2. acknowledge_message(messageId) — mark as handled ONLY AFTER you have taken action.
767+
3. Optionally use send_message(content) to answer questions or explain decisions that don't require creating nodes.
768+
IMPORTANT: The message stays visible in your prompt until acknowledged, preserving context. Do NOT acknowledge before acting. Do NOT call send_message repeatedly — one reply per user message is enough.
769769
770770
`;
771771
}
@@ -1329,9 +1329,6 @@ export async function runAutopilotLoop(blueprintId: string, options?: AutopilotL
13291329
let blueprintMemory = getAutopilotMemory(blueprintId);
13301330
const globalMemory = readGlobalMemory();
13311331
let lastReflectionIteration = 0;
1332-
// Track whether the previous iteration handled a user message (acknowledge/send_message).
1333-
// When true, skip auto-exit for one iteration so the LLM can act on the message content.
1334-
let lastIterationHandledMessage = false;
13351332

13361333
const safeguardState: LoopSafeguardState = {
13371334
recentActions: [],
@@ -1351,17 +1348,15 @@ export async function runAutopilotLoop(blueprintId: string, options?: AutopilotL
13511348

13521349
// 2. CHECK EXIT CONDITIONS
13531350
// Exit loop when all nodes are done AND no pending user messages.
1354-
// BUT skip auto-exit if the previous iteration handled a message — give the LLM
1355-
// one more iteration to act on the message content (create nodes, run commands, etc.)
13561351
// Blueprint status is NOT changed — it's managed by the user only.
1352+
// Unacknowledged messages naturally prevent exit (pendingMessages > 0).
1353+
// After the LLM acknowledges (which happens AFTER taking action), exit is allowed.
13571354
const pendingMessages = getUnacknowledgedMessages(blueprintId);
1358-
if (state.allNodesDone && pendingMessages.length === 0 && !lastIterationHandledMessage) {
1355+
if (state.allNodesDone && pendingMessages.length === 0) {
13591356
logAutopilot(blueprintId, iteration, state.summary, "All nodes complete, no pending user messages", "loop_exit");
13601357
log.info(`Autopilot loop exiting for ${blueprintId.slice(0, 8)} at iteration ${iteration} (all nodes done, no pending messages)`);
13611358
break;
13621359
}
1363-
// Reset the flag — it applies for one iteration only
1364-
lastIterationHandledMessage = false;
13651360

13661361
// Check if user switched to manual mode
13671362
const current = getBlueprint(blueprintId);
@@ -1448,12 +1443,6 @@ export async function runAutopilotLoop(blueprintId: string, options?: AutopilotL
14481443
// 4. EXECUTE — Carry out the AI's decision
14491444
const result = await executeDecision(blueprintId, decision);
14501445

1451-
// Track message-handling actions to prevent premature auto-exit.
1452-
// Only acknowledge/send count — read_user_messages is a passive read that shouldn't defer exit.
1453-
if (decision.action === "acknowledge_message" || decision.action === "send_message") {
1454-
lastIterationHandledMessage = true;
1455-
}
1456-
14571446
// 5. LOG
14581447
logAutopilot(blueprintId, iteration, state.summary, decision, result);
14591448

0 commit comments

Comments
 (0)