Reuse existing sessions for load/resume if possible (#426)

Closes #415

Add `getOrCreateSession` so resume/load returns existing session config
without recreating the session. Replace ad-hoc permission tracking with
`modes.currentModeId` and centralize config updates through
`syncSessionConfigState` to keep `modes` and `models` in sync with
config option changes. Update tests for the new session shape.
This commit is contained in:
Ben Brandt
2026-03-16 15:59:45 +01:00
committed by GitHub
parent 61a39ea801
commit 31539b4ee9
2 changed files with 82 additions and 39 deletions

View File

@@ -112,9 +112,10 @@ type Session = {
input: Pushable<SDKUserMessage>;
cancelled: boolean;
cwd: string;
permissionMode: PermissionMode;
settingsManager: SettingsManager;
accumulatedUsage: AccumulatedUsage;
modes: SessionModeState;
models: SessionModelState;
configOptions: SessionConfigOption[];
promptRunning: boolean;
pendingMessages: Map<string, { resolve: (cancelled: boolean) => void; order: number }>;
@@ -398,34 +399,17 @@ export class ClaudeAcpAgent implements Agent {
}
async unstable_resumeSession(params: ResumeSessionRequest): Promise<ResumeSessionResponse> {
const response = await this.createSession(
{
cwd: params.cwd,
mcpServers: params.mcpServers ?? [],
_meta: params._meta,
},
{
resume: params.sessionId,
},
);
const result = await this.getOrCreateSession(params);
// Needs to happen after we return the session
setTimeout(() => {
this.sendAvailableCommandsUpdate(response.sessionId);
this.sendAvailableCommandsUpdate(params.sessionId);
}, 0);
return response;
return result;
}
async loadSession(params: LoadSessionRequest): Promise<LoadSessionResponse> {
const response = await this.createSession(
{
cwd: params.cwd,
mcpServers: params.mcpServers ?? [],
_meta: params._meta,
},
{
resume: params.sessionId,
},
);
const result = await this.getOrCreateSession(params);
await this.replaySessionHistory(params.sessionId);
@@ -434,11 +418,7 @@ export class ClaudeAcpAgent implements Agent {
this.sendAvailableCommandsUpdate(params.sessionId);
}, 0);
return {
modes: response.modes,
models: response.models,
configOptions: response.configOptions,
};
return result;
}
async unstable_listSessions(params: ListSessionsRequest): Promise<ListSessionsResponse> {
@@ -941,6 +921,8 @@ export class ClaudeAcpAgent implements Agent {
await this.sessions[params.sessionId].query.setModel(params.value);
}
this.syncSessionConfigState(session, params.configId, params.value);
session.configOptions = session.configOptions.map((o) =>
o.id === params.configId && typeof o.currentValue === "string"
? { ...o, currentValue: params.value }
@@ -961,7 +943,6 @@ export class ClaudeAcpAgent implements Agent {
default:
throw new Error("Invalid Mode");
}
this.sessions[sessionId].permissionMode = modeId;
try {
await this.sessions[sessionId].query.setPermissionMode(modeId);
} catch (error) {
@@ -1064,7 +1045,6 @@ export class ClaudeAcpAgent implements Agent {
response.outcome.optionId === "acceptEdits" ||
response.outcome.optionId === "bypassPermissions")
) {
session.permissionMode = response.outcome.optionId;
await this.client.sessionUpdate({
sessionId,
update: {
@@ -1089,7 +1069,7 @@ export class ClaudeAcpAgent implements Agent {
}
}
if (session.permissionMode === "bypassPermissions") {
if (session.modes.currentModeId === "bypassPermissions") {
return {
behavior: "allow",
updatedInput: toolInput,
@@ -1176,6 +1156,8 @@ export class ClaudeAcpAgent implements Agent {
const session = this.sessions[sessionId];
if (!session) return;
this.syncSessionConfigState(session, configId, value);
session.configOptions = session.configOptions.map((o) =>
o.id === configId && typeof o.currentValue === "string" ? { ...o, currentValue: value } : o,
);
@@ -1189,6 +1171,49 @@ export class ClaudeAcpAgent implements Agent {
});
}
private syncSessionConfigState(session: Session, configId: string, value: string): void {
if (configId === "mode") {
session.modes = { ...session.modes, currentModeId: value };
} else if (configId === "model") {
session.models = { ...session.models, currentModelId: value };
}
}
private async getOrCreateSession(params: {
sessionId: string;
cwd: string;
mcpServers?: NewSessionRequest["mcpServers"];
_meta?: NewSessionRequest["_meta"];
}): Promise<NewSessionResponse> {
const existingSession = this.sessions[params.sessionId];
if (existingSession) {
return {
sessionId: params.sessionId,
modes: existingSession.modes,
models: existingSession.models,
configOptions: existingSession.configOptions,
};
}
const response = await this.createSession(
{
cwd: params.cwd,
mcpServers: params.mcpServers ?? [],
_meta: params._meta,
},
{
resume: params.sessionId,
},
);
return {
sessionId: response.sessionId,
modes: response.modes,
models: response.models,
configOptions: response.configOptions,
};
}
private async createSession(
params: NewSessionRequest,
creationOpts: { resume?: string; forkSession?: boolean } = {},
@@ -1315,10 +1340,6 @@ export class ClaudeAcpAgent implements Agent {
hooks: [
createPostToolUseHook(this.logger, {
onEnterPlanMode: async () => {
const session = this.sessions[sessionId];
if (session) {
session.permissionMode = "plan";
}
await this.client.sessionUpdate({
sessionId,
update: {
@@ -1418,7 +1439,6 @@ export class ClaudeAcpAgent implements Agent {
input: input,
cancelled: false,
cwd: params.cwd,
permissionMode,
settingsManager,
accumulatedUsage: {
inputTokens: 0,
@@ -1426,6 +1446,8 @@ export class ClaudeAcpAgent implements Agent {
cachedReadTokens: 0,
cachedWriteTokens: 0,
},
modes,
models,
configOptions,
promptRunning: false,
pendingMessages: new Map(),

View File

@@ -1328,7 +1328,14 @@ describe("stop reason propagation", () => {
input,
cancelled: false,
cwd: "/test",
permissionMode: "default",
modes: {
currentModeId: "default",
availableModes: [],
},
models: {
currentModelId: "default",
availableModels: [],
},
settingsManager: {} as any,
accumulatedUsage: {
inputTokens: 0,
@@ -1455,7 +1462,14 @@ describe("stop reason propagation", () => {
input,
cwd: "/tmp/test",
cancelled: false,
permissionMode: "default",
modes: {
currentModeId: "default",
availableModes: [],
},
models: {
currentModelId: "default",
availableModels: [],
},
settingsManager: {} as any,
accumulatedUsage: {
inputTokens: 0,
@@ -1521,7 +1535,14 @@ describe("session/close", () => {
input: new Pushable(),
cancelled: false,
cwd: "/test",
permissionMode: "default",
modes: {
currentModeId: "default",
availableModes: [],
},
models: {
currentModelId: "default",
availableModels: [],
},
settingsManager: {} as any,
accumulatedUsage: {
inputTokens: 0,