Skip to content

Commit a4bef4e

Browse files
committed
Fence stale session and turn cleanup on close
1 parent b5e6afb commit a4bef4e

4 files changed

Lines changed: 321 additions & 24 deletions

File tree

src/CodexAcpClient.ts

Lines changed: 8 additions & 2 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 {
@@ -419,6 +421,10 @@ export class CodexAcpClient {
419421
this.codexClient.resolveTurnInterrupted(params.threadId, params.turnId);
420422
}
421423

424+
markTurnStale(params: { threadId: string, turnId: string }): void {
425+
this.codexClient.markTurnStale(params.threadId, params.turnId);
426+
}
427+
422428
async listSkills(params?: SkillsListParams): Promise<SkillsListResponse> {
423429
return this.codexClient.listSkills(params ?? {});
424430
}

src/CodexAcpServer.ts

Lines changed: 98 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ export class CodexAcpServer implements acp.Agent {
9494
private readonly pendingMcpStartupSessions: Map<string, PendingMcpStartupSession>;
9595
private readonly pendingTurnStarts: Map<string, PendingTurnStart>;
9696
private readonly activePrompts: Map<string, ActivePrompt>;
97-
private readonly closingSessions: Set<string>;
97+
private readonly closingSessions: Map<string, number>;
9898
private readonly sessionGenerations: Map<string, number>;
9999
private readonly sessionOpenGenerations: Map<string, number>;
100100

@@ -108,7 +108,7 @@ export class CodexAcpServer implements acp.Agent {
108108
this.pendingMcpStartupSessions = new Map();
109109
this.pendingTurnStarts = new Map();
110110
this.activePrompts = new Map();
111-
this.closingSessions = new Set();
111+
this.closingSessions = new Map();
112112
this.sessionGenerations = new Map();
113113
this.sessionOpenGenerations = new Map();
114114
this.connection = connection;
@@ -209,34 +209,57 @@ export class CodexAcpServer implements acp.Agent {
209209

210210
private beginSessionOpen(sessionId: string): number {
211211
const generation = this.getSessionGeneration(sessionId);
212-
if (this.closingSessions.has(sessionId)) {
212+
if (this.sessionIsClosing(sessionId)) {
213213
throw RequestError.invalidRequest(`Session ${sessionId} is closing`);
214214
}
215215
this.sessionOpenGenerations.set(sessionId, generation);
216216
return generation;
217217
}
218218

219219
private sessionOpenCanInstall(sessionId: string, generation: number): boolean {
220-
return !this.closingSessions.has(sessionId) && this.getSessionGeneration(sessionId) === generation;
220+
return !this.sessionIsClosing(sessionId) && this.getSessionGeneration(sessionId) === generation;
221221
}
222222

223-
private async closeStaleSessionOpen(sessionId: string, generation: number): Promise<void> {
223+
private async cleanupStaleSessionOpen(sessionId: string, generation: number): Promise<boolean> {
224224
if (this.sessionOpenGenerations.get(sessionId) === generation) {
225-
const staleCloseGeneration = this.bumpSessionGeneration(sessionId);
226-
this.closingSessions.add(sessionId);
225+
if (!this.sessionIsClosing(sessionId)) {
226+
this.bumpSessionGeneration(sessionId);
227+
}
228+
this.beginSessionCloseFence(sessionId);
227229
try {
228230
await this.runWithProcessCheck(() => this.codexAcpClient.closeSession(sessionId));
229231
} catch (err) {
230232
logger.error(`Failed to close stale session open for ${sessionId}`, err);
231233
} finally {
232-
if (this.getSessionGeneration(sessionId) === staleCloseGeneration) {
233-
this.closingSessions.delete(sessionId);
234-
}
234+
this.endSessionCloseFence(sessionId);
235235
}
236+
return true;
236237
}
238+
return false;
239+
}
240+
241+
private async closeStaleSessionOpen(sessionId: string, generation: number): Promise<void> {
242+
await this.cleanupStaleSessionOpen(sessionId, generation);
237243
throw RequestError.invalidRequest(`Session ${sessionId} is closing`);
238244
}
239245

246+
private sessionIsClosing(sessionId: string): boolean {
247+
return (this.closingSessions.get(sessionId) ?? 0) > 0;
248+
}
249+
250+
private beginSessionCloseFence(sessionId: string): void {
251+
this.closingSessions.set(sessionId, (this.closingSessions.get(sessionId) ?? 0) + 1);
252+
}
253+
254+
private endSessionCloseFence(sessionId: string): void {
255+
const count = this.closingSessions.get(sessionId) ?? 0;
256+
if (count <= 1) {
257+
this.closingSessions.delete(sessionId);
258+
return;
259+
}
260+
this.closingSessions.set(sessionId, count - 1);
261+
}
262+
240263
private getSessionGeneration(sessionId: string): number {
241264
return this.sessionGenerations.get(sessionId) ?? 0;
242265
}
@@ -258,18 +281,39 @@ export class CodexAcpServer implements acp.Agent {
258281
: null;
259282

260283
let sessionMetadata: SessionMetadata;
284+
let resumeSubscribed = false;
261285
if ("sessionId" in request) {
262286
logger.log(`Resume existing session: ${request.sessionId}...`)
263-
sessionMetadata = await this.runWithProcessCheck(() => this.codexAcpClient.resumeSession(request));
287+
try {
288+
sessionMetadata = await this.runWithProcessCheck(() =>
289+
this.codexAcpClient.resumeSession(request, () => {
290+
resumeSubscribed = true;
291+
})
292+
);
293+
} catch (err) {
294+
if (resumeSubscribed && requestedSessionGeneration !== null) {
295+
await this.cleanupStaleSessionOpen(request.sessionId, requestedSessionGeneration);
296+
}
297+
throw err;
298+
}
264299
} else {
265300
logger.log(`Create new session...`)
266301
sessionMetadata = await this.runWithProcessCheck(() => this.codexAcpClient.newSession(request));
267302
}
268303

269-
const account = await this.getActiveAccount();
270304
const {sessionId, currentModelId, models} = sessionMetadata;
305+
let account: Account | null;
306+
try {
307+
account = await this.getActiveAccount();
308+
} catch (err) {
309+
if (resumeSubscribed && requestedSessionGeneration !== null) {
310+
await this.cleanupStaleSessionOpen(sessionId, requestedSessionGeneration);
311+
}
312+
throw err;
313+
}
271314
const sessionGeneration = requestedSessionGeneration ?? this.beginSessionOpen(sessionId);
272315
if (!this.sessionOpenCanInstall(sessionId, sessionGeneration)) {
316+
resumeSubscribed = false;
273317
await this.closeStaleSessionOpen(sessionId, sessionGeneration);
274318
}
275319
const sessionMcpServers = this.resolveSessionMcpServers(requestedMcpServers, "sessionId" in request);
@@ -293,6 +337,7 @@ export class CodexAcpServer implements acp.Agent {
293337
sessionMcpServers: sessionMcpServers,
294338
}
295339
this.sessions.set(sessionId, sessionState);
340+
resumeSubscribed = false;
296341

297342
if (requestedMcpServers.length > 0 && mcpServerStartupVersion !== null) {
298343
this.pendingMcpStartupSessions.set(sessionId, {
@@ -366,7 +411,7 @@ export class CodexAcpServer implements acp.Agent {
366411
logger.log("Closing session...", {sessionId: params.sessionId});
367412
const closeGeneration = this.bumpSessionGeneration(params.sessionId);
368413
const sessionState = this.sessions.get(params.sessionId);
369-
this.closingSessions.add(params.sessionId);
414+
this.beginSessionCloseFence(params.sessionId);
370415

371416
try {
372417
if (sessionState) {
@@ -389,8 +434,8 @@ export class CodexAcpServer implements acp.Agent {
389434
this.pendingMcpStartupSessions.delete(params.sessionId);
390435
this.pendingTurnStarts.delete(params.sessionId);
391436
this.activePrompts.delete(params.sessionId);
392-
this.closingSessions.delete(params.sessionId);
393437
}
438+
this.endSessionCloseFence(params.sessionId);
394439
}
395440

396441
return {};
@@ -568,13 +613,33 @@ export class CodexAcpServer implements acp.Agent {
568613
: null;
569614

570615
logger.log(`Load existing session: ${request.sessionId}...`);
571-
const sessionMetadata: SessionMetadataWithThread = await this.runWithProcessCheck(() =>
572-
this.codexAcpClient.loadSession(request)
573-
);
616+
let subscribed = false;
617+
let sessionMetadata: SessionMetadataWithThread;
618+
try {
619+
sessionMetadata = await this.runWithProcessCheck(() =>
620+
this.codexAcpClient.loadSession(request, () => {
621+
subscribed = true;
622+
})
623+
);
624+
} catch (err) {
625+
if (subscribed) {
626+
await this.cleanupStaleSessionOpen(request.sessionId, requestedSessionGeneration);
627+
}
628+
throw err;
629+
}
574630

575-
const account = await this.getActiveAccount();
576631
const {sessionId, currentModelId, models, thread} = sessionMetadata;
632+
let account: Account | null;
633+
try {
634+
account = await this.getActiveAccount();
635+
} catch (err) {
636+
if (subscribed) {
637+
await this.cleanupStaleSessionOpen(request.sessionId, requestedSessionGeneration);
638+
}
639+
throw err;
640+
}
577641
if (!this.sessionOpenCanInstall(sessionId, requestedSessionGeneration)) {
642+
subscribed = false;
578643
await this.closeStaleSessionOpen(sessionId, requestedSessionGeneration);
579644
}
580645
const sessionMcpServers = this.resolveSessionMcpServers(requestedMcpServers, true);
@@ -598,6 +663,7 @@ export class CodexAcpServer implements acp.Agent {
598663
sessionMcpServers: sessionMcpServers,
599664
};
600665
this.sessions.set(sessionId, sessionState);
666+
subscribed = false;
601667

602668
if (requestedMcpServers.length > 0 && mcpServerStartupVersion !== null) {
603669
this.pendingMcpStartupSessions.set(sessionId, {
@@ -887,7 +953,7 @@ export class CodexAcpServer implements acp.Agent {
887953
)
888954
);
889955
if (!this.sessions.has(sessionId)
890-
|| this.closingSessions.has(sessionId)
956+
|| this.sessionIsClosing(sessionId)
891957
|| this.pendingMcpStartupSessions.get(sessionId) !== pendingStartup) {
892958
return;
893959
}
@@ -969,6 +1035,10 @@ export class CodexAcpServer implements acp.Agent {
9691035
}
9701036

9711037
private interruptLateStartedTurn(sessionId: string, turnId: string): void {
1038+
this.codexAcpClient.markTurnStale({
1039+
threadId: sessionId,
1040+
turnId,
1041+
});
9721042
void this.runWithProcessCheck(() => this.codexAcpClient.turnInterrupt({
9731043
threadId: sessionId,
9741044
turnId,
@@ -983,7 +1053,7 @@ export class CodexAcpServer implements acp.Agent {
9831053
}
9841054

9851055
private promptIsClosedOrStale(sessionId: string, activePrompt: ActivePrompt): boolean {
986-
return this.activePrompts.get(sessionId) !== activePrompt || this.closingSessions.has(sessionId);
1056+
return this.activePrompts.get(sessionId) !== activePrompt || this.sessionIsClosing(sessionId);
9871057
}
9881058

9891059
private async interruptSessionTurn(
@@ -1000,6 +1070,12 @@ export class CodexAcpServer implements acp.Agent {
10001070
sessionId: sessionState.sessionId,
10011071
currentTurnId: turnId,
10021072
});
1073+
if (resolveInterruptedTurn) {
1074+
this.codexAcpClient.markTurnStale({
1075+
threadId: sessionState.sessionId,
1076+
turnId,
1077+
});
1078+
}
10031079
try {
10041080
await this.runWithProcessCheck(() => this.codexAcpClient.turnInterrupt({
10051081
threadId: sessionState.sessionId,
@@ -1079,7 +1155,7 @@ export class CodexAcpServer implements acp.Agent {
10791155
};
10801156
}
10811157

1082-
if (this.closingSessions.has(params.sessionId)) {
1158+
if (this.sessionIsClosing(params.sessionId)) {
10831159
return {
10841160
stopReason: "cancelled",
10851161
usage: this.buildPromptUsage(sessionState.lastTokenUsage),
@@ -1147,7 +1223,7 @@ export class CodexAcpServer implements acp.Agent {
11471223

11481224
// Check if turn was interrupted (cancelled)
11491225
if (turnCompleted.turn.status === "interrupted") {
1150-
if (!this.closingSessions.has(params.sessionId) && this.sessions.has(params.sessionId)) {
1226+
if (!this.sessionIsClosing(params.sessionId) && this.sessions.has(params.sessionId)) {
11511227
await this.connection.sessionUpdate({
11521228
sessionId: params.sessionId,
11531229
update: {

0 commit comments

Comments
 (0)