Merge pull request #9056 from continuedev/nate/fix-devbox-session-resume-initial-prompt

feat(cli): prevent initial prompt replay on devbox resume
This commit is contained in:
Nate Sesti
2025-12-07 16:50:36 -08:00
committed by GitHub
3 changed files with 83 additions and 3 deletions

View File

@@ -0,0 +1,12 @@
# Devbox entrypoint behavior (cn serve)
Context: runloop resumes a devbox by re-running the same entrypoint script, which invokes `cn serve --id <agentId> ...`. Because the entrypoint always replays, the CLI must avoid duplicating state on restart.
- **Session reuse:** `serve` now calls `loadOrCreateSessionById` when `--id` is provided so the same session file is reused instead of generating a new UUID. This keeps chat history intact across suspend/resume.
- **Skip replaying the initial prompt:** `shouldQueueInitialPrompt` checks existing history and only queues the initial prompt when there are no non-system messages. This prevents the first prompt from being resent when a suspended devbox restarts.
- **Environment persistence:** The devbox entrypoint (control-plane) writes all env vars to `~/.continue/devbox-env` and sources it before `cn serve`, so keys survive suspend/resume. The CLI assumes env is already present.
Operational notes:
- Changing the entrypoint is expensive; prefer adapting CLI/session behavior as above.
- When testing suspend/resume, confirm a single session file under `~/.continue/sessions` for the agent id and that follow-up messages append normally without replaying the first prompt.

View File

@@ -0,0 +1,30 @@
import type { ChatHistoryItem } from "core/index.js";
import { describe, expect, it } from "vitest";
import { shouldQueueInitialPrompt } from "./serve.js";
const systemOnly: ChatHistoryItem[] = [
{ message: { role: "system", content: "sys" }, contextItems: [] },
];
const withConversation: ChatHistoryItem[] = [
...systemOnly,
{ message: { role: "user", content: "hi" }, contextItems: [] },
{ message: { role: "assistant", content: "hello" }, contextItems: [] },
];
describe("shouldQueueInitialPrompt", () => {
it("returns false when no prompt is provided", () => {
expect(shouldQueueInitialPrompt([], undefined)).toBe(false);
expect(shouldQueueInitialPrompt([], null)).toBe(false);
});
it("returns true when prompt exists and only system history is present", () => {
expect(shouldQueueInitialPrompt([], "prompt")).toBe(true);
expect(shouldQueueInitialPrompt(systemOnly, "prompt")).toBe(true);
});
it("returns false when prompt exists but conversation already has non-system messages", () => {
expect(shouldQueueInitialPrompt(withConversation, "prompt")).toBe(false);
});
});

View File

@@ -51,6 +51,26 @@ interface ServeOptions extends ExtendedCommandOptions {
id?: string;
}
/**
* Decide whether to enqueue the initial prompt on server startup.
* We only want to send it when starting a brand-new session; if any non-system
* messages already exist (e.g., after resume), skip to avoid replaying.
*/
export function shouldQueueInitialPrompt(
history: ChatHistoryItem[],
prompt?: string | null,
): boolean {
if (!prompt) {
return false;
}
// If there are any non-system messages, we already have conversation context
const hasConversation = history.some(
(item) => item.message.role !== "system",
);
return !hasConversation;
}
// eslint-disable-next-line max-statements
export async function serve(prompt?: string, options: ServeOptions = {}) {
await posthogService.capture("sessionStart", {});
@@ -419,10 +439,28 @@ export async function serve(prompt?: string, options: ServeOptions = {}) {
agentFileState?.agentFile?.prompt,
actualPrompt,
);
if (initialPrompt) {
console.log(chalk.dim("\nProcessing initial prompt..."));
await messageQueue.enqueueMessage(initialPrompt);
processMessages(state, llmApi);
const existingHistory =
(() => {
try {
return services.chatHistory.getHistory();
} catch {
return state.session.history;
}
})() ?? [];
if (shouldQueueInitialPrompt(existingHistory, initialPrompt)) {
logger.info(chalk.dim("\nProcessing initial prompt..."));
await messageQueue.enqueueMessage(initialPrompt);
processMessages(state, llmApi);
} else {
logger.info(
chalk.dim(
"Skipping initial prompt because existing conversation history was found.",
),
);
}
}
});