fix(lifecycle): reject session work once close has started (#6411)

* fix(lifecycle): reject session work once close has started

Process shutdown closed the shared binary session without sealing it, so a
stdio tool call could open a provider after that close and leave the child
running. GDB ran a queued console command or a second close after the session
had started closing because the queued turn did not repeat admission checks.

Shutdown now refuses later target acquisition, and a queued GDB command or
duplicate close fails as unavailable once its turn finds the session closing
or gone.

Co-authored-by: morluto <morluto@users.noreply.github.com>

* fix(gdb): block console admission after cleanup starts

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: morluto <morluto@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2026-10-23 14:53:19 +08:00
1 parent 5d81363391
commit 592c3e2793
5 files changed
+436 -44

No files matched your search

+22
View File
@@ -154,6 +154,8 @@ export class BinarySession
#transition: Promise<void> = Promise.resolve();
#transitionGeneration = 0;
#transitionPending = 0;
/** Process shutdown has begun; later target acquisition must not outlive it. */
#shutdownStarted = false;
readonly #calls = new Map<Promise<unknown>, number>();
readonly #promptCompletionControllers = new Set<AbortController>();
readonly #providerRouter: SessionProviderRouter;
@@ -182,6 +184,8 @@ export class BinarySession
options: Pick<ExecutionOptions, "signal"> = {},
): Promise<Result<ResolvedAppBundleResourceRoot, AnalysisError>> {
return this.#serialize(async (admittedThrough) => {
const refused = this.#targetAcquisitionRefused("open_bundle_resources");
if (refused !== undefined) return refused;
await this.#drainCalls(admittedThrough);
if (isAborted(options.signal))
return err(
@@ -498,6 +502,8 @@ export class BinarySession
options: BinarySessionOpenOptions,
): Promise<Result<BinaryTarget, AnalysisError>> {
return this.#serialize(async (admittedThrough) => {
const refused = this.#targetAcquisitionRefused("open_binary");
if (refused !== undefined) return refused;
const admitted = await this.#withLoadedSnapshot(options);
if (!admitted.ok) return admitted;
return this.#openWithinTransition(
@@ -757,6 +763,22 @@ export class BinarySession
});
}
/**
* Reject target acquisition once process shutdown has begun.
* Acquisition already inside its transition still finishes; the shutdown
* close queued behind that transition retires it.
*/
beginShutdown(): void {
this.#shutdownStarted = true;
}
#targetAcquisitionRefused(
operation: "open_binary" | "open_bundle_resources",
): Result<never, AnalysisCancelledError> | undefined {
if (!this.#shutdownStarted) return undefined;
return err(new AnalysisCancelledError(operation));
}
/** Close the active target, if any. */
close(
options: Pick<ExecutionOptions, "progress"> & {