feat: Add experimental session delete support (#682)

- **Update Agent Client Protocol SDK to 0.22.0**
- **feat: Add session delete support**
This commit is contained in:
Ben Brandt
2026-05-18 12:43:07 +02:00
committed by GitHub
parent 798844726f
commit 2162d5af62
4 changed files with 118 additions and 8 deletions

8
package-lock.json generated
View File

@@ -9,7 +9,7 @@
"version": "0.35.0",
"license": "Apache-2.0",
"dependencies": {
"@agentclientprotocol/sdk": "0.21.1",
"@agentclientprotocol/sdk": "0.22.0",
"@anthropic-ai/claude-agent-sdk": "0.3.143",
"zod": "^3.25.0 || ^4.0.0"
},
@@ -32,9 +32,9 @@
}
},
"node_modules/@agentclientprotocol/sdk": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-0.21.1.tgz",
"integrity": "sha512-ZTLH+o9QxcZDLX/9ww+W7C2iExnXFM+vD/uGFVSlR61Kzj9FaxUqBC6Rv/kwgA7qVWYUEI9c5ZNqCuO9PM4rKg==",
"version": "0.22.0",
"resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-0.22.0.tgz",
"integrity": "sha512-mnkpiuEfGm7/snhWbrRhe3Hk+WDDjv7U2N/Ql0XiThl9zg7T984dPH8Tich4wlBQnaaXTV4Fl5OQSe2pa9hoFA==",
"license": "Apache-2.0",
"peerDependencies": {
"zod": "^3.25.0 || ^4.0.0"

View File

@@ -60,7 +60,7 @@
"author": "Zed Industries",
"license": "Apache-2.0",
"dependencies": {
"@agentclientprotocol/sdk": "0.21.1",
"@agentclientprotocol/sdk": "0.22.0",
"@anthropic-ai/claude-agent-sdk": "0.3.143",
"zod": "^3.25.0 || ^4.0.0"
},

View File

@@ -37,6 +37,8 @@ import {
SetSessionModeResponse,
CloseSessionRequest,
CloseSessionResponse,
DeleteSessionRequest,
DeleteSessionResponse,
TerminalHandle,
TerminalOutputResponse,
WriteTextFileRequest,
@@ -45,6 +47,7 @@ import {
} from "@agentclientprotocol/sdk";
import {
CanUseTool,
deleteSession,
getSessionMessages,
listSessions,
McpServerConfig,
@@ -629,6 +632,7 @@ export class ClaudeAcpAgent implements Agent {
list: {},
resume: {},
close: {},
delete: {},
},
},
agentInfo: {
@@ -1297,6 +1301,16 @@ export class ClaudeAcpAgent implements Agent {
return {};
}
async unstable_deleteSession(params: DeleteSessionRequest): Promise<DeleteSessionResponse> {
// Tear down any active in-memory state first so the on-disk file isn't
// recreated by an outstanding query writing to it.
if (this.sessions[params.sessionId]) {
await this.teardownSession(params.sessionId);
}
await deleteSession(params.sessionId);
return {};
}
async unstable_setSessionModel(
params: SetSessionModelRequest,
): Promise<SetSessionModelResponse | void> {
@@ -1834,7 +1848,7 @@ export class ClaudeAcpAgent implements Agent {
? Object.fromEntries(server.headers.map((e) => [e.name, e.value]))
: undefined,
};
} else {
} else if (!("type" in server)) {
// Stdio type MCP server (with or without explicit type field)
mcpServers[server.name] = {
type: "stdio",

View File

@@ -1,4 +1,4 @@
import { describe, it, expect, beforeAll, afterAll, vi } from "vitest";
import { describe, it, expect, beforeAll, beforeEach, afterAll, vi } from "vitest";
import { spawn, spawnSync } from "child_process";
import {
Agent,
@@ -35,8 +35,16 @@ import {
type SDKMessageFilter,
} from "../acp-agent.js";
import { Pushable } from "../utils.js";
import { query, SDKAssistantMessage } from "@anthropic-ai/claude-agent-sdk";
import { deleteSession, query, SDKAssistantMessage } from "@anthropic-ai/claude-agent-sdk";
import { randomUUID } from "crypto";
vi.mock("@anthropic-ai/claude-agent-sdk", async (importOriginal) => {
const actual = await importOriginal<typeof import("@anthropic-ai/claude-agent-sdk")>();
return {
...actual,
deleteSession: vi.fn(),
};
});
import type {
BetaToolResultBlockParam,
BetaToolSearchToolResultBlockParam,
@@ -1888,6 +1896,94 @@ describe("session/close", () => {
});
});
describe("session/delete", () => {
function createMockAgent() {
const mockClient = {
sessionUpdate: async () => {},
} as unknown as AgentSideConnection;
return new ClaudeAcpAgent(mockClient, { log: () => {}, error: () => {} });
}
function injectSession(agent: ClaudeAcpAgent, sessionId: string) {
function* empty() {}
const gen = Object.assign(empty(), { interrupt: vi.fn(), close: vi.fn() });
agent.sessions[sessionId] = {
query: gen as any,
input: new Pushable(),
cancelled: false,
cwd: "/test",
sessionFingerprint: JSON.stringify({ cwd: "/test", mcpServers: [] }),
modes: { currentModeId: "default", availableModes: [] },
models: { currentModelId: "default", availableModels: [] },
modelInfos: [],
settingsManager: { dispose: vi.fn() } as any,
accumulatedUsage: {
inputTokens: 0,
outputTokens: 0,
cachedReadTokens: 0,
cachedWriteTokens: 0,
},
configOptions: [],
promptRunning: false,
pendingMessages: new Map(),
nextPendingOrder: 0,
abortController: new AbortController(),
emitRawSDKMessages: false,
contextWindowSize: 200000,
taskState: new Map(),
};
return agent.sessions[sessionId]!;
}
beforeEach(() => {
vi.mocked(deleteSession).mockReset();
vi.mocked(deleteSession).mockResolvedValue(undefined);
});
it("tears down the active session and deletes it from disk", async () => {
const agent = createMockAgent();
const session = injectSession(agent, "session-1");
const result = await agent.unstable_deleteSession({ sessionId: "session-1" });
expect(result).toEqual({});
expect(agent.sessions["session-1"]).toBeUndefined();
expect(session.query.interrupt).toHaveBeenCalled();
expect(session.settingsManager.dispose).toHaveBeenCalled();
expect(session.abortController.signal.aborted).toBe(true);
expect(deleteSession).toHaveBeenCalledWith("session-1");
});
it("deletes a session from disk that is not currently active", async () => {
const agent = createMockAgent();
const result = await agent.unstable_deleteSession({ sessionId: "not-active" });
expect(result).toEqual({});
expect(deleteSession).toHaveBeenCalledWith("not-active");
});
it("propagates errors from the SDK delete call", async () => {
const agent = createMockAgent();
vi.mocked(deleteSession).mockRejectedValueOnce(new Error("Session not found on disk"));
await expect(agent.unstable_deleteSession({ sessionId: "missing" })).rejects.toThrow(
"Session not found on disk",
);
});
it("does not affect other sessions when deleting one", async () => {
const agent = createMockAgent();
injectSession(agent, "session-a");
injectSession(agent, "session-b");
await agent.unstable_deleteSession({ sessionId: "session-a" });
expect(agent.sessions["session-a"]).toBeUndefined();
expect(agent.sessions["session-b"]).toBeDefined();
});
});
describe("getOrCreateSession param change detection", () => {
function createMockAgent() {
const mockClient = {