fix(kuna): preserve live cleanup in caller failures

Reuse canonical typed failure attribution instead of copying cleanup markers. Preserve captured facts, typed primary outcomes and opaque cancellation reasons; bind caller projections to the exact producer cleanup owner.

Refs: #4693
This commit is contained in:
morluto committed 2026-10-17 14:13:16 +08:00
1 parent 5717ad24aa
commit 6eb55be4e8
4 files changed
+236 -97

No files matched your search

+3 -9
View File
@@ -1,6 +1,6 @@
import type { JsonValue } from "./jsonValue.js";
import { ProviderAdapterError } from "./providerAdapterError.js";
import type { AnalysisPartialObservation } from "./analysisErrorBase.js";
import type { AnalysisErrorOptions } from "./analysisErrorBase.js";
/** Provider resources could not be proven closed after bounded cleanup. */
export class ProviderCleanupError extends ProviderAdapterError {
@@ -13,20 +13,14 @@ export class ProviderCleanupError extends ProviderAdapterError {
resources: readonly string[],
diagnostics: Readonly<Record<string, JsonValue>>,
/** Identify the operation owning these resources; binary callers retain their default. */
options?: ErrorOptions & {
options?: Omit<AnalysisErrorOptions, "diagnostics"> & {
readonly operation?: string;
readonly partialObservation?: AnalysisPartialObservation;
readonly remediationAction?: string;
},
) {
const {
operation = "close_binary",
partialObservation,
...errorOptions
} = options ?? {};
const { operation = "close_binary", ...errorOptions } = options ?? {};
super(providerId, operation, {
...errorOptions,
...(partialObservation === undefined ? {} : { partialObservation }),
diagnostics,
});
this.setCleanupResources(resources);
+11 -86
View File
@@ -1,16 +1,6 @@
import type {
AnalysisError,
AnalysisErrorOptions,
} from "../domain/analysisErrorBase.js";
import {
AnalysisArtifactChangedError,
AnalysisCancelledError,
AnalysisOutputError,
AnalysisResourceConstraintError,
AnalysisTimeoutError,
} from "../domain/analysisErrorCore.js";
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
import type { AnalysisError } from "../domain/analysisErrorBase.js";
import { analysisClientFailureForOperation } from "../providers/AnalysisClientFailure.js";
import { KUNA_PROVIDER_ID } from "./KunaRelease.js";
/** Keep one producer failure immutable while each waiter receives its context. */
export const kunaQueryFailureForCaller = (
@@ -23,77 +13,12 @@ export const kunaQueryFailureForCaller = (
source.operation === operation
)
return source;
const diagnostics = {
...source.diagnostics,
producer_operation: source.operation,
};
const options: AnalysisErrorOptions & {
userMessage?: string;
remediationAction?: string;
} = {
cause: source,
diagnostics,
...(source.capturedOutput === undefined
? {}
: { capturedOutput: source.capturedOutput }),
...(source.cleanup === undefined ? {} : { cleanup: source.cleanup }),
...(source.partialObservation === undefined
? {}
: { partialObservation: source.partialObservation }),
...(source.userMessage === undefined
? {}
: { userMessage: source.userMessage }),
...("remediationAction" in source &&
typeof source.remediationAction === "string"
? { remediationAction: source.remediationAction }
: {}),
};
// The command owner keeps the original cleanup failure and its supervisor.
// This contextual diagnostic retains that source as its cause and every resource.
if (source instanceof ProviderCleanupError)
return new ProviderCleanupError(
source.providerId,
source.cleanupResources,
diagnostics,
{
operation,
cause: source,
...(source.partialObservation === undefined
? {}
: { partialObservation: source.partialObservation }),
},
);
if (source instanceof AnalysisTimeoutError)
return new AnalysisTimeoutError(operation, source.timeoutMs, options);
if (source instanceof AnalysisResourceConstraintError)
return new AnalysisResourceConstraintError(
operation,
source.resource,
source.reason,
source.reportedLimits,
{
...options,
...(source.retryable === undefined
? {}
: { retryable: source.retryable }),
},
);
if (source instanceof AnalysisOutputError)
return new AnalysisOutputError(operation, source.reason, {
...options,
cleanupOnly: source.cleanupOnly,
});
if (source instanceof ProviderAdapterError)
return new ProviderAdapterError(source.providerId, operation, options);
if (source instanceof AnalysisCancelledError)
return new AnalysisCancelledError(operation, options);
if (source instanceof AnalysisArtifactChangedError)
return new AnalysisArtifactChangedError(
operation,
source.path,
source.reason,
options,
);
// Preserve unfamiliar error variants rather than flattening their semantics.
return source;
return analysisClientFailureForOperation(
source,
KUNA_PROVIDER_ID,
operation,
{
producer_operation: source.operation,
},
);
};
+59 -2
View File
@@ -13,8 +13,12 @@ import {
AnalysisAccessDeniedError,
AnalysisArtifactChangedError,
AnalysisResourceConstraintError,
AnalysisOutputError,
AnalysisTimeoutError,
} from "../domain/analysisErrorCore.js";
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
import type { JsonValue } from "../domain/jsonValue.js";
import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
import { bindArtifactFileCleanup } from "../artifacts/ArtifactFileOperation.js";
@@ -86,10 +90,14 @@ export const analysisClientFailureForOperation = (
cause: unknown,
providerId: string,
operation: string,
callerDiagnostics?: Readonly<Record<string, JsonValue>>,
): AnalysisError => {
if (!(cause instanceof AnalysisError)) {
const failure = new ProviderAdapterError(providerId, operation, {
cause,
...(callerDiagnostics === undefined
? {}
: { diagnostics: callerDiagnostics }),
...(cause instanceof ArtifactReaderFailure && cause.cleanup !== undefined
? { cleanup: cause.cleanup }
: {}),
@@ -106,10 +114,24 @@ export const analysisClientFailureForOperation = (
!(cause instanceof AnalysisCapabilityUnavailableError) &&
!(cause instanceof AnalysisAccessDeniedError) &&
!(cause instanceof AnalysisArtifactChangedError) &&
!(cause instanceof AnalysisTimeoutError) &&
!(cause instanceof AnalysisOutputError) &&
!(cause instanceof AnalysisResourceConstraintError)
)
return cause;
const context = analysisClientFailureContext(cause, providerId, operation);
const sourceContext = analysisClientFailureContext(
cause,
providerId,
operation,
);
const context = {
...sourceContext,
...(callerDiagnostics === undefined
? {}
: {
diagnostics: { ...sourceContext.diagnostics, ...callerDiagnostics },
}),
};
let result: AnalysisError;
if (cause instanceof HopperRemoteError)
result = new HopperRemoteError(cause.code, cause.safeMessage, {
@@ -142,6 +164,41 @@ export const analysisClientFailureForOperation = (
? {}
: { launcherFailure: cause.launcherFailure }),
});
else if (cause instanceof ProviderCleanupError) {
const { diagnostics, ...outcomeContext } = context;
result = new ProviderCleanupError(
cause.providerId,
cause.cleanupResources,
diagnostics ?? {},
{
...outcomeContext,
operation,
...(cause.remediationAction === undefined
? {}
: { remediationAction: cause.remediationAction }),
},
);
} else if (cause instanceof AnalysisTimeoutError)
result = new AnalysisTimeoutError(operation, cause.timeoutMs, {
...context,
...(cause.userMessage === undefined
? {}
: { userMessage: cause.userMessage }),
...(cause.remediationAction === undefined
? {}
: { remediationAction: cause.remediationAction }),
});
else if (cause instanceof AnalysisOutputError)
result = new AnalysisOutputError(operation, cause.reason, {
...context,
cleanupOnly: cause.cleanupOnly,
...(cause.userMessage === undefined
? {}
: { userMessage: cause.userMessage }),
...(cause.remediationAction === undefined
? {}
: { remediationAction: cause.remediationAction }),
});
else if (cause instanceof AnalysisCancelledError)
result = new AnalysisCancelledError(operation, {
...context,
@@ -202,5 +259,5 @@ export const analysisClientFailureForOperation = (
});
return cause.cleanupIncomplete
? result.bindCleanupDependencies([cause])
: result;
: result.resolveCleanup();
};