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:
12
extensions/cli/src/commands/devbox-entrypoint.md
Normal file
12
extensions/cli/src/commands/devbox-entrypoint.md
Normal 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.
|
||||
30
extensions/cli/src/commands/serve.initialPrompt.test.ts
Normal file
30
extensions/cli/src/commands/serve.initialPrompt.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
@@ -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.",
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user