mirror of
https://github.com/morluto/rea.git
synced 2026-11-12 01:41:21 +00:00
* fix: retain address context cleanup failures * fix!: preserve address context input errors BREAKING CHANGE: Strict clients that cached the previous full inspect_address_context output schema must refresh tools/list and accept the typed error field on unavailable facets. * test: use session contract for address context * fix!: preserve address context resource recovery BREAKING CHANGE: Clients validating unavailable context facets against a cached strict full output schema must refresh tools/list to accept resource_constraint errors. * fix: expose authenticated records refused by the evidence ledger (#8426) * fix: preserve completed evidence on retention refusal * fix: preserve context when unretained evidence exceeds delivery limits * fix(mcp): recover oversized context from session evidence * fix(evidence): preserve completed atomic write results * fix: retain completed results when source publication fails * test: cover bounded recovery with unknown retention * fix(evidence): preserve recovery identity and finalization facts * fix(evidence): clarify retention of omitted observations * test(evidence): compare omission guidance with its owner * fix(evidence): preserve completed composed workflows * fix(evidence): clarify unretained recovery export * test(evidence): cover mixed recovery guidance * fix(evidence): preserve caller scope in secondary failures * refactor(evidence): remove superseded cleanup projection * test(evidence): align recovery checks with owners * fix: preserve cleanup on partial recovery limits * test: use owners for diagnostic wording * fix: admit completed analysis before evidence construction * fix: preserve selected paths when reusing evidence * fix(cli): preserve caller path in unretained evidence * fix(evidence): preserve recovery metadata and admission bounds * refactor: consolidate evidence closure imports * style(test): format CLI help default annotations (#9280) * test(mcp): keep recovery checks focused on contract facts * fix(evidence): preserve feature trace replay revision * test: align evidence recovery checks with ownership * test: align partial recovery and cleanup diagnostics * fix(evidence): preserve nested partials during cleanup * fix(evidence): respect outer recovery ownership * fix(capture): discard rejected nested observations * test(frida): assert inline evidence recovery --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com>
431 lines
15 KiB
TypeScript
431 lines
15 KiB
TypeScript
import type {
|
|
AnalysisExecution,
|
|
AnalysisOperation,
|
|
AnalysisOperationPort,
|
|
} from "./AnalysisProvider.js";
|
|
import type { AnalysisError } from "../domain/analysisErrorBase.js";
|
|
import {
|
|
AnalysisCancelledError,
|
|
AnalysisCapabilityUnavailableError,
|
|
AnalysisInputError,
|
|
AnalysisOutputError,
|
|
AnalysisResourceConstraintError,
|
|
} from "../domain/analysisErrorCore.js";
|
|
import {
|
|
omitUnretainedEvidenceFromError,
|
|
projectAnalysisError,
|
|
unretainedEvidenceCandidates,
|
|
type AnalysisErrorProjection,
|
|
} from "../domain/analysisErrorProjection.js";
|
|
import { analysisErrorWithCleanupFailure } from "../domain/analysisErrorCleanup.js";
|
|
import { analysisErrorWithCancellation } from "../domain/analysisErrorCancellation.js";
|
|
import { EvidenceIntegrityError } from "../domain/evidenceErrors.js";
|
|
import type { JsonValue } from "../domain/jsonValue.js";
|
|
import { analysisBookmarkSchema } from "../domain/native/nativeInventory.js";
|
|
import { parseContainingProcedureResolution } from "../domain/native/nativeObservation.js";
|
|
import { nativeAddressCoordinateInputSchema } from "../domain/native/nativeAddress.js";
|
|
import { resolveAnalysisDocument } from "./AnalysisDocument.js";
|
|
import { estimateJsonStorage } from "./JsonAuthenticationHeadroom.js";
|
|
import { err, ok, type Result } from "../domain/result.js";
|
|
|
|
type Facet =
|
|
| { readonly state: "available"; readonly value: JsonValue }
|
|
| {
|
|
readonly state: "unavailable";
|
|
readonly reason: string;
|
|
readonly remediation: string;
|
|
readonly error?: AnalysisErrorProjection;
|
|
};
|
|
|
|
type AddressContext = {
|
|
readonly address: string;
|
|
readonly document: string;
|
|
readonly name: Facet;
|
|
readonly procedure: Facet;
|
|
readonly comment: Facet;
|
|
readonly inline_comment: Facet;
|
|
readonly bookmarks: Facet;
|
|
};
|
|
|
|
/** Facets inspected after an explicit or provider-resolved document is known. */
|
|
export const ADDRESS_CONTEXT_FACET_OPERATIONS = [
|
|
"address_name",
|
|
"resolve_containing_procedure",
|
|
"comment",
|
|
"inline_comment",
|
|
"list_bookmarks",
|
|
] as const satisfies readonly AnalysisOperation[];
|
|
|
|
const parameters = (
|
|
document: string | undefined,
|
|
address?: string,
|
|
): Readonly<Record<string, JsonValue>> => ({
|
|
...(document === undefined ? {} : { document }),
|
|
...(address === undefined ? {} : { address }),
|
|
});
|
|
|
|
const executionOptions = (
|
|
signal: AbortSignal | undefined,
|
|
): { readonly signal?: AbortSignal } =>
|
|
signal === undefined ? {} : { signal };
|
|
|
|
/** Compose the provider's volatile selection state into one coherent query. */
|
|
export const getNavigationContext = async (
|
|
analysis: AnalysisOperationPort,
|
|
input: { readonly document?: string | undefined },
|
|
signal?: AbortSignal,
|
|
): Promise<Result<JsonValue, AnalysisError>> => {
|
|
const document =
|
|
input.document ??
|
|
(await analysis.execute("current_document", {}, executionOptions(signal)));
|
|
if (typeof document !== "string" && !document.ok) return document;
|
|
const documentName =
|
|
typeof document === "string" ? document : document.value.result;
|
|
if (typeof documentName !== "string")
|
|
return err(
|
|
new AnalysisOutputError(
|
|
"current_document",
|
|
"expected a document identity string",
|
|
),
|
|
);
|
|
const documentArgs = parameters(documentName);
|
|
const address = await analysis.execute(
|
|
"current_address",
|
|
documentArgs,
|
|
executionOptions(signal),
|
|
);
|
|
if (!address.ok) return address;
|
|
if (typeof address.value.result !== "string")
|
|
return err(
|
|
new AnalysisOutputError("current_address", "expected an address string"),
|
|
);
|
|
const procedure = await analysis.execute(
|
|
"resolve_containing_procedure",
|
|
parameters(documentName, address.value.result),
|
|
executionOptions(signal),
|
|
);
|
|
if (!procedure.ok) return procedure;
|
|
const resolution = parseContainingProcedureResolution(procedure.value.result);
|
|
if (!resolution.ok) return resolution;
|
|
return ok({
|
|
document: documentName,
|
|
address: address.value.result,
|
|
procedure: resolution.value.found ? resolution.value.procedure.name : null,
|
|
});
|
|
};
|
|
|
|
/** Preserve other facets and smaller partials if unretained Evidence cannot fit. */
|
|
export function* omitContextEvidenceForDelivery(
|
|
context: AddressContext,
|
|
limit: {
|
|
readonly constraint: "receive-buffer" | "string-length";
|
|
readonly bytesAtLeast: number;
|
|
},
|
|
budgetBytes: number,
|
|
): Generator<AddressContext> {
|
|
const names = [
|
|
"name",
|
|
"procedure",
|
|
"comment",
|
|
"inline_comment",
|
|
"bookmarks",
|
|
] as const;
|
|
const candidates = names.flatMap((name) => {
|
|
const facet = context[name];
|
|
if (facet.state !== "unavailable" || facet.error === undefined) return [];
|
|
return [...unretainedEvidenceCandidates(facet.error)].map((candidate) => {
|
|
const size = estimateJsonStorage(candidate.value, {
|
|
maximumValueBytes: budgetBytes,
|
|
includeSerialized: false,
|
|
}).valueBytes;
|
|
return { name, candidate, size };
|
|
});
|
|
});
|
|
candidates.sort((left, right) => right.size - left.size);
|
|
let current = context;
|
|
for (const { name, candidate } of candidates) {
|
|
const facet = current[name];
|
|
if (facet.state !== "unavailable" || facet.error === undefined) continue;
|
|
const compact = omitUnretainedEvidenceFromError(
|
|
facet.error,
|
|
limit,
|
|
candidate,
|
|
);
|
|
if (compact === undefined) continue;
|
|
current = {
|
|
...current,
|
|
[name]: {
|
|
...facet,
|
|
remediation: compact.remediation.action,
|
|
error: compact,
|
|
},
|
|
};
|
|
yield current;
|
|
}
|
|
}
|
|
|
|
/** Inspect bounded provider-neutral facets at one explicit reproducible address. */
|
|
export const inspectAddressContext = async (
|
|
analysis: AnalysisOperationPort,
|
|
input: { readonly address: string; readonly document?: string | undefined },
|
|
signal?: AbortSignal,
|
|
): Promise<Result<AddressContext, AnalysisError>> => {
|
|
const document = await resolveAnalysisDocument(
|
|
analysis,
|
|
input.document,
|
|
signal,
|
|
);
|
|
if (!document.ok) return document;
|
|
const args = parameters(document.value, input.address);
|
|
const operations = ADDRESS_CONTEXT_FACET_OPERATIONS;
|
|
const completed: Result<AnalysisExecution, AnalysisError>[] = [];
|
|
if (signal?.aborted === true)
|
|
return err(
|
|
new AnalysisCancelledError("inspect_address_context", {
|
|
cause: signal.reason,
|
|
}),
|
|
);
|
|
let signalCancellation: AnalysisCancelledError | undefined;
|
|
const cancelled = () => {
|
|
signalCancellation = new AnalysisCancelledError("inspect_address_context", {
|
|
cause: signal?.reason,
|
|
});
|
|
completed.push(err(signalCancellation));
|
|
};
|
|
signal?.addEventListener("abort", cancelled, { once: true });
|
|
let results: Result<AnalysisExecution, AnalysisError>[];
|
|
try {
|
|
results = await Promise.all(
|
|
operations.map(async (operation) => {
|
|
const result = await analysis.execute(
|
|
operation,
|
|
operation === "list_bookmarks" ? parameters(document.value) : args,
|
|
executionOptions(signal),
|
|
);
|
|
completed.push(result);
|
|
return result;
|
|
}),
|
|
);
|
|
} finally {
|
|
signal?.removeEventListener("abort", cancelled);
|
|
}
|
|
const fatal = contextFailure(completed, signalCancellation);
|
|
if (fatal !== undefined) return err(fatal);
|
|
const name = facet(results[0], operations[0]);
|
|
const procedureResult = results[1];
|
|
const resolution = procedureResult?.ok
|
|
? parseContainingProcedureResolution(procedureResult.value.result)
|
|
: procedureResult;
|
|
const procedure: Facet = resolution?.ok
|
|
? { state: "available", value: resolution.value }
|
|
: unavailableFacet(resolution?.error, operations[1]);
|
|
const entry =
|
|
resolution?.ok && resolution.value.found
|
|
? resolution.value.procedure.address
|
|
: undefined;
|
|
const comment = commentFacet(results[2], operations[2], entry);
|
|
const inlineComment = commentFacet(results[3], operations[3], entry);
|
|
const bookmarks = facet(results[4], operations[4]);
|
|
return ok({
|
|
address: input.address,
|
|
document: document.value,
|
|
name,
|
|
procedure,
|
|
comment,
|
|
inline_comment: inlineComment,
|
|
bookmarks:
|
|
bookmarks.state === "available"
|
|
? matchingBookmarks(
|
|
bookmarks.value,
|
|
resolution?.ok ? resolution.value.query_address : input.address,
|
|
)
|
|
: bookmarks,
|
|
});
|
|
};
|
|
|
|
const commentFacet = (
|
|
result: Result<AnalysisExecution, AnalysisError> | undefined,
|
|
operation: "comment" | "inline_comment",
|
|
entry: string | undefined,
|
|
): Facet => {
|
|
if (
|
|
result?.ok === false &&
|
|
result.error instanceof AnalysisCapabilityUnavailableError &&
|
|
!result.error.retryable &&
|
|
result.error.remediationAction === undefined &&
|
|
result.error.continuingCallerGuidance?.remediationAction === undefined &&
|
|
entry !== undefined
|
|
) {
|
|
return {
|
|
state: "unavailable",
|
|
reason: result.error.message,
|
|
remediation: `If analyze_function is available for this same document, call it with ${JSON.stringify({ procedure: entry })}; use its summary's parent_evidence_id with inspect_analysis_view's native comments facet. Check facet availability, pagination, and the exact requested address; an omitted comment does not establish absence.`,
|
|
};
|
|
}
|
|
return facet(result, operation);
|
|
};
|
|
|
|
const facet = (
|
|
result: Result<AnalysisExecution, AnalysisError> | undefined,
|
|
operation: AnalysisOperation,
|
|
): Facet =>
|
|
result?.ok === true
|
|
? { state: "available", value: result.value.result }
|
|
: unavailableFacet(result?.error, operation);
|
|
|
|
const unavailableFacet = (
|
|
error: AnalysisError | undefined,
|
|
operation: AnalysisOperation,
|
|
): Facet => {
|
|
if (error instanceof AnalysisCapabilityUnavailableError) {
|
|
const projected = projectAnalysisError(error);
|
|
return {
|
|
state: "unavailable",
|
|
reason: error.userMessage ?? error.message,
|
|
remediation:
|
|
error.retryable ||
|
|
error.remediationAction !== undefined ||
|
|
error.fallbackRemediationAction !== undefined ||
|
|
error.continuingCallerGuidance?.remediationAction !== undefined
|
|
? projected.remediation.action
|
|
: "Review provider availability in binary_session with detail complete and select another supported provider if appropriate. After switching, if a name is known and search_procedures is available, use it to find candidates and verify their identity and coordinates.",
|
|
};
|
|
}
|
|
if (
|
|
error instanceof AnalysisInputError ||
|
|
error instanceof AnalysisResourceConstraintError ||
|
|
(error instanceof EvidenceIntegrityError &&
|
|
error.sourcePublicationRecovery !== undefined)
|
|
) {
|
|
const projected = projectAnalysisError(error);
|
|
if (
|
|
projected.code === "invalid_request" ||
|
|
projected.code === "resource_constraint" ||
|
|
projected.code === "evidence_integrity_mismatch"
|
|
)
|
|
return {
|
|
state: "unavailable",
|
|
reason: projected.message,
|
|
remediation: projected.remediation.action,
|
|
error: projected,
|
|
};
|
|
}
|
|
return {
|
|
state: "unavailable",
|
|
reason: error?.message ?? `${operation} returned no result`,
|
|
remediation: `Call ${operation} directly to inspect provider availability and diagnostics.`,
|
|
};
|
|
};
|
|
|
|
const isRequestLevelFailure = (error: AnalysisError): boolean =>
|
|
error._tag === "AnalysisCancelledError" ||
|
|
error._tag === "AnalysisTimeoutError" ||
|
|
error._tag === "NoBinaryOpenError" ||
|
|
(error.sourcePublicationRecovery !== undefined &&
|
|
!(error instanceof AnalysisInputError) &&
|
|
!(error instanceof AnalysisResourceConstraintError) &&
|
|
!(error instanceof EvidenceIntegrityError));
|
|
|
|
/** Wait for all facets and retain every unresolved cleanup owner. */
|
|
const contextFailure = (
|
|
results: readonly Result<AnalysisExecution, AnalysisError>[],
|
|
signalCancellation?: AnalysisCancelledError,
|
|
): AnalysisError | undefined => {
|
|
const failures = [
|
|
...new Set(results.flatMap((result) => (result.ok ? [] : [result.error]))),
|
|
];
|
|
// A provider's cancellation for this exact signal carries its own diagnostic
|
|
// and cleanup facts. Attribute that returned error to the abort event rather
|
|
// than replacing it with our generic observation of the same signal.
|
|
const marker =
|
|
signalCancellation === undefined
|
|
? -1
|
|
: failures.indexOf(signalCancellation);
|
|
if (marker >= 0) {
|
|
const reported = failures.findIndex(
|
|
(failure, index) =>
|
|
index > marker &&
|
|
failure instanceof AnalysisCancelledError &&
|
|
failure.cause === signalCancellation?.cause,
|
|
);
|
|
if (reported >= 0) {
|
|
failures[marker] = failures[reported]!;
|
|
failures.splice(reported, 1);
|
|
}
|
|
}
|
|
const first = failures.find(
|
|
(failure) => isRequestLevelFailure(failure) || failure.cleanupIncomplete,
|
|
);
|
|
if (first === undefined) return undefined;
|
|
let combined = first;
|
|
for (const failure of failures) {
|
|
if (failure === first) continue;
|
|
if (failure instanceof AnalysisCancelledError)
|
|
combined = analysisErrorWithCancellation(combined, failure);
|
|
if (failure.cleanupIncomplete) {
|
|
const previous = combined;
|
|
combined = analysisErrorWithCleanupFailure(
|
|
previous,
|
|
failure,
|
|
"inspect_address_context",
|
|
).bindCleanupDependencies([previous, failure]);
|
|
} else if (
|
|
!(failure instanceof AnalysisCancelledError) &&
|
|
(isRequestLevelFailure(failure) ||
|
|
failure.sourcePublicationRecovery !== undefined)
|
|
) {
|
|
combined.retainSecondaryFailure(failure);
|
|
}
|
|
}
|
|
return combined;
|
|
};
|
|
|
|
const matchingBookmarks = (value: JsonValue, address: string): Facet => {
|
|
if (!Array.isArray(value))
|
|
return {
|
|
state: "unavailable",
|
|
reason: "list_bookmarks returned malformed output; expected an array.",
|
|
remediation:
|
|
"Retry list_bookmarks directly and report the provider output shape if it remains malformed.",
|
|
};
|
|
const parsed = analysisBookmarkSchema.array().safeParse(value);
|
|
if (!parsed.success)
|
|
return {
|
|
state: "unavailable",
|
|
reason:
|
|
"list_bookmarks returned malformed entries; expected an address string and a string or null name.",
|
|
remediation:
|
|
"Retry list_bookmarks directly and report the provider output shape if it remains malformed.",
|
|
};
|
|
const target = nativeAddressCoordinateInputSchema.safeParse(address);
|
|
const matching = [];
|
|
for (const bookmark of parsed.data) {
|
|
if (bookmark.address === address) {
|
|
matching.push(bookmark);
|
|
continue;
|
|
}
|
|
const candidate = nativeAddressCoordinateInputSchema.safeParse(
|
|
bookmark.address,
|
|
);
|
|
if (
|
|
!target.success ||
|
|
!candidate.success ||
|
|
(target.data.offset === candidate.data.offset &&
|
|
target.data.space !== candidate.data.space)
|
|
)
|
|
return {
|
|
state: "unavailable",
|
|
reason:
|
|
"Bookmark address identity could not be compared with the selected address.",
|
|
remediation:
|
|
"Call list_bookmarks directly to inspect the provider addresses.",
|
|
};
|
|
if (
|
|
target.data.offset === candidate.data.offset &&
|
|
target.data.space === candidate.data.space
|
|
)
|
|
matching.push(bookmark);
|
|
}
|
|
return { state: "available", value: matching };
|
|
};
|