Skip to content

Commit b9332d7

Browse files
authored
Merge pull request #9055 from continuedev/nate/feat-session-id-persistence
feat(cli): add session ID support for serve command to persist chat history
2 parents 3c2271a + 09deed4 commit b9332d7

File tree

3 files changed

+98
-15
lines changed

3 files changed

+98
-15
lines changed

extensions/cli/src/commands/serve.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,11 @@ import {
2323
ConfigServiceState,
2424
ModelServiceState,
2525
} from "../services/types.js";
26-
import { createSession, getCompleteStateSnapshot } from "../session.js";
26+
import {
27+
createSession,
28+
getCompleteStateSnapshot,
29+
loadOrCreateSessionById,
30+
} from "../session.js";
2731
import { messageQueue } from "../stream/messageQueue.js";
2832
import { constructSystemMessage } from "../systemMessage.js";
2933
import { telemetryService } from "../telemetry/telemetryService.js";
@@ -142,7 +146,11 @@ export async function serve(prompt?: string, options: ServeOptions = {}) {
142146
});
143147
}
144148

145-
const session = createSession(initialHistory);
149+
const trimmedId = options.id?.trim();
150+
const session =
151+
trimmedId && trimmedId.length > 0
152+
? loadOrCreateSessionById(trimmedId, initialHistory)
153+
: createSession(initialHistory);
146154

147155
// Align ChatHistoryService with server session and enable remote mode
148156
try {

extensions/cli/src/session.test.ts

Lines changed: 65 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
createSession,
1111
getCurrentSession,
1212
hasSession,
13+
loadOrCreateSessionById,
1314
loadSession,
1415
saveSession,
1516
startNewSession,
@@ -36,17 +37,24 @@ vi.mock("./util/logger.js", () => ({
3637
error: vi.fn(),
3738
},
3839
}));
39-
vi.mock("../../core/util/history.js", () => ({
40-
default: {
41-
save: vi.fn(),
42-
load: vi.fn(() => ({
43-
sessionId: "test-session-id",
44-
title: "Test Session",
45-
workspaceDirectory: "/test/workspace",
46-
history: [],
47-
})),
48-
list: vi.fn(() => []),
49-
},
40+
const mockHistoryManager: any = vi.hoisted(() => ({
41+
save: vi.fn((session: any) => {
42+
// Mimic writing the session payload to disk so expectations on fs still work
43+
fs.writeFileSync(
44+
`/home/test/.continue/sessions/${session.sessionId}.json`,
45+
JSON.stringify(session),
46+
);
47+
}),
48+
load: vi.fn(() => ({
49+
sessionId: "test-session-id",
50+
title: "Test Session",
51+
workspaceDirectory: "/test/workspace",
52+
history: [],
53+
})),
54+
list: vi.fn(() => []),
55+
}));
56+
vi.mock("core/util/history.js", () => ({
57+
default: mockHistoryManager,
5058
}));
5159

5260
const mockFs = vi.mocked(fs);
@@ -71,6 +79,9 @@ describe("SessionManager", () => {
7179
mockFs.readFileSync.mockReturnValue("[]");
7280
mockFs.readdirSync.mockReturnValue([]);
7381
mockFs.statSync.mockReturnValue({ mtime: new Date() } as any);
82+
83+
// Ensure each test starts with a fresh session
84+
clearSession();
7485
});
7586

7687
describe("getCurrentSession", () => {
@@ -145,6 +156,13 @@ describe("SessionManager", () => {
145156

146157
expect(currentSession).toBe(session);
147158
});
159+
160+
it("should use provided sessionId when supplied", () => {
161+
const session = createSession([], "custom-session-id");
162+
163+
expect(session.sessionId).toBe("custom-session-id");
164+
expect(uuidv4).not.toHaveBeenCalled();
165+
});
148166
});
149167

150168
describe("updateSessionHistory", () => {
@@ -235,6 +253,42 @@ describe("SessionManager", () => {
235253
});
236254
});
237255

256+
describe("loadOrCreateSessionById", () => {
257+
it("should load an existing session and set it as current", () => {
258+
const existingSession: Session = {
259+
sessionId: "existing-id",
260+
title: "Existing",
261+
workspaceDirectory: "/test/workspace",
262+
history: [
263+
{
264+
message: { role: "user", content: "hi" },
265+
contextItems: [],
266+
},
267+
],
268+
};
269+
270+
mockHistoryManager.load.mockReturnValue(existingSession);
271+
272+
const session = loadOrCreateSessionById("existing-id");
273+
274+
expect(session).toBe(existingSession);
275+
expect(mockHistoryManager.load).toHaveBeenCalledWith("existing-id");
276+
expect(getCurrentSession()).toBe(existingSession);
277+
});
278+
279+
it("should create a new session when none exists for the id", () => {
280+
mockHistoryManager.load.mockImplementation(() => {
281+
throw new Error("not found");
282+
});
283+
284+
const session = loadOrCreateSessionById("new-id");
285+
286+
expect(session.sessionId).toBe("new-id");
287+
expect(session.history).toEqual([]);
288+
expect(getCurrentSession()).toBe(session);
289+
});
290+
});
291+
238292
describe("loadSession", () => {
239293
it("should return null if session directory doesn't exist", () => {
240294
mockFs.existsSync.mockReturnValue(false);

extensions/cli/src/session.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -316,9 +316,12 @@ export function loadSession(): Session | null {
316316
/**
317317
* Create a new session
318318
*/
319-
export function createSession(history: ChatHistoryItem[] = []): Session {
319+
export function createSession(
320+
history: ChatHistoryItem[] = [],
321+
sessionId?: string,
322+
): Session {
320323
const session: Session = {
321-
sessionId: uuidv4(),
324+
sessionId: sessionId ?? uuidv4(),
322325
title: DEFAULT_SESSION_TITLE,
323326
workspaceDirectory: process.cwd(),
324327
history,
@@ -526,6 +529,24 @@ export function loadSessionById(sessionId: string): Session | null {
526529
}
527530
}
528531

532+
/**
533+
* Load an existing session by ID or create a new one with that ID.
534+
* Useful for long-lived processes (e.g., cn serve) that need to
535+
* preserve chat history across restarts for the same storage/agent id.
536+
*/
537+
export function loadOrCreateSessionById(
538+
sessionId: string,
539+
history: ChatHistoryItem[] = [],
540+
): Session {
541+
const existing = loadSessionById(sessionId);
542+
if (existing) {
543+
SessionManager.getInstance().setSession(existing);
544+
return existing;
545+
}
546+
547+
return createSession(history, sessionId);
548+
}
549+
529550
/**
530551
* Update the current session's history
531552
*/

0 commit comments

Comments
 (0)