fix(session): own target admission descriptors through finalization

This commit is contained in:
morluto
2026-10-24 01:40:14 +08:00
parent a8276d1289
commit 2732d538e8
7 changed files with 695 additions and 213 deletions
+30 -10
View File
@@ -53,6 +53,10 @@ import { ProviderAdapterError } from "../../domain/providerAdapterError.js";
import { profilelessNativeDispatchProfile } from "./ProfilelessNativeWorkflowSnapshot.js";
import type { AnalysisSnapshot } from "../../domain/analysisSnapshot.js";
import { AnalysisSnapshotMismatchError } from "../../domain/evidenceErrors.js";
import {
parseBinaryTarget,
type BinaryTargetResolverOptions,
} from "../BinaryTargetResolver.js";
import {
resolveAppBundleResourceRoot,
type ResolvedAppBundleResourceRoot,
@@ -422,6 +426,17 @@ export class BinarySession
return () => this.#availabilityListeners.delete(listener);
}
/** Resolve target identity under this session's file cleanup owner. */
resolveTarget(
path: string,
options: Omit<BinaryTargetResolverOptions, "resources"> = {},
) {
return parseBinaryTarget(path, {
...options,
resources: this.fileResources,
});
}
/** Resolve a target and provider profile without creating a provider client. */
previewTarget(
target: BinaryTarget,
@@ -464,6 +479,7 @@ export class BinarySession
return this.#open(
(admitted) =>
resolveSessionOpen({
resources: this.fileResources,
router: this.#providerRouter,
current: this.#active,
path,
@@ -850,7 +866,7 @@ export class BinarySession
});
if (clearRecords) this.clearSessionRecords();
this.#clearRuntimeAvailability();
const fileResources = await this.closeWriteResources();
const fileResources = await this.closeFileResources();
if (!fileResources.ok) onCleanupFailure?.(fileResources.error);
const candidateResources = await this.#providerRouter.closeOwnedResources();
if (!candidateResources.ok) onCleanupFailure?.(candidateResources.error);
@@ -865,20 +881,24 @@ export class BinarySession
];
let resourceFailure = resourceFailures[0];
for (const next of resourceFailures.slice(1))
if (resourceFailure !== undefined)
if (resourceFailure !== undefined) {
const previous = resourceFailure;
resourceFailure = analysisErrorWithCleanupFailure(
resourceFailure,
previous,
next,
"close_binary",
);
if (!closed.ok && resourceFailure !== undefined)
return err(
analysisErrorWithCleanupFailure(
closed.error,
resourceFailure,
"close_binary",
),
resourceFailure.bindCleanupDependencies([previous, next]);
}
if (!closed.ok && resourceFailure !== undefined) {
const combined = analysisErrorWithCleanupFailure(
closed.error,
resourceFailure,
"close_binary",
);
combined.bindCleanupDependencies([closed.error, resourceFailure]);
return err(combined);
}
if (!closed.ok) return closed;
return resourceFailure === undefined ? ok(null) : err(resourceFailure);
}
@@ -9,6 +9,7 @@ import {
} from "../../domain/analysisSnapshot.js";
import type { AnalysisSnapshot } from "../../domain/analysisSnapshot.js";
import { parseBinaryTarget } from "../BinaryTargetResolver.js";
import type { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js";
import type { BinaryTarget } from "../../domain/binaryTargetTypes.js";
import {
EvidenceIntegrityError,
@@ -56,6 +57,7 @@ export interface ResolvedSessionOpen {
}
interface ResolveSessionOpenInput {
readonly resources: ArtifactResourceScope;
readonly router: SessionProviderRouter;
readonly current: CurrentOpenBinding | undefined;
readonly path: string;
@@ -83,6 +85,7 @@ export const resolveSessionOpen = async (
): Promise<Result<ResolvedSessionOpen, AnalysisError>> => {
const { path, options } = input;
const parsed = await parseBinaryTarget(path, {
resources: input.resources,
...(options.targetKind === undefined
? {}
: { targetKind: options.targetKind }),
+25 -16
View File
@@ -33,7 +33,12 @@ import { AnalysisSnapshotCache } from "./AnalysisSnapshotCache.js";
import { InvestigationRecords } from "../investigation/InvestigationRecords.js";
import { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js";
import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js";
import { writeAnalysisSnapshot as writeSnapshotFile } from "./AnalysisSnapshotFiles.js";
import { bindRegularFileCleanup } from "../RegularFileRead.js";
import {
writeAnalysisSnapshot as writeSnapshotFile,
readAnalysisSnapshot as readSnapshotFile,
} from "./AnalysisSnapshotFiles.js";
import { ProviderCleanupError } from "../../domain/providerCleanupError.js";
import { workflowAnalysisProfile } from "../WorkflowAnalysisProfile.js";
import {
hasOnlyProfilelessNativeDispatchEntries,
@@ -63,10 +68,10 @@ export interface WorkflowSnapshotRecordInput {
/** Own binary snapshots and coordinate the composed investigation records. */
export abstract class BinarySessionRecords {
readonly #records: InvestigationRecords;
readonly #writeResources = new ArtifactResourceScope();
protected readonly fileResources = new ArtifactResourceScope();
protected get hasPendingFileResources(): boolean {
return this.#writeResources.hasPending;
return this.fileResources.hasPending;
}
readonly #snapshot = new AnalysisSnapshotCache();
#snapshotInvalidated = false;
@@ -112,6 +117,11 @@ export abstract class BinarySessionRecords {
return this.#records.immutableEvidenceBundle();
}
/** Read one snapshot while retaining failed descriptors under this session. */
readAnalysisSnapshot(path: string, signal?: AbortSignal) {
return readSnapshotFile(path, signal, this.fileResources);
}
/** Persist one provider-neutral snapshot under this session's resource owner. */
writeAnalysisSnapshot(
snapshot: AnalysisSnapshot,
@@ -120,28 +130,27 @@ export abstract class BinarySessionRecords {
): Promise<
Result<{ readonly path: string; readonly bytes: number }, AnalysisError>
> {
return writeSnapshotFile(snapshot, path, overwrite, this.#writeResources);
return writeSnapshotFile(snapshot, path, overwrite, this.fileResources);
}
/** Retry and release staged snapshot files at the owning session shutdown. */
async closeWriteResources(): Promise<Result<null, AnalysisError>> {
/** Retry retained target reads and snapshot files at the owning session shutdown. */
async closeFileResources(): Promise<Result<null, AnalysisError>> {
try {
await this.#writeResources.retryCleanup();
await this.fileResources.retryCleanup();
return ok(null);
} catch (cause: unknown) {
const observation = ArtifactReaderFailure.cleanupObservation(
cause,
"analysis snapshot staging",
"analysis session files",
);
return err(
new EvidenceFileError("write", "io", {
cause,
cleanup: {
reason: observation.reason,
resources: observation.resources,
},
}),
const error = new ProviderCleanupError(
"rea",
observation.resources,
{ reason: observation.reason },
{ cause, operation: "close_binary" },
);
bindRegularFileCleanup(cause, error);
return err(error);
}
}