Files
reverse-engineering-anything/src/domain/analysisErrorPresentation.ts
T
Kaoru0822andClaude Opus 5.5 710fad8acf feat(evidence): discover missing Evidence links without transferring records (#8633)
* feat(evidence): discover missing Evidence links without transferring records

An agent could only learn that a retained record's source was absent by reading
complete records or by having a downstream analysis refuse. The summary said
nothing about links, and complete-record retention was easy to mistake for
source closure.

- Add detail: links to get_evidence_bundle and CLI inspect-evidence-bundle:
  paged link rows with retained or missing status and no record payloads, for
  selected records against the whole session or file.
- Add retained-closure traversal, the walk composed workflows require, reporting
  complete or incomplete. Direct reads make no closure claim.
- Count direct and missing direct links in the summary.

Fixes #8454

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(evidence): count a repeated Evidence link once in every delivery

The Evidence schema does not deduplicate evidence_links, so the summary counted
a repeated link twice while link rows and counts reported it once. Both now read
one helper for a record's distinct direct links.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(evidence): validate link delivery against the advertised schemas

The test never called tools/list, so neither the shared validator nor the MCP
client checked its replies against what each profile advertises. A schema that
rejected the links result could have passed. Fetch the advertised definition and
validate every request and reply against it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 05:14:34 -08:00

740 lines
35 KiB
TypeScript

import { describeResidualUnknownGraphConstraint } from "./residualUnknownGraphError.js";
import {
AnalysisAccessDeniedError,
AnalysisArtifactChangedError,
AnalysisCapabilityUnavailableError,
AnalysisInputError,
AnalysisOutputError,
AnalysisUnsupportedTargetError,
AnalysisResourceConstraintError,
AnalysisTimeoutError,
} from "./analysisErrorCore.js";
import { ArtifactOperationError } from "./artifactOperationError.js";
import { OPENABLE_APPLE_BUNDLE_TYPES_TEXT } from "./apple/appleBundleTypes.js";
import {
BinaryTargetError,
ConfigurationError,
NoBinaryOpenError,
} from "./configurationErrors.js";
import { BrowserObservationError } from "./browserObservationError.js";
import {
EvidenceFileError,
EvidenceIntegrityError,
EvidenceReferenceError,
AnalysisSnapshotMismatchError,
ResourceSnapshotMismatchError,
} from "./evidenceErrors.js";
import {
HOPPER_STARTUP_TIMEOUT_REMEDIATION,
HopperRemoteError,
HopperProcessError,
HopperStartError,
HopperTimeoutError,
} from "./hopperErrors.js";
import { ProviderSelectionError } from "./providerSelectionError.js";
import { ProviderAdapterError } from "./providerAdapterError.js";
import { UnknownRegistryError } from "./unknownRegistryError.js";
import { providerRetryAction } from "./providerOperationHealth.js";
import { fixedAllowanceRemediation } from "./fixedAllowanceRemediation.js";
import {
primaryAnalysisError,
resolvedAnalysisError,
type AnalysisCallerLifetime,
type AnalysisContinuingCallerGuidance,
type AnalysisError,
type AnalysisErrorTag,
} from "./analysisErrorBase.js";
import type { AnalysisErrorProjection } from "./analysisErrorProjection.js";
/** File export when one MCP frame cannot carry the retained session bundle. */
export const MCP_TRANSPORT_EVIDENCE_EXPORT =
"Export complete retained session evidence through export_evidence_bundle to a caller-selected path, or use complete CLI JSON output. The connection remains usable.";
/** Next step when one MCP frame cannot carry a complete retained session bundle. */
export const MCP_TRANSPORT_EVIDENCE_DISCOVERY = `Discover retained records with get_evidence_bundle and detail: summary, then read selected complete records with detail: records and an exact evidence_id, or inspect compatible records through inspect_analysis_view using an exact Evidence reference. ${MCP_TRANSPORT_EVIDENCE_EXPORT}`;
/** Next step when one MCP frame cannot carry a selected page of complete records. */
export const MCP_TRANSPORT_EVIDENCE_RECORDS = `Request fewer records with get_evidence_bundle detail: records, a smaller limit or an exact evidence_id filter; inspect compatible records in pages through inspect_analysis_view. A single record larger than one response needs export. ${MCP_TRANSPORT_EVIDENCE_EXPORT}`;
/** Next step when one MCP frame cannot carry a selected page of link rows. */
export const MCP_TRANSPORT_EVIDENCE_LINKS =
"Request fewer rows with get_evidence_bundle detail: links, a smaller limit or an exact evidence_id filter, then follow next_offset.";
/** Name the next step that is true for how long the caller keeps REA's runtime and session. */
export const analysisErrorRemediationAction = (
error: AnalysisError,
callerLifetime: AnalysisCallerLifetime = "continuing",
): string => {
error = resolvedAnalysisError(error);
const continuing =
callerLifetime === "continuing"
? error.continuingCallerGuidance?.remediationAction
: undefined;
if (continuing !== undefined) return continuing;
if (
(error instanceof ProviderAdapterError ||
error instanceof AnalysisCapabilityUnavailableError) &&
error.remediationAction !== undefined
)
return error.remediationAction;
if (
error instanceof BrowserObservationError &&
error.remediationAction !== undefined
)
return error.remediationAction;
if (
error instanceof BrowserObservationError &&
error.reason === "service_worker_blocked"
)
return "Set environment.service_workers to allow to permit this capture, or remove the service worker registration through the browser. REA never removes existing registrations.";
if (error instanceof AnalysisCapabilityUnavailableError && error.retryable)
return "Review details.reason, then retry the same analysis after the condition clears.";
if (error instanceof EvidenceFileError && error.published !== undefined)
return error.published.path === undefined
? "Resolve the reported staging cleanup failure. Publication completed; its path identity is excluded."
: "Use the published file at details.published_path and resolve the reported staging cleanup failure.";
if (
error instanceof ProviderSelectionError &&
error.remediationAction !== undefined
)
return error.remediationAction;
if (error instanceof ResourceSnapshotMismatchError)
return error.operation === "export"
? "Call close_binary without snapshot_path to close this resource-only session."
: "Open an executable analysis target before importing a binary analysis snapshot.";
if (error instanceof AnalysisSnapshotMismatchError)
return "Select an artifact, provider, and analysis profile matching the snapshot, or run analysis without it and save a fresh snapshot for the intended binding.";
if (error instanceof AnalysisUnsupportedTargetError)
return (
error.remediationAction ??
"Select a target supported by this operation or choose an operation supporting the reported target format."
);
if (error instanceof AnalysisResourceConstraintError)
return (
error.remediationAction ??
(error.fixedAllowance !== undefined
? fixedAllowanceRemediation(
error.fixedAllowance.name,
error.fixedAllowance.kind,
)
: error.resource === "capacity"
? "Wait for an active analysis to complete, then retry within the reported admission limits."
: error.resource === "transport"
? MCP_TRANSPORT_EVIDENCE_DISCOVERY
: error.resource === "cpu"
? "Review the reported worker CPU limits and observed signal. Retry with sufficient CPU time or a smaller artifact; REA retains tighter inherited limits."
: error.resource === "file-size"
? "Review the reported worker file-size limits and write failure. Retry with a sufficient file-size allowance for the evidence reply; REA retains tighter inherited limits."
: "Review the reported worker memory limits and available host memory. Retry with sufficient memory or a smaller artifact; REA retains tighter inherited limits.")
);
if (
error instanceof AnalysisTimeoutError &&
error.remediationAction !== undefined
)
return error.remediationAction;
if (error instanceof AnalysisTimeoutError) return timeoutRemediation(error);
if (
error instanceof ArtifactOperationError &&
error.reason === "limit" &&
error.boundary === "capacity"
)
return "Use an artifact within the capacity reported in details.detail. Changing output detail or retrying the unchanged input does not remove this boundary.";
if (error instanceof HopperTimeoutError)
return error.operation === undefined
? HOPPER_STARTUP_TIMEOUT_REMEDIATION
: error.providerState === "busy"
? "Check binary_session.analysis_activity, wait for the active Hopper request to finish, then retry."
: "Check binary_session for Hopper health, then retry the operation.";
if (error instanceof HopperProcessError)
return hopperProcessRemediation(error);
if (error instanceof HopperStartError)
return error.ownerRunId === undefined
? "Check the Hopper launcher and target details, then retry opening the target."
: `Use the active REA session ${error.ownerRunId} or close it before opening this target again.`;
if (error instanceof HopperRemoteError)
return error.diagnosticType === "invalid_request"
? "Correct the reported address, document, or arguments and retry."
: "Review the Hopper diagnostic details and correct the request or target; run `rea doctor` if the provider itself is failing. Repeating the unchanged request is not expected to succeed.";
if (error instanceof EvidenceReferenceError)
return error.reason === "missing" &&
error.referenceScope === "retained-closure"
? "Import the originating Evidence bundle with the selected records and their complete linked sources into the current investigation, or re-run their producers there. Retry after the required records are retained. reset_investigation clears retained records; close_binary preserves them."
: "Use an exact Evidence reference available from the selected source, supply the complete inline Evidence, re-run its producer, or select/import its Evidence bundle. reset_investigation clears retained records; close_binary preserves them.";
if (
error instanceof BinaryTargetError &&
error.constraint === "directory_requires_file"
)
return `For a JavaScript/Electron application directory, call analyze_javascript_application with input_path or run \`rea analyze <directory>\`. For binary analysis, select its executable file or an Apple bundle directory (${OPENABLE_APPLE_BUNDLE_TYPES_TEXT}).`;
if (
error instanceof BinaryTargetError &&
error.constraint === "bundle_type_unsupported"
)
return `Select the bundle's program file directly, or open a bundle directory of a supported type (${OPENABLE_APPLE_BUNDLE_TYPES_TEXT}).`;
if (
error instanceof BinaryTargetError &&
(error.constraint === "bundle_executable_undeclared" ||
error.constraint === "bundle_program_missing") &&
error.infoPlist !== undefined
)
return `Read ${error.infoPlist} with inspect_plist (\`rea inspect-plist <path>\`) to review its CFBundleExecutable, then select an existing program file directly.`;
if (
error instanceof BinaryTargetError &&
(error.constraint === "bundle_info_plist_missing" ||
error.constraint === "bundle_layout_ambiguous" ||
error.constraint === "bundle_current_version_missing" ||
error.constraint === "bundle_path_escapes")
)
return "Review the bundle paths listed in details, then select the bundle's program file directly or repair the bundle layout.";
if (
error instanceof BinaryTargetError &&
error.constraint === "load_selection_rejected"
)
return "Correct the reported load argument (--target-format, --architecture, or --entry-point on the CLI; format, architecture, or entry_point through MCP) and retry.";
if (
error instanceof BinaryTargetError &&
error.constraint === "file_format_rejected"
)
return "Review the reported file-format reason and select a supported, well-formed target.";
if (
error instanceof BinaryTargetError &&
error.constraint === "wasm_requires_static_inspection"
)
return `For static inspection, call inspect_wasm_artifact with ${JSON.stringify({ path: error.path })} or run \`rea inspect-wasm-artifact <path> --format json\`. This separate lane requires caller-supplied WABT 1.0.42 via absolute REA_WABT_BIN_DIRECTORY; check its availability in binary_session. REA does not execute WASM or install tools, and the header alone does not establish that the complete module is valid.`;
if (
error instanceof AnalysisAccessDeniedError ||
(error instanceof BinaryTargetError && error.systemCode !== undefined)
)
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)
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 ??
`Read the declared input schema or CLI usage for ${error.operation}; if using MCP and the catalog may be stale, call tools/list. Correct the listed fields, then retry. Do not repeat the unchanged call.`
);
if (
error instanceof AnalysisOutputError &&
error.remediationAction !== undefined
)
return error.remediationAction;
if (
error instanceof UnknownRegistryError &&
error.reason === "constraint" &&
error.constraint !== undefined
) {
const suffix = " Do not repeat the unchanged call.";
switch (error.constraint.kind) {
case "missing_evidence":
return (
"Use an Evidence ID retained in this session; obtain or import the missing Evidence before referencing it." +
suffix
);
case "missing_unknown":
return (
"Use an unknown_id retained in this session; create or import the related unknown before referencing it." +
suffix
);
case "resolution_unproven":
return (
"Supply supporting non-mutation Evidence matching the unknown's required authority, confidence and environment, or withdraw the unsupported resolution." +
suffix
);
case "contradicting_evidence":
return (
"Reconcile the contradicting Evidence before claiming a verified resolution." +
suffix
);
case "dependency_cycle":
return (
"Correct the depends-on relationships to remove the dependency cycle." +
suffix
);
case "history_gap":
case "revision_chain":
break;
}
}
if (error instanceof UnknownRegistryError && error.reason === "not-found")
return "Check that the unknown_id belongs to this session, then retry.";
return analysisErrorUserMessage(error);
};
export const analysisErrorCategory = (
error: AnalysisError,
): AnalysisErrorProjection["category"] => {
error = primaryAnalysisError(error);
if (error instanceof AnalysisCapabilityUnavailableError && error.retryable)
return "unavailable";
if (
error instanceof BinaryTargetError &&
error.constraint === "load_selection_rejected"
)
return "invalid_input";
if (
error instanceof BinaryTargetError &&
(error.constraint === "wasm_requires_static_inspection" ||
error.constraint === "file_format_rejected" ||
error.constraint === "bundle_type_unsupported")
)
return "unsupported_target";
if (
error instanceof ProviderSelectionError &&
error.reason === "provider_unavailable"
)
return "unavailable";
if (error._tag === "ProcessCaptureError")
return error.userCategory ?? "execution_failure";
if (error instanceof BrowserObservationError)
return error.userCategory ?? browserErrorCategory(error.reason);
if (
error instanceof HopperRemoteError &&
error.diagnosticType === "invalid_request"
)
return "invalid_input";
if (
error instanceof HopperRemoteError &&
error.diagnosticType === "authorization"
)
return "unavailable";
if (error instanceof HopperProcessError || error instanceof HopperStartError)
return "unavailable";
if (error instanceof ArtifactOperationError)
return artifactErrorCategory(error.reason);
if (
error instanceof EvidenceFileError &&
(error.reason === "path-limit" || error.reason === "storage-limit")
)
return "resource_constraint";
if (
error instanceof EvidenceFileError &&
(error.reason === "missing" ||
error.reason === "symlink-traversal" ||
error.reason === "not-file" ||
error.reason === "wrong-kind" ||
error.reason === "exists")
)
return "invalid_input";
if (
error instanceof UnknownRegistryError &&
(error.reason === "already-exists" || error.reason === "constraint")
)
return "invalid_input";
return STATIC_ERROR_CATEGORIES[error._tag] ?? "execution_failure";
};
const browserErrorCategory = (
reason: BrowserObservationError["reason"],
): AnalysisErrorProjection["category"] => {
if (reason === "cancelled") return "cancelled";
if (reason === "timeout") return "timeout";
if (reason === "payload_limit") return "truncated";
if (
reason === "target_not_found" ||
reason === "target_not_allowed" ||
reason === "service_worker_blocked" ||
reason === "target_changed" ||
reason === "target_session_detached" ||
reason === "endpoint_unreachable" ||
reason === "disconnected"
)
return "unavailable";
return "execution_failure";
};
const artifactErrorCategory = (
reason: ArtifactOperationError["reason"],
): AnalysisErrorProjection["category"] => {
if (reason === "integrity") return "integrity_mismatch";
if (reason === "limit") return "truncated";
if (reason === "cancelled") return "cancelled";
if (reason === "unavailable") return "unavailable";
return "execution_failure";
};
const STATIC_ERROR_CATEGORIES: Readonly<
Partial<Record<AnalysisErrorTag, AnalysisErrorProjection["category"]>>
> = {
ConfigurationError: "invalid_input",
AnalysisInputError: "invalid_input",
AnalysisAccessDeniedError: "unavailable",
AnalysisArtifactChangedError: "integrity_mismatch",
AnalysisCapabilityUnavailableError: "unsupported_provider",
AnalysisUnsupportedTargetError: "unsupported_target",
ProviderSelectionError: "unsupported_provider",
EvidenceIntegrityError: "integrity_mismatch",
AnalysisCancelledError: "cancelled",
HopperCancelledError: "cancelled",
AnalysisTimeoutError: "timeout",
AnalysisResourceConstraintError: "resource_constraint",
HopperTimeoutError: "timeout",
NoBinaryOpenError: "unavailable",
BinaryTargetError: "unavailable",
};
/** Describe the request's outcome, adding continuing-caller facts only for that caller. */
export const analysisErrorUserMessage = (
error: AnalysisError,
callerLifetime: AnalysisCallerLifetime = "continuing",
): string => {
const { outcome, continuing } = analysisErrorUserMessageParts(error);
return callerLifetime === "invocation" || continuing === undefined
? outcome
: `${outcome} ${continuing}`;
};
/** The outcome every caller receives and the addition only a continuing caller does. */
const analysisErrorUserMessageParts = (
error: AnalysisError,
): { readonly outcome: string; readonly continuing?: string } => {
error = resolvedAnalysisError(error);
const outcome = outcomeUserMessage(error);
const continuing = error.continuingCallerGuidance?.userMessage;
return continuing === undefined ? { outcome } : { outcome, continuing };
};
/**
* Re-attribute one failure's message and another's remediation to a composed
* error, keeping the form each caller lifetime receives.
*/
export const composedCallerGuidance = (sources: {
readonly message?: AnalysisError;
readonly remediation: AnalysisError;
}): {
readonly userMessage?: string;
readonly remediationAction: string;
readonly continuingCallerGuidance?: AnalysisContinuingCallerGuidance;
} => {
const message =
sources.message === undefined
? undefined
: analysisErrorUserMessageParts(sources.message);
const remediationAction = analysisErrorRemediationAction(
sources.remediation,
"invocation",
);
const continuingRemediation = analysisErrorRemediationAction(
sources.remediation,
);
const continuingCallerGuidance = {
...(message?.continuing === undefined
? {}
: { userMessage: message.continuing }),
...(continuingRemediation === remediationAction
? {}
: { remediationAction: continuingRemediation }),
};
return {
...(message === undefined ? {} : { userMessage: message.outcome }),
remediationAction,
...(Object.keys(continuingCallerGuidance).length === 0
? {}
: { continuingCallerGuidance }),
};
};
const outcomeUserMessage = (error: AnalysisError): string => {
if (error instanceof AnalysisSnapshotMismatchError) return error.message;
if (error instanceof AnalysisUnsupportedTargetError) return error.message;
if (error instanceof AnalysisResourceConstraintError) return error.reason;
if (error instanceof AnalysisAccessDeniedError)
return "Host filesystem permissions denied read access to the selected path.";
if (error instanceof AnalysisArtifactChangedError)
return "The selected artifact changed during acquisition; no stable snapshot was decoded.";
if (error instanceof AnalysisInputError)
return (
error.userMessage ??
"Analysis input is invalid. Check the arguments and try again."
);
if (error instanceof AnalysisTimeoutError)
return error.userMessage ?? timeoutUserMessage(error);
const hopperMessage = hopperErrorUserMessage(error);
if (hopperMessage !== undefined) return hopperMessage;
if (error.userMessage !== undefined) return error.userMessage;
if (error instanceof AnalysisCapabilityUnavailableError && error.retryable)
return "Required runtime controls are unavailable for this attempt.";
const standardMessage = standardErrorMessage(error._tag);
if (standardMessage !== undefined) return standardMessage;
if (error instanceof ArtifactOperationError) return artifactMessage(error);
if (error instanceof EvidenceReferenceError)
return error.reason === "missing"
? error.referenceScope === "retained-closure"
? `Evidence ${error.evidenceId} is missing from the required retained Evidence closure in the current investigation. Import its originating bundle or re-create the required observations before retrying.`
: `Evidence ${error.evidenceId} is not available from the selected source. Supply complete inline Evidence, re-run its producer, or select/import its bundle before using this reference.`
: `Evidence ${error.evidenceId} does not match the requested reference (${error.reason}). Check the expected and actual identity in the diagnostic details.`;
if (error instanceof EvidenceIntegrityError)
return "Evidence is invalid or has changed. Recreate or re-import it, then try again.";
if (error instanceof EvidenceFileError) return evidenceFileMessage(error);
if (
error instanceof UnknownRegistryError &&
error.reason === "already-exists"
)
return "This residual unknown already exists in the session. Reuse it or call update_unknown with its current expected_revision; do not repeat record_unknown.";
if (error instanceof UnknownRegistryError && error.reason === "not-found")
return "The requested residual unknown does not exist in this session. Check the unknown_id and try again.";
if (error instanceof UnknownRegistryError && error.constraint !== undefined)
return describeResidualUnknownGraphConstraint(error.constraint);
if (error instanceof UnknownRegistryError)
return "Evidence state changed before the update completed. Refresh the current state and try again.";
if (error instanceof ConfigurationError)
return error.settings.length === 0
? "REA configuration is invalid. Run `rea doctor` and fix the reported setting."
: `REA configuration is invalid: ${error.settings
.map(({ setting, constraint }) =>
constraint.includes(setting)
? constraint.replace(/\.$/u, "")
: `${setting}: ${constraint}`,
)
.join("; ")}.`;
if (error instanceof NoBinaryOpenError) return error.message;
if (
error instanceof BinaryTargetError &&
error.constraint === "wasm_requires_static_inspection"
)
return `${error.message}. Static WebAssembly inspection uses a separate optional WABT lane, not a native analysis engine.`;
if (error instanceof BinaryTargetError)
return error.constraint === "directory_requires_file"
? `open_binary accepts files and Apple bundle directories (${OPENABLE_APPLE_BUNDLE_TYPES_TEXT}). This target is another directory; JavaScript/Electron application directories can be analyzed directly with analyze_javascript_application or \`rea analyze <directory>\`.`
: error.constraint === undefined
? `${error.message}. Check that the path exists, is readable, and points to a supported file.`
: `${error.message}.`;
if (error._tag === "ProcessCaptureError")
return (
error.userMessage ??
"Process capture could not complete. Run `rea doctor`, then review capture policy and try again."
);
return "Analysis could not complete. Run `rea doctor`, then try again.";
};
const hopperErrorUserMessage = (error: AnalysisError): string | undefined => {
if (error instanceof HopperTimeoutError) {
if (error.operation === undefined)
return "REA timed out waiting for Hopper bridge readiness, which includes initial analysis. Hopper may still be analyzing the target or waiting for a loader, demo, or license dialog. Review the captured launcher outcome and resolve retained cleanup before retrying.";
const request = error.operation;
return error.providerState === "busy"
? `Hopper timed out during ${request} while the provider remained busy. Check binary_session.analysis_activity, wait for the active request to finish, then retry.`
: `Hopper timed out during ${request} before it started. Check binary_session for provider health, then retry.`;
}
if (error instanceof HopperProcessError) return hopperProcessMessage(error);
if (error instanceof HopperStartError)
return (
error.userMessage ??
"Hopper could not start. Check the launcher and target details, then retry opening the target."
);
if (error instanceof HopperRemoteError)
return `Hopper ${error.operation ?? "analysis"} failed (${String(error.code)}, ${error.diagnosticType}): ${error.safeMessage}`;
return undefined;
};
const RESTART_OWNED_PROVIDER =
"Restart the owned provider by calling close_binary, then open the target again and retry.";
const RESTART_UNREACHABLE_HOPPER = `${RESTART_OWNED_PROVIDER} close_binary preserves retained Evidence and Unknown history on this connection; export_evidence_bundle before ending the REA process if you need to resume later.`;
const hopperProcessRemediation = (error: HopperProcessError): string => {
if (error.failureCode !== undefined && error.userMessage !== undefined)
return error.userMessage;
if (error.stage === "launch")
return "Review the captured launcher diagnostics and Hopper setup, then retry opening the target.";
if (error.providerState === "unreachable") return RESTART_UNREACHABLE_HOPPER;
if (providerRetryAction(error.providerState) === "restart_provider")
return RESTART_OWNED_PROVIDER;
return "Read binary_session.provider_operation_health for this request before retrying or restarting the owned provider.";
};
const hopperProcessMessage = (error: HopperProcessError): string => {
const stage = error.stage;
const where = error.operation ?? stage;
const request =
error.requestId === undefined
? ""
: ` Request id ${String(error.requestId)}.`;
const startup =
error.userMessage === undefined ? "" : ` ${error.userMessage}`;
if (error.providerState === "exited") {
const signal = error.launcherFailure?.signal;
const exit =
error.exitCode === null
? signal === undefined || signal === null
? "no exit code was observed"
: `signal ${signal}`
: `exit code ${String(error.exitCode)}`;
const recovery =
error.failureCode === undefined && stage !== "launch"
? ` ${RESTART_OWNED_PROVIDER}`
: "";
return `Hopper exited during ${where} (${exit}).${startup}${recovery}${request}`;
}
if (error.providerState === "unreachable")
return `Hopper became unreachable during ${where}; its process exit was not observed. ${RESTART_UNREACHABLE_HOPPER}${startup}${request}`;
return `Hopper failed during ${where} and REA could not determine whether the provider is busy, exited, or unreachable.${startup} Read binary_session.provider_operation_health for this request before retrying or restarting.${request}`;
};
const standardErrorMessage = (tag: AnalysisErrorTag): string | undefined => {
if (UNREADABLE_OUTPUT_TAGS.has(tag))
return "Analysis returned an unreadable result. Run `rea doctor` and review the reported details; repeating the unchanged request is not expected to succeed.";
if (UNSUPPORTED_PROVIDER_TAGS.has(tag))
return "This analysis is unavailable for the current target. Choose another analysis or target.";
if (CANCELLED_TAGS.has(tag))
return "Analysis was cancelled. Start it again when ready.";
if (TIMEOUT_TAGS.has(tag))
return "Analysis took too long. Try a smaller request, then run `rea doctor` if it continues.";
if (ADAPTER_FAILURE_TAGS.has(tag))
return "Analysis could not complete. Run `rea doctor` and review the reported details; repeating the unchanged request is not expected to succeed.";
if (START_FAILURE_TAGS.has(tag))
return "Analysis could not start or stopped unexpectedly. Run `rea doctor`, then try again.";
return undefined;
};
const timeoutUserMessage = (error: AnalysisTimeoutError): string =>
`REA's ${error.operation} operation reached its ${String(error.timeoutMs)}ms server/provider deadline. This is not a client-side timeout; extending only the MCP client's deadline does not change this budget.`;
const timeoutRemediation = (error: AnalysisTimeoutError): string =>
`Inspect any partial Evidence, provider diagnostics and reported session/provider cleanup state for ${error.operation}. Reduce the request if supported, or resolve the reported provider condition before retrying. Do not repeat the unchanged call or retry while work or cleanup may still be active.`;
const UNREADABLE_OUTPUT_TAGS: ReadonlySet<AnalysisErrorTag> = new Set([
"AnalysisProtocolError",
"AnalysisOutputError",
"HopperProtocolError",
]);
const UNSUPPORTED_PROVIDER_TAGS: ReadonlySet<AnalysisErrorTag> = new Set([
"AnalysisCapabilityUnavailableError",
"ProviderSelectionError",
]);
const CANCELLED_TAGS: ReadonlySet<AnalysisErrorTag> = new Set([
"AnalysisCancelledError",
"HopperCancelledError",
]);
const TIMEOUT_TAGS: ReadonlySet<AnalysisErrorTag> = new Set([
"AnalysisTimeoutError",
"HopperTimeoutError",
]);
const ADAPTER_FAILURE_TAGS: ReadonlySet<AnalysisErrorTag> = new Set([
"ProviderAdapterError",
"HopperRemoteError",
]);
const START_FAILURE_TAGS: ReadonlySet<AnalysisErrorTag> = new Set([
"HopperProcessError",
"HopperStartError",
]);
/** Operations that accept integrity_policy for declared-integrity mismatches. */
const INTEGRITY_POLICY_OPERATIONS: ReadonlySet<
ArtifactOperationError["operation"]
> = new Set([
"inventory_artifact",
"inspect_artifact",
"extract_artifact",
"analyze_javascript_application",
]);
const artifactMessage = ({
operation,
reason,
artifactDetails: details,
detail,
}: ArtifactOperationError): string => {
if (reason === "cancelled")
return "Artifact operation was cancelled. Start it again when ready.";
if (reason === "limit")
return (
detail ??
"Artifact processing reached a resource limit. Select an artifact within the reported limits and retry."
);
if (reason === "path")
return (
detail ??
"Artifact contains a conflicting internal path. Inspect the reported path before retrying."
);
if (reason === "unavailable" && details?.unpacked === true)
return `ASAR unpacked companion bytes are unavailable for ${details.logicalPath}. Select the archive in place with its .unpacked directory beside it, then retry.`;
if (reason === "unavailable")
return "Artifact processing is unavailable. Review the reported availability reason and resolve that condition before retrying.";
if (
reason === "integrity" &&
details !== undefined &&
INTEGRITY_POLICY_OPERATIONS.has(operation)
)
return `${
details?.unpacked === true &&
"calculatedSha256" in details &&
details.calculatedSha256 !== null
? `Declared ASAR integrity for unpacked entry ${details.logicalPath} contradicts its companion file; packaging tools commonly sign or strip unpacked native binaries after writing the archive.`
: "Artifact bytes contradict declared integrity."
} If expected, rerun ${operation === "inventory_artifact" ? "inspect_artifact" : operation} with integrity_policy=record-and-continue (CLI: --integrity-policy record-and-continue) to retain observed bytes as untrusted; otherwise get a fresh copy.`;
if (reason === "format" || reason === "integrity")
return "Artifact is invalid or has changed. Get a fresh copy and try again.";
return "Artifact could not be read or written. Check file access and try again.";
};
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")
return operation === "read"
? "Evidence file does not exist at the selected path. Check the path and try again."
: "Evidence output directory does not exist. Choose an existing directory and try again.";
if (reason === "not-file")
return "Evidence path does not point to a regular file. Choose a file and try again.";
if (reason === "exists")
return "Evidence file already exists. Choose another path or allow overwrite.";
if (reason === "invalid-json")
return "Evidence file is not valid JSON. Repair or recreate the file and try again.";
return "Evidence file could not be accessed. Check file permissions and try again.";
};
const KNOWN_ERROR_TAGS = {
AnalysisProtocolError: true,
AnalysisInputError: true,
AnalysisAccessDeniedError: true,
AnalysisArtifactChangedError: true,
AnalysisOutputError: true,
AnalysisCapabilityUnavailableError: true,
AnalysisUnsupportedTargetError: true,
AnalysisCancelledError: true,
AnalysisTimeoutError: true,
AnalysisResourceConstraintError: true,
ProviderSelectionError: true,
ProviderAdapterError: true,
BrowserObservationError: true,
ArtifactOperationError: true,
ProcessCaptureError: true,
EvidenceIntegrityError: true,
EvidenceFileError: true,
UnknownRegistryError: true,
HopperTimeoutError: true,
HopperCancelledError: true,
HopperProtocolError: true,
HopperRemoteError: true,
HopperProcessError: true,
HopperStartError: true,
ConfigurationError: true,
NoBinaryOpenError: true,
BinaryTargetError: true,
} as const satisfies Readonly<Record<AnalysisErrorTag, true>>;
export const assertKnownAnalysisErrorTag = (tag: AnalysisErrorTag): void => {
if (KNOWN_ERROR_TAGS[tag] !== true)
throw new TypeError("Unknown analysis error tag");
};