mirror of
https://github.com/agentclientprotocol/claude-agent-acp.git
synced 2026-11-05 00:42:05 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0111786af2 | ||
|
|
aa82193330 | ||
|
|
26f3e8a521 | ||
|
|
769255c9a9 | ||
|
|
bea1a40e9b | ||
|
|
f9b15ba1c5 | ||
|
|
0990a8e520 | ||
|
|
15e91fb5c3 |
@@ -1,3 +1,3 @@
|
||||
{
|
||||
".": "0.25.0"
|
||||
".": "0.25.3"
|
||||
}
|
||||
|
||||
22
CHANGELOG.md
22
CHANGELOG.md
@@ -1,5 +1,27 @@
|
||||
# Changelog
|
||||
|
||||
## [0.25.3](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.25.2...v0.25.3) (2026-04-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Drop claude-agent-sdk back to 0.2.91 to fix broken import ([#513](https://github.com/agentclientprotocol/claude-agent-acp/issues/513)) ([26f3e8a](https://github.com/agentclientprotocol/claude-agent-acp/commit/26f3e8a5216295985fadb80fb3b977045c0c1b2c))
|
||||
* Recreate resumed sessions when params change ([#515](https://github.com/agentclientprotocol/claude-agent-acp/issues/515)) ([aa82193](https://github.com/agentclientprotocol/claude-agent-acp/commit/aa82193330026bae132fed8391c47dde777dcf5a))
|
||||
|
||||
## [0.25.2](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.25.1...v0.25.2) (2026-04-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* prioritize ANTHROPIC_MODEL env var over settings.model in model … ([#505](https://github.com/agentclientprotocol/claude-agent-acp/issues/505)) ([bea1a40](https://github.com/agentclientprotocol/claude-agent-acp/commit/bea1a40e9bfe1e06672b118a727f9339def3be23))
|
||||
|
||||
## [0.25.1](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.25.0...v0.25.1) (2026-04-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add `auto` to valid modes in applySessionMode to fix mode cycling ([#507](https://github.com/agentclientprotocol/claude-agent-acp/issues/507)) ([15e91fb](https://github.com/agentclientprotocol/claude-agent-acp/commit/15e91fb5c3449de2600b583cb7a8d36b5b510443))
|
||||
|
||||
## [0.25.0](https://github.com/agentclientprotocol/claude-agent-acp/compare/v0.24.2...v0.25.0) (2026-04-03)
|
||||
|
||||
|
||||
|
||||
78
package-lock.json
generated
78
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@agentclientprotocol/claude-agent-acp",
|
||||
"version": "0.25.0",
|
||||
"version": "0.25.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@agentclientprotocol/claude-agent-acp",
|
||||
"version": "0.25.0",
|
||||
"version": "0.25.3",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "0.18.0",
|
||||
@@ -19,10 +19,10 @@
|
||||
"devDependencies": {
|
||||
"@anthropic-ai/sdk": "0.82.0",
|
||||
"@eslint/js": "10.0.1",
|
||||
"@types/node": "25.5.1",
|
||||
"@types/node": "25.5.2",
|
||||
"@typescript-eslint/eslint-plugin": "8.58.0",
|
||||
"@typescript-eslint/parser": "8.58.0",
|
||||
"eslint": "10.1.0",
|
||||
"eslint": "10.2.0",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"globals": "17.4.0",
|
||||
"prettier": "3.8.1",
|
||||
@@ -197,13 +197,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/config-array": {
|
||||
"version": "0.23.3",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.3.tgz",
|
||||
"integrity": "sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw==",
|
||||
"version": "0.23.4",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.4.tgz",
|
||||
"integrity": "sha512-lf19F24LSMfF8weXvW5QEtnLqW70u7kgit5e9PSx0MsHAFclGd1T9ynvWEMDT1w5J4Qt54tomGeAhdoAku1Xow==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@eslint/object-schema": "^3.0.3",
|
||||
"@eslint/object-schema": "^3.0.4",
|
||||
"debug": "^4.3.1",
|
||||
"minimatch": "^10.2.4"
|
||||
},
|
||||
@@ -212,22 +212,22 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/config-helpers": {
|
||||
"version": "0.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.3.tgz",
|
||||
"integrity": "sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw==",
|
||||
"version": "0.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.4.tgz",
|
||||
"integrity": "sha512-jJhqiY3wPMlWWO3370M86CPJ7pt8GmEwSLglMfQhjXal07RCvhmU0as4IuUEW5SJeunfItiEetHmSxCCe9lDBg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@eslint/core": "^1.1.1"
|
||||
"@eslint/core": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/core": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.1.1.tgz",
|
||||
"integrity": "sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==",
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.0.tgz",
|
||||
"integrity": "sha512-8FTGbNzTvmSlc4cZBaShkC6YvFMG0riksYWRFKXztqVdXaQbcZLXlFbSpC05s70sGEsXAw0qwhx69JiW7hQS7A==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
@@ -259,9 +259,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/object-schema": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.3.tgz",
|
||||
"integrity": "sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ==",
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.4.tgz",
|
||||
"integrity": "sha512-55lO/7+Yp0ISKRP0PsPtNTeNGapXaO085aELZmWCVc5SH3jfrqpuU6YgOdIxMS99ZHkQN1cXKE+cdIqwww9ptw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
@@ -269,13 +269,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/plugin-kit": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.1.tgz",
|
||||
"integrity": "sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==",
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.0.tgz",
|
||||
"integrity": "sha512-ejvBr8MQCbVsWNZnCwDXjUKq40MDmHalq7cJ6e9s/qzTUFIIo/afzt1Vui9T97FM/V/pN4YsFVoed5NIa96RDg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@eslint/core": "^1.1.1",
|
||||
"@eslint/core": "^1.2.0",
|
||||
"levn": "^0.4.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1095,9 +1095,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "25.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.1.tgz",
|
||||
"integrity": "sha512-lgrR3HRNQdTEeeXBnLURFO4JIIbpcVcMlLM9IG0jsNRTRNSbMkm9S2hyhxhnokke1NM25Dr9QghgeB5PQKolrw==",
|
||||
"version": "25.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.2.tgz",
|
||||
"integrity": "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1868,18 +1868,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint": {
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-10.1.0.tgz",
|
||||
"integrity": "sha512-S9jlY/ELKEUwwQnqWDO+f+m6sercqOPSqXM5Go94l7DOmxHVDgmSFGWEzeE/gwgTAr0W103BWt0QLe/7mabIvA==",
|
||||
"version": "10.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-10.2.0.tgz",
|
||||
"integrity": "sha512-+L0vBFYGIpSNIt/KWTpFonPrqYvgKw1eUI5Vn7mEogrQcWtWYtNQ7dNqC+px/J0idT3BAkiWrhfS7k+Tum8TUA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.2",
|
||||
"@eslint/config-array": "^0.23.3",
|
||||
"@eslint/config-helpers": "^0.5.3",
|
||||
"@eslint/core": "^1.1.1",
|
||||
"@eslint/plugin-kit": "^0.6.1",
|
||||
"@eslint/config-array": "^0.23.4",
|
||||
"@eslint/config-helpers": "^0.5.4",
|
||||
"@eslint/core": "^1.2.0",
|
||||
"@eslint/plugin-kit": "^0.7.0",
|
||||
"@humanfs/node": "^0.16.6",
|
||||
"@humanwhocodes/module-importer": "^1.0.1",
|
||||
"@humanwhocodes/retry": "^0.4.2",
|
||||
@@ -2474,9 +2474,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/hono": {
|
||||
"version": "4.12.10",
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.10.tgz",
|
||||
"integrity": "sha512-mx/p18PLy5og9ufies2GOSUqep98Td9q4i/EF6X7yJgAiIopxqdfIO3jbqsi3jRgTgw88jMDEzVKi+V2EF+27w==",
|
||||
"version": "4.12.11",
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.11.tgz",
|
||||
"integrity": "sha512-r4xbIa3mGGGoH9nN4A14DOg2wx7y2oQyJEb5O57C/xzETG/qx4c7CVDQ5WMeKHZ7ORk2W0hZ/sQKXTav3cmYBA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16.9.0"
|
||||
@@ -3839,9 +3839,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "8.0.3",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.3.tgz",
|
||||
"integrity": "sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==",
|
||||
"version": "8.0.5",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.5.tgz",
|
||||
"integrity": "sha512-nmu43Qvq9UopTRfMx2jOYW5l16pb3iDC1JH6yMuPkpVbzK0k+L7dfsEDH4jRgYFmsg0sTAqkojoZgzLMlwHsCQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -3866,7 +3866,7 @@
|
||||
"peerDependencies": {
|
||||
"@types/node": "^20.19.0 || >=22.12.0",
|
||||
"@vitejs/devtools": "^0.1.0",
|
||||
"esbuild": "^0.27.0",
|
||||
"esbuild": "^0.27.0 || ^0.28.0",
|
||||
"jiti": ">=1.21.0",
|
||||
"less": "^4.0.0",
|
||||
"sass": "^1.70.0",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"version": "0.25.0",
|
||||
"version": "0.25.3",
|
||||
"description": "An ACP-compatible coding agent powered by the Claude Agent SDK (TypeScript)",
|
||||
"main": "dist/lib.js",
|
||||
"types": "dist/lib.d.ts",
|
||||
@@ -67,10 +67,10 @@
|
||||
"devDependencies": {
|
||||
"@anthropic-ai/sdk": "0.82.0",
|
||||
"@eslint/js": "10.0.1",
|
||||
"@types/node": "25.5.1",
|
||||
"@types/node": "25.5.2",
|
||||
"@typescript-eslint/eslint-plugin": "8.58.0",
|
||||
"@typescript-eslint/parser": "8.58.0",
|
||||
"eslint": "10.1.0",
|
||||
"eslint": "10.2.0",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"globals": "17.4.0",
|
||||
"prettier": "3.8.1",
|
||||
|
||||
@@ -114,6 +114,9 @@ type Session = {
|
||||
input: Pushable<SDKUserMessage>;
|
||||
cancelled: boolean;
|
||||
cwd: string;
|
||||
/** Serialized snapshot of session-defining params (cwd, mcpServers) used to
|
||||
* detect when loadSession/resumeSession is called with changed values. */
|
||||
sessionFingerprint: string;
|
||||
settingsManager: SettingsManager;
|
||||
accumulatedUsage: AccumulatedUsage;
|
||||
modes: SessionModeState;
|
||||
@@ -125,6 +128,18 @@ type Session = {
|
||||
abortController: AbortController;
|
||||
};
|
||||
|
||||
/** Compute a stable fingerprint of the session-defining params so we can
|
||||
* detect when a loadSession/resumeSession call requires tearing down and
|
||||
* recreating the underlying Query process. MCP servers are sorted by name
|
||||
* so that ordering differences don't trigger unnecessary recreations. */
|
||||
function computeSessionFingerprint(params: {
|
||||
cwd: string;
|
||||
mcpServers?: NewSessionRequest["mcpServers"];
|
||||
}): string {
|
||||
const servers = [...(params.mcpServers ?? [])].sort((a, b) => a.name.localeCompare(b.name));
|
||||
return JSON.stringify({ cwd: params.cwd, mcpServers: servers });
|
||||
}
|
||||
|
||||
type BackgroundTerminal =
|
||||
| {
|
||||
handle: TerminalHandle;
|
||||
@@ -901,17 +916,24 @@ export class ClaudeAcpAgent implements Agent {
|
||||
await session.query.interrupt();
|
||||
}
|
||||
|
||||
async unstable_closeSession(params: CloseSessionRequest): Promise<CloseSessionResponse> {
|
||||
const session = this.sessions[params.sessionId];
|
||||
/** Cleanly tear down a session: cancel in-flight work, dispose resources,
|
||||
* and remove it from the session map. */
|
||||
private async teardownSession(sessionId: string): Promise<void> {
|
||||
const session = this.sessions[sessionId];
|
||||
if (!session) {
|
||||
throw new Error("Session not found");
|
||||
return;
|
||||
}
|
||||
await this.cancel({ sessionId: params.sessionId });
|
||||
|
||||
await this.cancel({ sessionId });
|
||||
session.settingsManager.dispose();
|
||||
session.abortController.abort();
|
||||
delete this.sessions[params.sessionId];
|
||||
delete this.sessions[sessionId];
|
||||
}
|
||||
|
||||
async unstable_closeSession(params: CloseSessionRequest): Promise<CloseSessionResponse> {
|
||||
if (!this.sessions[params.sessionId]) {
|
||||
throw new Error("Session not found");
|
||||
}
|
||||
await this.teardownSession(params.sessionId);
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -1006,6 +1028,7 @@ export class ClaudeAcpAgent implements Agent {
|
||||
|
||||
private async applySessionMode(sessionId: string, modeId: string): Promise<void> {
|
||||
switch (modeId) {
|
||||
case "auto":
|
||||
case "default":
|
||||
case "acceptEdits":
|
||||
case "bypassPermissions":
|
||||
@@ -1259,12 +1282,20 @@ export class ClaudeAcpAgent implements Agent {
|
||||
}): Promise<NewSessionResponse> {
|
||||
const existingSession = this.sessions[params.sessionId];
|
||||
if (existingSession) {
|
||||
return {
|
||||
sessionId: params.sessionId,
|
||||
modes: existingSession.modes,
|
||||
models: existingSession.models,
|
||||
configOptions: existingSession.configOptions,
|
||||
};
|
||||
const fingerprint = computeSessionFingerprint(params);
|
||||
if (fingerprint === existingSession.sessionFingerprint) {
|
||||
return {
|
||||
sessionId: params.sessionId,
|
||||
modes: existingSession.modes,
|
||||
models: existingSession.models,
|
||||
configOptions: existingSession.configOptions,
|
||||
};
|
||||
}
|
||||
|
||||
// Session-defining params changed (e.g. cwd pointed at a git worktree,
|
||||
// or MCP servers reconfigured). Tear down the existing session and
|
||||
// recreate it so the underlying Query process picks up the new values.
|
||||
await this.teardownSession(params.sessionId);
|
||||
}
|
||||
|
||||
const response = await this.createSession(
|
||||
@@ -1530,6 +1561,7 @@ export class ClaudeAcpAgent implements Agent {
|
||||
input: input,
|
||||
cancelled: false,
|
||||
cwd: params.cwd,
|
||||
sessionFingerprint: computeSessionFingerprint(params),
|
||||
settingsManager,
|
||||
accumulatedUsage: {
|
||||
inputTokens: 0,
|
||||
@@ -1698,7 +1730,16 @@ async function getAvailableModels(
|
||||
|
||||
let currentModel = models[0];
|
||||
|
||||
if (settings.model) {
|
||||
// Model priority (highest to lowest):
|
||||
// 1. ANTHROPIC_MODEL environment variable
|
||||
// 2. settings.model (user configuration)
|
||||
// 3. models[0] (default first model)
|
||||
if (process.env.ANTHROPIC_MODEL) {
|
||||
const match = resolveModelPreference(models, process.env.ANTHROPIC_MODEL);
|
||||
if (match) {
|
||||
currentModel = match;
|
||||
}
|
||||
} else if (settings.model) {
|
||||
const match = resolveModelPreference(models, settings.model);
|
||||
if (match) {
|
||||
currentModel = match;
|
||||
|
||||
@@ -1328,6 +1328,7 @@ describe("stop reason propagation", () => {
|
||||
input,
|
||||
cancelled: false,
|
||||
cwd: "/test",
|
||||
sessionFingerprint: JSON.stringify({ cwd: "/test", mcpServers: [] }),
|
||||
modes: {
|
||||
currentModeId: "default",
|
||||
availableModes: [],
|
||||
@@ -1466,6 +1467,7 @@ describe("stop reason propagation", () => {
|
||||
query: messageGenerator() as any,
|
||||
input,
|
||||
cwd: "/tmp/test",
|
||||
sessionFingerprint: JSON.stringify({ cwd: "/tmp/test", mcpServers: [] }),
|
||||
cancelled: false,
|
||||
modes: {
|
||||
currentModeId: "default",
|
||||
@@ -1540,6 +1542,7 @@ describe("session/close", () => {
|
||||
input: new Pushable(),
|
||||
cancelled: false,
|
||||
cwd: "/test",
|
||||
sessionFingerprint: JSON.stringify({ cwd: "/test", mcpServers: [] }),
|
||||
modes: {
|
||||
currentModeId: "default",
|
||||
availableModes: [],
|
||||
@@ -1609,6 +1612,141 @@ describe("session/close", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("getOrCreateSession param change detection", () => {
|
||||
function createMockAgent() {
|
||||
const mockClient = {
|
||||
sessionUpdate: async () => {},
|
||||
} as unknown as AgentSideConnection;
|
||||
return new ClaudeAcpAgent(mockClient, { log: () => {}, error: () => {} });
|
||||
}
|
||||
|
||||
function injectSession(
|
||||
agent: ClaudeAcpAgent,
|
||||
sessionId: string,
|
||||
opts: { cwd?: string; mcpServers?: { name: string }[] } = {},
|
||||
) {
|
||||
const cwd = opts.cwd ?? "/test";
|
||||
const mcpServers = (opts.mcpServers ?? []) as any[];
|
||||
function* empty() {}
|
||||
const gen = Object.assign(empty(), {
|
||||
interrupt: vi.fn(),
|
||||
supportedCommands: vi.fn().mockResolvedValue([]),
|
||||
});
|
||||
agent.sessions[sessionId] = {
|
||||
query: gen as any,
|
||||
input: new Pushable(),
|
||||
cancelled: false,
|
||||
cwd,
|
||||
sessionFingerprint: JSON.stringify({
|
||||
cwd,
|
||||
mcpServers: [...mcpServers].sort((a: any, b: any) => a.name.localeCompare(b.name)),
|
||||
}),
|
||||
modes: { currentModeId: "default", availableModes: [] },
|
||||
models: { currentModelId: "default", availableModels: [] },
|
||||
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(),
|
||||
};
|
||||
return agent.sessions[sessionId]!;
|
||||
}
|
||||
|
||||
it("returns cached session when params are unchanged", async () => {
|
||||
const agent = createMockAgent();
|
||||
const session = injectSession(agent, "s1", { cwd: "/project" });
|
||||
|
||||
await agent.unstable_resumeSession({
|
||||
sessionId: "s1",
|
||||
cwd: "/project",
|
||||
mcpServers: [],
|
||||
});
|
||||
|
||||
// Session object should be the exact same reference (not recreated)
|
||||
expect(agent.sessions["s1"]).toBe(session);
|
||||
expect(session.settingsManager.dispose).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("tears down existing session when cwd changes", async () => {
|
||||
const agent = createMockAgent();
|
||||
const session = injectSession(agent, "s1", { cwd: "/old" });
|
||||
|
||||
// Mock createSession to avoid spawning a real process.
|
||||
// It will throw, but we can catch that — we only need to verify
|
||||
// the old session was torn down before createSession was attempted.
|
||||
const createSessionSpy = vi
|
||||
.spyOn(agent as any, "createSession")
|
||||
.mockRejectedValue(new Error("mock"));
|
||||
|
||||
await expect(
|
||||
agent.unstable_resumeSession({ sessionId: "s1", cwd: "/new", mcpServers: [] }),
|
||||
).rejects.toThrow("mock");
|
||||
|
||||
// Old session should have been fully torn down
|
||||
expect(session.settingsManager.dispose).toHaveBeenCalled();
|
||||
expect(session.abortController.signal.aborted).toBe(true);
|
||||
expect(session.query.interrupt).toHaveBeenCalled();
|
||||
expect(agent.sessions["s1"]).toBeUndefined();
|
||||
|
||||
// createSession should have been called with the new cwd
|
||||
expect(createSessionSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ cwd: "/new" }),
|
||||
expect.objectContaining({ resume: "s1" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("tears down existing session when mcpServers change", async () => {
|
||||
const agent = createMockAgent();
|
||||
const session = injectSession(agent, "s1", { cwd: "/project" });
|
||||
|
||||
const createSessionSpy = vi
|
||||
.spyOn(agent as any, "createSession")
|
||||
.mockRejectedValue(new Error("mock"));
|
||||
|
||||
await expect(
|
||||
agent.unstable_resumeSession({
|
||||
sessionId: "s1",
|
||||
cwd: "/project",
|
||||
mcpServers: [{ name: "new-server", command: "node", args: ["server.js"], env: [] }],
|
||||
}),
|
||||
).rejects.toThrow("mock");
|
||||
|
||||
expect(session.settingsManager.dispose).toHaveBeenCalled();
|
||||
expect(session.abortController.signal.aborted).toBe(true);
|
||||
expect(agent.sessions["s1"]).toBeUndefined();
|
||||
expect(createSessionSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("treats mcpServers in different order as unchanged", async () => {
|
||||
const agent = createMockAgent();
|
||||
const servers = [
|
||||
{ name: "b-server", command: "node", args: ["b.js"], env: [] },
|
||||
{ name: "a-server", command: "node", args: ["a.js"], env: [] },
|
||||
] as const;
|
||||
const session = injectSession(agent, "s1", {
|
||||
cwd: "/project",
|
||||
mcpServers: servers as any,
|
||||
});
|
||||
|
||||
// Same servers but reversed order — should NOT trigger teardown
|
||||
await agent.unstable_resumeSession({
|
||||
sessionId: "s1",
|
||||
cwd: "/project",
|
||||
mcpServers: [...servers].reverse() as any,
|
||||
});
|
||||
|
||||
expect(agent.sessions["s1"]).toBe(session);
|
||||
expect(session.settingsManager.dispose).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("usage_update computation", () => {
|
||||
function createAssistantMessage(overrides: {
|
||||
model: string;
|
||||
@@ -1710,6 +1848,7 @@ describe("usage_update computation", () => {
|
||||
input,
|
||||
cancelled: false,
|
||||
cwd: "/test",
|
||||
sessionFingerprint: JSON.stringify({ cwd: "/test", mcpServers: [] }),
|
||||
modes: {
|
||||
currentModeId: "default",
|
||||
availableModes: [],
|
||||
|
||||
Reference in New Issue
Block a user