Skip to content

Commit 08e2688

Browse files
authored
Merge pull request #9056 from continuedev/nate/fix-devbox-session-resume-initial-prompt
feat(cli): prevent initial prompt replay on devbox resume
2 parents b9332d7 + 00b665f commit 08e2688

File tree

3 files changed

+83
-3
lines changed

3 files changed

+83
-3
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Devbox entrypoint behavior (cn serve)
2+
3+
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.
4+
5+
- **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.
6+
- **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.
7+
- **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.
8+
9+
Operational notes:
10+
11+
- Changing the entrypoint is expensive; prefer adapting CLI/session behavior as above.
12+
- 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.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import type { ChatHistoryItem } from "core/index.js";
2+
import { describe, expect, it } from "vitest";
3+
4+
import { shouldQueueInitialPrompt } from "./serve.js";
5+
6+
const systemOnly: ChatHistoryItem[] = [
7+
{ message: { role: "system", content: "sys" }, contextItems: [] },
8+
];
9+
10+
const withConversation: ChatHistoryItem[] = [
11+
...systemOnly,
12+
{ message: { role: "user", content: "hi" }, contextItems: [] },
13+
{ message: { role: "assistant", content: "hello" }, contextItems: [] },
14+
];
15+
16+
describe("shouldQueueInitialPrompt", () => {
17+
it("returns false when no prompt is provided", () => {
18+
expect(shouldQueueInitialPrompt([], undefined)).toBe(false);
19+
expect(shouldQueueInitialPrompt([], null)).toBe(false);
20+
});
21+
22+
it("returns true when prompt exists and only system history is present", () => {
23+
expect(shouldQueueInitialPrompt([], "prompt")).toBe(true);
24+
expect(shouldQueueInitialPrompt(systemOnly, "prompt")).toBe(true);
25+
});
26+
27+
it("returns false when prompt exists but conversation already has non-system messages", () => {
28+
expect(shouldQueueInitialPrompt(withConversation, "prompt")).toBe(false);
29+
});
30+
});

extensions/cli/src/commands/serve.ts

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,26 @@ interface ServeOptions extends ExtendedCommandOptions {
5151
id?: string;
5252
}
5353

54+
/**
55+
* Decide whether to enqueue the initial prompt on server startup.
56+
* We only want to send it when starting a brand-new session; if any non-system
57+
* messages already exist (e.g., after resume), skip to avoid replaying.
58+
*/
59+
export function shouldQueueInitialPrompt(
60+
history: ChatHistoryItem[],
61+
prompt?: string | null,
62+
): boolean {
63+
if (!prompt) {
64+
return false;
65+
}
66+
67+
// If there are any non-system messages, we already have conversation context
68+
const hasConversation = history.some(
69+
(item) => item.message.role !== "system",
70+
);
71+
return !hasConversation;
72+
}
73+
5474
// eslint-disable-next-line max-statements
5575
export async function serve(prompt?: string, options: ServeOptions = {}) {
5676
await posthogService.capture("sessionStart", {});
@@ -419,10 +439,28 @@ export async function serve(prompt?: string, options: ServeOptions = {}) {
419439
agentFileState?.agentFile?.prompt,
420440
actualPrompt,
421441
);
442+
422443
if (initialPrompt) {
423-
console.log(chalk.dim("\nProcessing initial prompt..."));
424-
await messageQueue.enqueueMessage(initialPrompt);
425-
processMessages(state, llmApi);
444+
const existingHistory =
445+
(() => {
446+
try {
447+
return services.chatHistory.getHistory();
448+
} catch {
449+
return state.session.history;
450+
}
451+
})() ?? [];
452+
453+
if (shouldQueueInitialPrompt(existingHistory, initialPrompt)) {
454+
logger.info(chalk.dim("\nProcessing initial prompt..."));
455+
await messageQueue.enqueueMessage(initialPrompt);
456+
processMessages(state, llmApi);
457+
} else {
458+
logger.info(
459+
chalk.dim(
460+
"Skipping initial prompt because existing conversation history was found.",
461+
),
462+
);
463+
}
426464
}
427465
});
428466

0 commit comments

Comments
 (0)