mirror of
https://github.com/morluto/rea.git
synced 2026-11-11 01:13:39 +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>
194 lines
6.2 KiB
TypeScript
194 lines
6.2 KiB
TypeScript
import {
|
|
sourcePublicationRecoveryOf,
|
|
type AnalysisError,
|
|
} from "../domain/analysisErrorBase.js";
|
|
import {
|
|
childFailureDiagnostics,
|
|
projectAnalysisError,
|
|
} from "../domain/analysisErrorProjection.js";
|
|
import {
|
|
analysisErrorUserMessage,
|
|
composedCallerGuidance,
|
|
} from "../domain/analysisErrorPresentation.js";
|
|
import { jsonValueSchema, type JsonValue } from "../domain/jsonValue.js";
|
|
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
|
|
import { err, type Result } from "../domain/result.js";
|
|
import type { AnalysisExecution } from "./AnalysisProvider.js";
|
|
import type { BinarySession } from "./binary/BinarySession.js";
|
|
import { analysisErrorWithCleanupFailure } from "../domain/analysisErrorCleanup.js";
|
|
import {
|
|
tryParseEvidence,
|
|
withEvidenceCallerInputPath,
|
|
} from "../domain/evidence.js";
|
|
|
|
/**
|
|
* Project a one-shot failure for its invocation, which has released its
|
|
* provider session and runtime settings by the time the caller reads it.
|
|
*/
|
|
export const projectInvocationFailure = (
|
|
error: AnalysisError,
|
|
): ReturnType<typeof projectAnalysisError> =>
|
|
projectAnalysisError(error, "invocation");
|
|
|
|
/** Add the actual outer request path to authenticated partial Evidence. */
|
|
export const projectDirectAnalysisFailure = (
|
|
error: AnalysisError,
|
|
callerInputPath: string,
|
|
): ReturnType<typeof projectAnalysisError> => {
|
|
const projection = projectInvocationFailure(error);
|
|
const partial = projection.details?.partial_observation;
|
|
const wrapped =
|
|
typeof partial === "object" &&
|
|
partial !== null &&
|
|
!Array.isArray(partial) &&
|
|
partial.kind === "unretained-evidence";
|
|
const observation = tryParseEvidence(wrapped ? partial.evidence : partial);
|
|
const rebound =
|
|
observation === undefined
|
|
? undefined
|
|
: withEvidenceCallerInputPath(observation, callerInputPath);
|
|
const completed = sourcePublicationRecoveryOf(error)?.completed;
|
|
const completedWithPath =
|
|
completed?.kind === "unretained-evidence" ||
|
|
completed?.kind === "inline-evidence"
|
|
? {
|
|
...completed,
|
|
evidence: withEvidenceCallerInputPath(
|
|
completed.evidence,
|
|
callerInputPath,
|
|
),
|
|
}
|
|
: completed?.kind === "unretained-observation"
|
|
? {
|
|
...completed,
|
|
observation: {
|
|
...completed.observation,
|
|
caller_input_path: callerInputPath,
|
|
},
|
|
}
|
|
: undefined;
|
|
if (rebound === undefined && completedWithPath === undefined)
|
|
return projection;
|
|
return {
|
|
...projection,
|
|
details: {
|
|
...projection.details,
|
|
...(rebound === undefined
|
|
? {}
|
|
: {
|
|
partial_observation: wrapped
|
|
? { ...partial, evidence: rebound }
|
|
: rebound,
|
|
}),
|
|
...(completedWithPath === undefined
|
|
? {}
|
|
: { completed_observation: completedWithPath }),
|
|
},
|
|
};
|
|
};
|
|
|
|
/** Await one-shot cleanup and retain either the outcome or both failures. */
|
|
export const withSessionCleanup = async <Value>(
|
|
session: BinarySession,
|
|
operation: () => Promise<Value>,
|
|
onCleanupFailure: (value: Value, error: AnalysisError) => Value,
|
|
): Promise<Value> => {
|
|
let value: Value;
|
|
try {
|
|
value = await operation();
|
|
} catch (cause: unknown) {
|
|
const closed = await session.close();
|
|
if (!closed.ok)
|
|
throw new AggregateError(
|
|
[cause, closed.error],
|
|
"Analysis and provider cleanup both failed",
|
|
{ cause },
|
|
);
|
|
throw cause;
|
|
}
|
|
const closed = await session.close();
|
|
return closed.ok ? value : onCleanupFailure(value, closed.error);
|
|
};
|
|
|
|
/** Project typed one-shot outcomes only after execution and cleanup settle. */
|
|
export const directAnalysisCleanupFailure = (
|
|
result: Result<JsonValue, AnalysisError>,
|
|
error: AnalysisError,
|
|
callerInputPath?: string,
|
|
): JsonValue => {
|
|
const project = (failure: AnalysisError) =>
|
|
callerInputPath === undefined
|
|
? projectInvocationFailure(failure)
|
|
: projectDirectAnalysisFailure(failure, callerInputPath);
|
|
const cleanup = project(error);
|
|
if (!result.ok) {
|
|
const primary = project(result.error);
|
|
const combined = analysisErrorWithCleanupFailure(result.error, error);
|
|
return {
|
|
error: "Analysis failed",
|
|
...primary,
|
|
code: "cleanup_incomplete",
|
|
retryable: false,
|
|
remediation: cleanup.remediation,
|
|
details: {
|
|
...primary.details,
|
|
// The domain composition owns the union of still-retained resources;
|
|
// neither projection may overwrite the other owner's cleanup state.
|
|
resources: [...combined.cleanupResources],
|
|
cleanup: "incomplete",
|
|
execution_failure: primary.details?.execution_failure ?? primary.code,
|
|
cleanup_error: cleanup,
|
|
},
|
|
};
|
|
}
|
|
return {
|
|
error: "Analysis failed",
|
|
...cleanup,
|
|
code: "cleanup_incomplete",
|
|
retryable: false,
|
|
details: {
|
|
...cleanup.details,
|
|
cleanup: "incomplete",
|
|
partial_observation: result.value,
|
|
},
|
|
};
|
|
};
|
|
|
|
/** Preserve managed observations or the original typed failure in diagnostics. */
|
|
export const managedAnalysisCleanupFailure = (
|
|
result: Result<AnalysisExecution, AnalysisError>,
|
|
error: AnalysisError,
|
|
): Result<AnalysisExecution, AnalysisError> => {
|
|
// A close rejection without a live cleanup state has unknown completion;
|
|
// an empty dependency set must not turn that rejection into confirmed cleanup.
|
|
if (!result.ok) {
|
|
const failure = analysisErrorWithCleanupFailure(result.error, error);
|
|
return err(
|
|
error.cleanupIncomplete
|
|
? failure.bindCleanupDependencies([result.error, error])
|
|
: failure,
|
|
);
|
|
}
|
|
const failure = new ProviderAdapterError(
|
|
error instanceof ProviderAdapterError ? error.providerId : "rea",
|
|
"close_binary",
|
|
{
|
|
cause: error,
|
|
...composedCallerGuidance({ remediation: error }),
|
|
cleanup: {
|
|
reason: analysisErrorUserMessage(error, "invocation"),
|
|
resources: error.cleanupResources,
|
|
},
|
|
...childFailureDiagnostics(
|
|
{ cleanup_error: { error } },
|
|
{ partial_observation: jsonValueSchema.parse(result.value) },
|
|
),
|
|
},
|
|
);
|
|
return err(
|
|
error.cleanupIncomplete
|
|
? failure.bindCleanupDependencies([error])
|
|
: failure,
|
|
);
|
|
};
|