diff --git a/src/config/nativeMatchingAllowance.test.ts b/src/config/nativeMatchingAllowance.test.ts index 044717bc50..218c2c188c 100644 --- a/src/config/nativeMatchingAllowance.test.ts +++ b/src/config/nativeMatchingAllowance.test.ts @@ -1,3 +1,4 @@ +import { projectAnalysisError } from "../domain/analysisErrorProjection.js"; import { expect, it } from "vitest"; import { ConfigurationError } from "../domain/configurationErrors.js"; import { parseEnvironment } from "./environment.js"; @@ -76,3 +77,19 @@ it("refuses invalid launch values instead of falling back to the default", () => }); } }); + +it("advertises current-session repair for an invalid native matching launch seed", () => { + const parsed = parseNativeMatchingAllowance({ + [NATIVE_MATCHING_ALLOWANCE_SETTING]: "0", + }); + if (parsed.ok) throw new Error("Invalid seed was accepted"); + const projection = projectAnalysisError(parsed.error); + expect(projection.code).toBe("configuration_invalid"); + expect(projection.remediation.action).toContain("update_analysis_settings"); + expect(projection.remediation.action).toContain( + "native.matching_representation_bytes", + ); + expect(projection.remediation.action).toContain( + NATIVE_MATCHING_ALLOWANCE_SETTING, + ); +}); diff --git a/src/config/nativeMatchingAllowance.ts b/src/config/nativeMatchingAllowance.ts index 55b5ea91e8..74f8906848 100644 --- a/src/config/nativeMatchingAllowance.ts +++ b/src/config/nativeMatchingAllowance.ts @@ -47,6 +47,7 @@ export const parseNativeMatchingAllowance = ( ? ok({ maximumBytes: parsed.data, source: "setting" }) : err( new ConfigurationError(nativeMatchingRepresentationBytesConstraint, { + remediationAction: `On a running MCP session, call update_analysis_settings with ${JSON.stringify({ changes: [{ setting: "native.matching_representation_bytes", action: "set", value: DEFAULT_NATIVE_MATCHING_REPRESENTATION_BYTES }] })}, then retry matching. This repairs the current session for the next operation. For CLI or future REA processes, correct ${NATIVE_MATCHING_ALLOWANCE_SETTING} in the launch environment.`, settings: [ { setting: NATIVE_MATCHING_ALLOWANCE_SETTING, diff --git a/src/domain/analysisErrorPresentation.ts b/src/domain/analysisErrorPresentation.ts index 1cc6317db9..1297e104fd 100644 --- a/src/domain/analysisErrorPresentation.ts +++ b/src/domain/analysisErrorPresentation.ts @@ -158,8 +158,13 @@ export const analysisErrorRemediationAction = ( return "Check the current process's read access to the selected path. Retry with a readable local file."; if (error instanceof AnalysisArtifactChangedError) return "Wait until the selected file is stable. For an active binary session, reopen the target with open_binary before retrying so REA acquires its current identity; for a CLI command or target-free tool, rerun the operation."; - if (error instanceof ConfigurationError && error.settings.length > 0) - return `Correct ${[...new Set(error.settings.map(({ setting }) => setting))].join(", ")} in the environment that starts REA (your shell or the MCP client's registration), then rerun.`; + if (error instanceof ConfigurationError) + return ( + error.remediationAction ?? + (error.settings.length > 0 + ? `Correct ${[...new Set(error.settings.map(({ setting }) => setting))].join(", ")} in the environment that starts REA (your shell or the MCP client's registration), then rerun.` + : analysisErrorUserMessage(error)) + ); if (error instanceof AnalysisInputError) return ( error.remediationAction ?? diff --git a/src/domain/analysisErrorProjection.test.ts b/src/domain/analysisErrorProjection.test.ts index 74cbe5b89e..b9030cfd9a 100644 --- a/src/domain/analysisErrorProjection.test.ts +++ b/src/domain/analysisErrorProjection.test.ts @@ -9,7 +9,10 @@ import { AnalysisResourceConstraintError, } from "./analysisErrorCore.js"; import { ArtifactOperationError } from "./artifactOperationError.js"; -import { BinaryTargetError } from "./configurationErrors.js"; +import { + BinaryTargetError, + ConfigurationError, +} from "./configurationErrors.js"; import { BrowserObservationError } from "./browserObservationError.js"; import { EvidenceFileError, EvidenceIntegrityError } from "./evidenceErrors.js"; import { @@ -1170,3 +1173,24 @@ it("does not offer entry-policy retries for integrity refusals without a declare expect(native.message).not.toContain("record-and-continue"); expect(native.remediation.action).not.toContain("record-and-continue"); }); + +it("preserves ordinary configuration recovery and projects a producer-owned session repair", () => { + const settings = [{ setting: "REA_EXAMPLE", constraint: "must be positive" }]; + const original = projectAnalysisError( + new ConfigurationError("Invalid setting", { settings }), + ); + const repairing = projectAnalysisError( + new ConfigurationError("Invalid setting", { + settings, + remediationAction: "Repair the running session, then retry.", + }), + ); + expect(original.remediation.action).toBe( + "Correct REA_EXAMPLE in the environment that starts REA (your shell or the MCP client's registration), then rerun.", + ); + expect(repairing).toEqual({ + ...original, + remediation: { action: "Repair the running session, then retry." }, + }); + expect(repairing.code).toBe("configuration_invalid"); +}); diff --git a/src/domain/configurationErrors.ts b/src/domain/configurationErrors.ts index 686e5c42ff..a2bbc25153 100644 --- a/src/domain/configurationErrors.ts +++ b/src/domain/configurationErrors.ts @@ -12,14 +12,18 @@ export interface ConfigurationSettingIssue { /** Rejected settings retained for diagnostics; their values are not. */ export interface ConfigurationErrorOptions extends AnalysisErrorOptions { readonly settings?: readonly ConfigurationSettingIssue[]; + /** Producer-owned recovery when this setting can be repaired in a running session. */ + readonly remediationAction?: string; } /** Runtime configuration could not be parsed safely. */ export class ConfigurationError extends AnalysisError { readonly _tag = "ConfigurationError"; readonly settings: readonly ConfigurationSettingIssue[]; + readonly remediationAction: string | undefined; constructor(message: string, options?: ConfigurationErrorOptions) { super(message, options); + this.remediationAction = options?.remediationAction; this.settings = (options?.settings ?? []).map( ({ setting, constraint }) => ({ setting, constraint }), ); diff --git a/tests/acceptance/analysisSettingsMcp.test.ts b/tests/acceptance/analysisSettingsMcp.test.ts index 53f8cbad0e..2f2b9e4120 100644 --- a/tests/acceptance/analysisSettingsMcp.test.ts +++ b/tests/acceptance/analysisSettingsMcp.test.ts @@ -266,6 +266,9 @@ it("lowers, restores and seeds the native matching allowance over MCP", async () if (text?.type !== "text") throw new Error("Missing projected error"); expect(JSON.parse(text.text).error).toMatchObject({ code: "configuration_invalid", + remediation: { + action: expect.stringContaining("update_analysis_settings"), + }, details: { settings: [{ setting: "REA_NATIVE_MATCHING_REPRESENTATION_BYTES" }], },