fix(filesystem): retain stable hash cleanup owners across callers

Preserve completed digests and primary failures when descriptor close fails.
Carry the exact handle owner through provider, readiness, cache, doctor and
verifier boundaries, and retry cleanup without rehashing or retrying a raw fd.
Close candidate-owned resources even when no target client was opened.
Keep verifier workspaces when provider shutdown remains incomplete.

Refs #4266
This commit is contained in:
morluto committed 2026-10-16 06:46:25 +08:00
1 parent 034293df14
commit 131fd5ddcd
24 files changed
+1313 -139

No files matched your search

@@ -7,6 +7,7 @@ import type { AnalysisProfileCommitment } from "../../domain/analysisProfile.js"
import type { BinaryTarget } from "../../domain/binaryTargetTypes.js";
import { AnalysisCancelledError } from "../../domain/analysisErrorCore.js";
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
import { ProviderCleanupError } from "../../domain/providerCleanupError.js";
import {
ProviderSelectionError,
type ProviderSelectionFailureReason,
@@ -135,6 +136,28 @@ export class AnalysisProviderRegistry {
);
}
/** Close candidate-owned resources even when no target client was opened. */
async closeOwnedResources(): Promise<Result<null, AnalysisError>> {
const failures: AnalysisError[] = [];
for (const provider of this.#providers) {
if (provider.closeOwnedResources === undefined) continue;
const result = await provider.closeOwnedResources();
if (!result.ok) failures.push(result.error);
}
if (failures.length === 0) return ok(null);
if (failures.length === 1) return err(failures[0]!);
return err(
new ProviderCleanupError(
"rea",
failures.flatMap((failure) => failure.cleanupResources),
{
reason: failures.map((failure) => failure.message).join("; "),
},
{ cause: new AggregateError(failures) },
),
);
}
/** List the unique deep-operation declarations without probing candidates. */
declaredOperations(): readonly string[] {
return [...this.#declaredOperations];
+13 -2
View File
@@ -499,15 +499,26 @@ export class BinarySession
if (clearRecords) this.clearSessionRecords();
this.#clearRuntimeAvailability();
const fileResources = await this.closeWriteResources();
const candidates = await this.#providerRouter.closeOwnedResources();
let lifecycle: Result<null, AnalysisError>;
if (!closed.ok && !fileResources.ok)
return err(
lifecycle = err(
analysisErrorWithCleanupFailure(
closed.error,
fileResources.error,
"close_binary",
),
);
return closed.ok ? fileResources : closed;
else lifecycle = closed.ok ? fileResources : closed;
if (!lifecycle.ok && !candidates.ok)
return err(
analysisErrorWithCleanupFailure(
lifecycle.error,
candidates.error,
"close_binary",
),
);
return lifecycle.ok ? candidates : lifecycle;
}
async #retireClient(
@@ -70,6 +70,11 @@ export class SessionProviderRouter {
return this.route;
}
/** Close provider owners not represented by the currently bound client. */
closeOwnedResources() {
return this.registry.closeOwnedResources();
}
/** Resolve target routes before any provider client is created. */
async resolve(
target: BinaryTarget,