Skip to content

Commit 04b7d03

Browse files
authored
feat: Implement session/close (#177)
* feat: Add session close support * Resolve interrupted turns after failed interrupts * Suppress MCP startup updates during session close * Handle session close races cleanly * Close stale session opens on generation mismatch * Prevent stale session cleanup races * Block session reopen during stale cleanup * Fence stale session and turn cleanup on close
1 parent 6ec28b8 commit 04b7d03

8 files changed

Lines changed: 1065 additions & 46 deletions

src/CodexAcpClient.ts

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ export class CodexAcpClient {
199199
return this.codexClient.accountRead({refreshToken: false});
200200
}
201201

202-
async resumeSession(request: acp.ResumeSessionRequest): Promise<SessionMetadata> {
202+
async resumeSession(request: acp.ResumeSessionRequest, onSubscribed?: () => void): Promise<SessionMetadata> {
203203
await this.refreshSkills(request.cwd, request._meta);
204204

205205
const response = await this.codexClient.threadResume({
@@ -208,6 +208,7 @@ export class CodexAcpClient {
208208
modelProvider: this.getResumeModelProvider(),
209209
threadId: request.sessionId,
210210
});
211+
onSubscribed?.();
211212
const codexModels = await this.fetchAvailableModels();
212213
const currentModelId = this.createModelId(codexModels, response.model, response.reasoningEffort).toString();
213214
return {
@@ -218,13 +219,14 @@ export class CodexAcpClient {
218219
}
219220
}
220221

221-
async loadSession(request: acp.LoadSessionRequest): Promise<SessionMetadataWithThread> {
222+
async loadSession(request: acp.LoadSessionRequest, onSubscribed?: () => void): Promise<SessionMetadataWithThread> {
222223
const response = await this.codexClient.threadResume({
223224
config: await this.createSessionConfig(request.cwd, request.mcpServers ?? []),
224225
cwd: request.cwd,
225226
modelProvider: this.getResumeModelProvider(),
226227
threadId: request.sessionId,
227228
});
229+
onSubscribed?.();
228230
const codexModels = await this.fetchAvailableModels();
229231
const currentModelId = this.createModelId(codexModels, response.model, response.reasoningEffort).toString();
230232
return {
@@ -258,6 +260,14 @@ export class CodexAcpClient {
258260
};
259261
}
260262

263+
async closeSession(sessionId: string): Promise<void> {
264+
try {
265+
await this.codexClient.threadUnsubscribe({threadId: sessionId});
266+
} finally {
267+
this.codexClient.clearThreadHandlers(sessionId);
268+
}
269+
}
270+
261271
async awaitMcpServerStartup(serverNames: Array<string>, afterVersion: number): Promise<McpStartupResult> {
262272
return await this.codexClient.awaitMcpServerStartup(serverNames, afterVersion);
263273
}
@@ -385,11 +395,16 @@ export class CodexAcpClient {
385395
serviceTier: ServiceTier | null,
386396
disableSummary: boolean,
387397
cwd: string,
388-
): Promise<TurnCompletedNotification> {
398+
onTurnStarted?: (turnId: string) => void,
399+
shouldCancel?: () => boolean,
400+
): Promise<TurnCompletedNotification | null> {
389401
const input = buildPromptItems(request.prompt);
390402
const effort = modelId.effort as ReasoningEffort | null; //TODO remove unsafe conversion
391403

392404
await this.refreshSkills(cwd, request._meta);
405+
if (shouldCancel?.()) {
406+
return null;
407+
}
393408
return await this.codexClient.runTurn({
394409
threadId: request.sessionId,
395410
input: input,
@@ -399,7 +414,15 @@ export class CodexAcpClient {
399414
effort: effort,
400415
model: modelId.model,
401416
serviceTier: serviceTier,
402-
});
417+
}, onTurnStarted);
418+
}
419+
420+
resolveTurnInterrupted(params: { threadId: string, turnId: string }): void {
421+
this.codexClient.resolveTurnInterrupted(params.threadId, params.turnId);
422+
}
423+
424+
markTurnStale(params: { threadId: string, turnId: string }): void {
425+
this.codexClient.markTurnStale(params.threadId, params.turnId);
403426
}
404427

405428
async listSkills(params?: SkillsListParams): Promise<SkillsListResponse> {

0 commit comments

Comments
 (0)