fix(evidence)!: report storage publication limits (#7887)

* fix(evidence): report storage publication limits

* test(evidence): cover staged storage failure cleanup
This commit is contained in:
morluto authored and GitHub committed 2026-10-29 23:03:09 +08:00
1 parent 539a494d25
commit bf115be2cd
6 files changed
+187 -12

No files matched your search

+27 -10
View File
@@ -411,16 +411,16 @@ const writeTextPartsOwned = async (
}),
cause.lease,
);
const failure = classifyWriteFailure(cause.outcome.error);
return stagedCleanupFailure(
new EvidenceFileError(
"write",
fileAccessFailureReason(cause.outcome.error),
{
cause,
path: requestedPath,
cleanup,
},
),
new EvidenceFileError("write", failure.reason, {
cause,
path: requestedPath,
cleanup,
...(failure.systemCode === undefined
? {}
: { systemCode: failure.systemCode }),
}),
cause.lease,
);
}
@@ -437,15 +437,32 @@ const writeTextPartsOwned = async (
path: requestedPath,
}),
);
const failure = classifyWriteFailure(cause);
return err(
new EvidenceFileError("write", fileAccessFailureReason(cause), {
new EvidenceFileError("write", failure.reason, {
cause,
path: requestedPath,
...(failure.systemCode === undefined
? {}
: { systemCode: failure.systemCode }),
}),
);
}
};
/** Recognize only host codes that establish a publication resource refusal. */
const classifyWriteFailure = (
cause: unknown,
): {
readonly reason: EvidenceFileError["reason"];
readonly systemCode?: "EFBIG" | "ENOSPC" | "EDQUOT";
} => {
const code = fileErrorCode(cause);
return code === "EFBIG" || code === "ENOSPC" || code === "EDQUOT"
? { reason: "storage-limit", systemCode: code }
: { reason: fileAccessFailureReason(cause) };
};
/** The retained staging lease clears this error only when a retry confirms release. */
const stagedCleanupFailure = <
Failure extends EvidenceFileError | AnalysisCancelledError,
+11 -1
View File
@@ -260,7 +260,10 @@ export const analysisErrorCategory = (
return "unavailable";
if (error instanceof ArtifactOperationError)
return artifactErrorCategory(error.reason);
if (error instanceof EvidenceFileError && error.reason === "path-limit")
if (
error instanceof EvidenceFileError &&
(error.reason === "path-limit" || error.reason === "storage-limit")
)
return "resource_constraint";
if (
error instanceof EvidenceFileError &&
@@ -565,11 +568,18 @@ const evidenceFileMessage = ({
operation,
reason,
published,
systemCode,
}: EvidenceFileError): string => {
if (published !== undefined)
return "The output file was published, but staging cleanup remains incomplete.";
if (reason === "path-limit")
return "Filesystem path-length constraints prevented reading or atomic publication at the selected path. Choose a shorter local path.";
if (reason === "storage-limit")
return systemCode === "EFBIG"
? "The host file-size limit prevented Evidence publication. Increase the inherited file-size allowance and retry."
: systemCode === undefined
? "A host filesystem resource limit prevented Evidence publication. Check the file-size allowance, filesystem space and quota before retrying."
: "The filesystem lacks space or quota for Evidence publication. Free space or increase quota and retry.";
if (reason === "symlink-traversal")
return "The selected path cannot be resolved because its symbolic-link chain loops or exceeds the host traversal limit. Repair the chain or select another path.";
if (reason === "missing")
+5 -1
View File
@@ -236,7 +236,8 @@ const evidenceFileCode = (
reason: EvidenceFileError["reason"],
): AnalysisErrorProjection["code"] => {
if (reason === "invalid-json") return "evidence_integrity_mismatch";
if (reason === "path-limit") return "resource_constraint";
if (reason === "path-limit" || reason === "storage-limit")
return "resource_constraint";
// A missing, non-file or already-existing path is the caller's selection.
if (reason === "io") return "execution_failure";
return "invalid_request";
@@ -418,6 +419,9 @@ const artifactStateErrorDetails = (
operation: error.operation,
reason: error.reason,
...(error.reason === "path-limit" ? { boundary: "filesystem-path" } : {}),
...(error.systemCode === undefined
? {}
: { system_code: error.systemCode }),
...(error.path === undefined ? {} : { path: error.path }),
...(error.contentIssues === undefined
? {}
+4
View File
@@ -60,6 +60,7 @@ export class EvidenceFileError extends AnalysisError {
readonly published: PublishedEvidenceFileObservation | undefined;
/** Schema issues relative to the selected file's JSON root, never the request envelope. */
readonly contentIssues: readonly AnalysisInputIssue[] | undefined;
readonly systemCode: "EFBIG" | "ENOSPC" | "EDQUOT" | undefined;
constructor(
readonly operation: "read" | "write",
@@ -70,6 +71,7 @@ export class EvidenceFileError extends AnalysisError {
| "invalid-json"
| "missing"
| "path-limit"
| "storage-limit"
| "symlink-traversal"
| "io",
options?: AnalysisErrorOptions & {
@@ -77,6 +79,7 @@ export class EvidenceFileError extends AnalysisError {
readonly published?: PublishedEvidenceFileObservation;
readonly userMessage?: string;
readonly contentIssues?: readonly AnalysisInputIssue[];
readonly systemCode?: "EFBIG" | "ENOSPC" | "EDQUOT";
},
) {
super(`Evidence bundle ${operation} failed: ${reason}`, options);
@@ -84,5 +87,6 @@ export class EvidenceFileError extends AnalysisError {
this.published = options?.published;
this.userMessage = options?.userMessage;
this.contentIssues = options?.contentIssues;
this.systemCode = options?.systemCode;
}
}
+4
View File
@@ -189,6 +189,10 @@ const redactFailure = (
const publishedPath = identity(error.published?.path);
return new EvidenceFileError(error.operation, error.reason, {
...context,
...(error.systemCode === undefined ||
text(error.systemCode) !== error.systemCode
? {}
: { systemCode: error.systemCode }),
...(retainedPath === undefined ? {} : { path: retainedPath }),
...(error.userMessage === undefined
? {}