Skip to content

Commit e1ec517

Browse files
authored
fix: Prevent preview session race conditions on adapter switch (#899)
1 parent 72c9087 commit e1ec517

File tree

1 file changed

+19
-41
lines changed
  • apps/twig/src/renderer/features/sessions/service

1 file changed

+19
-41
lines changed

apps/twig/src/renderer/features/sessions/service/service.ts

Lines changed: 19 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,8 @@ export class SessionService {
9898
permission?: { unsubscribe: () => void };
9999
}
100100
>();
101-
/** Version counter to discard stale preview session results */
102-
private previewVersion = 0;
101+
/** AbortController for the current in-flight preview session start */
102+
private previewAbort: AbortController | null = null;
103103

104104
/**
105105
* Connect to a task session.
@@ -473,29 +473,16 @@ export class SessionService {
473473

474474
// --- Preview Session Management ---
475475

476-
/**
477-
* Start a lightweight preview session for the task input page.
478-
* This session is used solely to retrieve adapter-specific config options
479-
* (models, modes, reasoning levels) without creating a real PostHog task.
480-
*
481-
* Uses a version counter to prevent race conditions when rapidly switching
482-
* adapters — stale results from a previous start are discarded.
483-
*/
484476
async startPreviewSession(params: {
485477
adapter: "claude" | "codex";
486478
repoPath?: string;
487479
}): Promise<void> {
488-
// Increment version to invalidate any in-flight start
489-
const version = ++this.previewVersion;
480+
this.previewAbort?.abort();
481+
const abort = new AbortController();
482+
this.previewAbort = abort;
490483

491-
// Cancel any existing preview session first
492-
await this.cancelPreviewSession();
493-
494-
// Check if a newer start was requested while we were cancelling
495-
if (version !== this.previewVersion) {
496-
log.info("Preview session start superseded, skipping", { version });
497-
return;
498-
}
484+
await this.cleanupPreviewSession();
485+
if (abort.signal.aborted) return;
499486

500487
const auth = this.getAuthCredentials();
501488
if (!auth) {
@@ -523,13 +510,7 @@ export class SessionService {
523510
adapter: params.adapter,
524511
});
525512

526-
// Check again after the async start — a newer call may have superseded us
527-
if (version !== this.previewVersion) {
528-
log.info(
529-
"Preview session start superseded after agent.start, cleaning up stale session",
530-
{ taskRunId, version },
531-
);
532-
// Clean up the session we just started but is now stale
513+
if (abort.signal.aborted) {
533514
trpcVanilla.agent.cancel
534515
.mutate({ sessionId: taskRunId })
535516
.catch((err) => {
@@ -560,26 +541,24 @@ export class SessionService {
560541
configOptionsCount: configOptions?.length ?? 0,
561542
});
562543
} catch (error) {
563-
// Only clean up if we're still the current version
564-
if (version === this.previewVersion) {
565-
log.error("Failed to start preview session", { error });
566-
sessionStoreSetters.removeSession(taskRunId);
567-
}
544+
if (abort.signal.aborted) return;
545+
log.error("Failed to start preview session", { error });
546+
sessionStoreSetters.removeSession(taskRunId);
568547
}
569548
}
570549

571-
/**
572-
* Cancel and clean up the preview session.
573-
* Unsubscribes and removes from store first (so nothing writes to the old
574-
* session), then awaits the cancel on the main process.
575-
*/
576550
async cancelPreviewSession(): Promise<void> {
551+
this.previewAbort?.abort();
552+
this.previewAbort = null;
553+
await this.cleanupPreviewSession();
554+
}
555+
556+
private async cleanupPreviewSession(): Promise<void> {
577557
const session = sessionStoreSetters.getSessionByTaskId(PREVIEW_TASK_ID);
578558
if (!session) return;
579559

580560
const { taskRunId } = session;
581561

582-
// Unsubscribe and remove from store first so nothing writes to the old session
583562
this.unsubscribeFromChannel(taskRunId);
584563
sessionStoreSetters.removeSession(taskRunId);
585564

@@ -588,8 +567,6 @@ export class SessionService {
588567
} catch (error) {
589568
log.warn("Failed to cancel preview session", { taskRunId, error });
590569
}
591-
592-
log.info("Preview session cancelled", { taskRunId });
593570
}
594571

595572
// --- Subscription Management ---
@@ -661,7 +638,8 @@ export class SessionService {
661638
}
662639

663640
this.connectingTasks.clear();
664-
this.previewVersion = 0;
641+
this.previewAbort?.abort();
642+
this.previewAbort = null;
665643
}
666644

667645
private handleSessionEvent(taskRunId: string, acpMsg: AcpMessage): void {

0 commit comments

Comments
 (0)