Skip to content

Commit 5b36bcc

Browse files
authored
fix(acp): make session/load fail for an unknown session ID (#1907)
session/load registered an empty session and succeeded when the ID was neither in memory nor on disk, so a client could not tell that a stored session was gone. It now throws the same 'Session not found on disk' error as session/resume. Closes #1894
1 parent 751d697 commit 5b36bcc

4 files changed

Lines changed: 36 additions & 18 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@nanocollective/nanocoder": patch
3+
---
4+
5+
ACP `session/load` now fails with `Session not found on disk: <id>` for a session ID that is neither in memory nor on disk, the same error `session/resume` already returned. It used to register an empty session and succeed, so a client could not tell that a stored session was gone, and a prompt sent to that ID saved a new session under an ID the client had invented. Loading a session that exists, in memory or on disk, is unchanged.

‎docs/features/acp.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ With ACP the **client is the UI**: the agent runs headless and everything (strea
3434
- **Model display and switching** — the editor shows the current model and lets you switch between the models configured for your active provider.
3535
- **`ask_user`** — when the agent asks a clarifying question, the options appear as selectable buttons in the editor. (Selection only; a free-form typed answer is not available over ACP.)
3636
- **`@`-mentioned files** — files you reference in the editor are read and included in the prompt, using the editor's live buffer (including unsaved edits) when available.
37-
- **Session persistence** - ACP sessions are saved to disk like TUI sessions (see [Session Management](session-management.md)). Loading or resuming a thread replays its history, even after the editor and agent restart.
37+
- **Session persistence** - ACP sessions are saved to disk like TUI sessions (see [Session Management](session-management.md)). Loading or resuming a thread replays its history, even after the editor and agent restart. Loading or resuming a session ID that is neither in memory nor on disk fails with `Session not found on disk: <id>` instead of creating an empty session; only `session/new` creates one.
3838
- **Session management** - clients can list, resume, close and delete sessions. Nanocoder also answers a `renameSession` extension method (`sessionId`, `title`), and new sessions get a generated title from the first message.
3939
- **Images** - PNG, JPEG, GIF and WebP attachments are sent to the model as images.
4040

‎source/acp/acp-agent.spec.ts‎

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -223,21 +223,32 @@ test('AcpAgent.initialize - advertises loadSession capability', async t => {
223223
t.true(result.agentCapabilities?.loadSession);
224224
});
225225

226-
test('AcpAgent.loadSession - creates a usable session for an unknown id', async t => {
226+
test('AcpAgent.loadSession - rejects an unknown id without creating a session', async t => {
227227
const {agent} = createAgent();
228-
const result = await agent.loadSession({
229-
sessionId: 'persisted-123',
230-
cwd: '/tmp',
231-
mcpServers: [],
232-
});
233-
t.truthy(result.modes);
234-
t.truthy(result.configOptions);
235-
// The loaded session must accept prompts (no "session not found").
236-
const prompt = await agent.prompt({
237-
sessionId: 'persisted-123',
238-
prompt: [{type: 'text', text: 'hi'}],
239-
});
240-
t.truthy(prompt.stopReason);
228+
const sessionId = randomUUID();
229+
await t.throwsAsync(
230+
agent.loadSession({sessionId, cwd: '/tmp', mcpServers: []}),
231+
{message: `Session not found on disk: ${sessionId}`},
232+
);
233+
t.false((agent as any).sessions.has(sessionId));
234+
// Nothing was registered, so a following prompt fails and nothing is saved.
235+
await t.throwsAsync(
236+
agent.prompt({sessionId, prompt: [{type: 'text', text: 'hi'}]}),
237+
{message: `Session not found: ${sessionId}`},
238+
);
239+
t.is(await sessionManager.loadSession(sessionId), null);
240+
});
241+
242+
test('AcpAgent - loadSession and resumeSession reject an unknown id with the same error', async t => {
243+
const {agent} = createAgent();
244+
const sessionId = randomUUID();
245+
const loadError = await t.throwsAsync(
246+
agent.loadSession({sessionId, cwd: '/tmp', mcpServers: []}),
247+
);
248+
const resumeError = await t.throwsAsync(
249+
agent.resumeSession({sessionId, cwd: '/tmp'}),
250+
);
251+
t.is(loadError?.message, resumeError?.message);
241252
});
242253

243254
test('AcpAgent.loadSession - replays in-memory history for a known session', async t => {

‎source/acp/acp-agent.ts‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -172,11 +172,13 @@ export class AcpAgent implements Agent {
172172
// Try loading from disk first so history persists across process restarts.
173173
await sessionManager.initialize();
174174
const persisted = await sessionManager.loadSession(params.sessionId);
175+
if (!persisted) {
176+
// Same error as resumeSession so clients can tell a missing session apart.
177+
throw new Error(`Session not found on disk: ${params.sessionId}`);
178+
}
175179
session = this.registerSession(params.sessionId, params.cwd);
176180
clearAcpReadStubs(params.sessionId);
177-
if (persisted) {
178-
session.messages = persisted.messages;
179-
}
181+
session.messages = persisted.messages;
180182
}
181183

182184
logger.info(

0 commit comments

Comments
 (0)