fix(errors): store resource-constraint recovery as one shape (#8055)

* fix(errors): store resource-constraint recovery as one shape

AnalysisResourceConstraintError kept remediationAction, fixedAllowance,
and retryable as independent optionals, so a refusal could claim both a
fixed allowance and a separate remediation. The constructor now stores
the existing ResourceConstraintRecovery union, and the three accessors
read that object.

* test(errors): cover competing resource-constraint recovery fields

A runtime options object can still carry both a fixed allowance and a
remediation action. The constructor keeps the allowance and drops the
other field; the regression now supplies both.

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
This commit is contained in:
morlutoandCursor Agent authored and GitHub committed 2026-10-30 04:50:02 +08:00
1 parent b894be6361
commit fa1aa64ce4
2 files changed
+122 -28

No files matched your search

+63 -27
View File
@@ -33,19 +33,12 @@ export const ANALYSIS_RESOURCE_KINDS = [
/** A measured resource constraint or bounded admission refusal. */
export class AnalysisResourceConstraintError extends AnalysisError {
readonly _tag = "AnalysisResourceConstraintError";
readonly remediationAction: string | undefined;
/**
* The fixed format, decoder, or capture allowance that refused the request.
* Worker limits and host resources do not raise it, so recovery derives from
* this allowance rather than from `resource`.
* The one recovery this refusal carries. A fixed allowance and a separate
* remediation sentence do not co-occur; retryability belongs to that same
* object.
*/
readonly fixedAllowance: FixedAllowance | undefined;
/**
* Whether retrying the same request can succeed. A caller-selected bound is
* deterministic for the same input, so it is not retryable even when its
* resource kind would otherwise be treated as admission capacity.
*/
readonly retryable: boolean | undefined;
readonly recovery: ResourceConstraintRecovery;
constructor(
readonly operation: string,
@@ -55,36 +48,79 @@ export class AnalysisResourceConstraintError extends AnalysisError {
options?: AnalysisErrorOptions & ResourceConstraintRecovery,
) {
super(`Resource constraint during ${operation}: ${reason}`, options);
this.remediationAction = options?.remediationAction;
this.fixedAllowance = options?.fixedAllowance;
this.retryable = options?.retryable;
this.recovery = resourceConstraintRecoveryFrom(options);
}
/** Projection of `recovery`. Absent when recovery names a fixed allowance. */
get remediationAction(): string | undefined {
return this.recovery.remediationAction;
}
/**
* Projection of `recovery`. The fixed format, decoder, or capture allowance
* that refused the request. Worker limits and host resources do not raise
* it, so recovery derives from this allowance rather than from `resource`.
*/
get fixedAllowance(): FixedAllowance | undefined {
return this.recovery.fixedAllowance;
}
/**
* Projection of `recovery`. Whether retrying the same request can succeed.
* A caller-selected bound is deterministic for the same input, so it is not
* retryable even when its resource kind would otherwise be treated as
* admission capacity.
*/
get retryable(): boolean | undefined {
return this.recovery.retryable;
}
}
const resourceConstraintRecoveryFrom = (
options: ResourceConstraintRecovery | undefined,
): ResourceConstraintRecovery => {
if (options?.fixedAllowance !== undefined)
return {
fixedAllowance: options.fixedAllowance,
...(options.retryable === undefined
? {}
: { retryable: options.retryable }),
};
return {
...(options?.remediationAction === undefined
? {}
: { remediationAction: options.remediationAction }),
...(options?.retryable === undefined
? {}
: { retryable: options.retryable }),
};
};
/** Carry one refusal's recovery to an error re-attributed or redacted elsewhere. */
export const resourceConstraintRecovery = (
error: AnalysisResourceConstraintError,
text: (value: string) => string = (value) => value,
): ResourceConstraintRecovery =>
error.fixedAllowance !== undefined
): ResourceConstraintRecovery => {
const recovery = error.recovery;
return recovery.fixedAllowance !== undefined
? {
fixedAllowance: {
name: text(error.fixedAllowance.name),
kind: error.fixedAllowance.kind,
name: text(recovery.fixedAllowance.name),
kind: recovery.fixedAllowance.kind,
},
...retryable(error),
...(recovery.retryable === undefined
? {}
: { retryable: recovery.retryable }),
}
: {
...(error.remediationAction === undefined
...(recovery.remediationAction === undefined
? {}
: { remediationAction: text(error.remediationAction) }),
...retryable(error),
: { remediationAction: text(recovery.remediationAction) }),
...(recovery.retryable === undefined
? {}
: { retryable: recovery.retryable }),
};
const retryable = (
error: AnalysisResourceConstraintError,
): { readonly retryable?: boolean } =>
error.retryable === undefined ? {} : { retryable: error.retryable };
};
/** Producer-specific recovery, or a fixed allowance whose recovery is derived. */
export type ResourceConstraintRecovery =
+59 -1
View File
@@ -1,5 +1,8 @@
import { describe, expect, it } from "vitest";
import { AnalysisResourceConstraintError } from "./analysisErrorCore.js";
import {
AnalysisResourceConstraintError,
resourceConstraintRecovery,
} from "./analysisErrorCore.js";
import { projectAnalysisError } from "./analysisErrorProjection.js";
import {
fixedAllowanceOptions,
@@ -25,6 +28,61 @@ const forbidden = {
work: [/worker CPU limits and observed signal/u],
} as const;
describe("resource constraint recovery shape", () => {
it("keeps a fixed allowance and a remediation action from co-occurring", () => {
const allowance = new AnalysisResourceConstraintError(
"inspect_fixture",
"memory",
"allowance_exceeded",
null,
fixedAllowanceOptions("parser_allowance", "parser"),
);
expect(allowance.recovery).toEqual({
retryable: false,
fixedAllowance: { name: "parser_allowance", kind: "parser" },
});
expect("remediationAction" in allowance.recovery).toBe(false);
const guided = new AnalysisResourceConstraintError(
"inspect_fixture",
"capacity",
"slots_occupied",
null,
{ remediationAction: "wait_for_slot", retryable: true },
);
expect(guided.recovery).toEqual({
remediationAction: "wait_for_slot",
retryable: true,
});
expect("fixedAllowance" in guided.recovery).toBe(false);
expect(resourceConstraintRecovery(guided)).toEqual(guided.recovery);
});
it("keeps only the fixed allowance when both recovery fields are supplied", () => {
const conflict = {
fixedAllowance: { name: "parser_allowance", kind: "parser" as const },
remediationAction: "wait_for_slot",
retryable: false as const,
};
const error = new AnalysisResourceConstraintError(
"inspect_fixture",
"memory",
"allowance_exceeded",
null,
// The public options type cannot express both fields. This is the
// runtime object the constructor still has to normalize.
conflict as never,
);
expect(error.recovery).toEqual({
fixedAllowance: { name: "parser_allowance", kind: "parser" },
retryable: false,
});
expect(error.fixedAllowance).toEqual(conflict.fixedAllowance);
expect(error.remediationAction).toBeUndefined();
expect(resourceConstraintRecovery(error)).toEqual(error.recovery);
});
});
describe("fixed allowance recovery", () => {
it.each([
["parser", "memory"],