mirror of
https://github.com/morluto/rea.git
synced 2026-10-31 11:56:12 +00:00
fix(evidence): link composed workflows to consumed observations (#7028)
This commit is contained in:
1 parent
ed79bf9361
commit
1ca7d38764
9 files changed
+558
-3
No files matched your search
@@ -96,6 +96,7 @@ export const requiresNativeSourceAdmissionBeforeReplay = (
|
||||
/** Bounded in-memory cache for one immutable binary identity. */
|
||||
export class AnalysisSnapshotCache {
|
||||
readonly #entries = new Map<string, AnalysisSnapshotEntry>();
|
||||
readonly #evidenceIds = new Map<string, string>();
|
||||
readonly #workflowEntries = new Map<string, AnalysisSnapshotWorkflowEntry>();
|
||||
#target: AnalysisSnapshotTarget | undefined;
|
||||
#binding: AnalysisSnapshotBinding | undefined;
|
||||
@@ -138,10 +139,40 @@ export class AnalysisSnapshotCache {
|
||||
this.#target = structuredClone(snapshot.target);
|
||||
this.#binding = structuredClone(snapshot.binding);
|
||||
let imported = 0;
|
||||
const stagedQueryIds = new Set<string>();
|
||||
for (const entry of snapshot.entries) {
|
||||
if (!this.#entries.has(entry.query_id) && !this.#hasCapacity()) continue;
|
||||
if (!this.#entries.has(entry.query_id)) imported += 1;
|
||||
this.#entries.set(entry.query_id, entry);
|
||||
stagedQueryIds.add(entry.query_id);
|
||||
this.#evidenceIds.delete(entry.query_id);
|
||||
}
|
||||
// Snapshot validation authenticated these records. Recover the original
|
||||
// identity before lookup adds replay caveats; do not mint a new record.
|
||||
const evidenceIds = new Map<string, string>();
|
||||
const ambiguous = new Set<string>();
|
||||
for (const evidence of snapshot.evidence_bundle.records) {
|
||||
const queryId = analysisQueryId(
|
||||
snapshot.target,
|
||||
snapshot.binding,
|
||||
evidence.operation,
|
||||
evidence.parameters,
|
||||
);
|
||||
if (!stagedQueryIds.has(queryId)) continue;
|
||||
const entry = this.#entries.get(queryId);
|
||||
if (
|
||||
entry === undefined ||
|
||||
!evidenceMatchesEntry(evidence, entry, snapshot)
|
||||
)
|
||||
continue;
|
||||
const previous = evidenceIds.get(queryId);
|
||||
if (previous !== undefined && previous !== evidence.evidence_id)
|
||||
ambiguous.add(queryId);
|
||||
else evidenceIds.set(queryId, evidence.evidence_id);
|
||||
}
|
||||
for (const [queryId, evidenceId] of evidenceIds) {
|
||||
if (ambiguous.has(queryId)) this.#evidenceIds.delete(queryId);
|
||||
else this.#evidenceIds.set(queryId, evidenceId);
|
||||
}
|
||||
for (const entry of snapshot.workflow_entries) {
|
||||
if (!this.#workflowEntries.has(entry.query_id) && !this.#hasCapacity())
|
||||
@@ -363,7 +394,9 @@ export class AnalysisSnapshotCache {
|
||||
},
|
||||
target,
|
||||
);
|
||||
const evidenceId = this.#evidenceIds.get(queryId);
|
||||
return structuredClone({
|
||||
...(evidenceId === undefined ? {} : { evidenceId }),
|
||||
result: cached.execution.result,
|
||||
rawResult: cached.execution.raw_result,
|
||||
provider: cached.execution.provider,
|
||||
@@ -431,6 +464,9 @@ export class AnalysisSnapshotCache {
|
||||
},
|
||||
});
|
||||
this.#entries.set(entry.query_id, entry);
|
||||
if (execution.evidenceId === undefined)
|
||||
this.#evidenceIds.delete(entry.query_id);
|
||||
else this.#evidenceIds.set(entry.query_id, execution.evidenceId);
|
||||
}
|
||||
|
||||
/** Forget all target-bound entries. */
|
||||
@@ -442,6 +478,7 @@ export class AnalysisSnapshotCache {
|
||||
|
||||
#clearEntries(): void {
|
||||
this.#entries.clear();
|
||||
this.#evidenceIds.clear();
|
||||
this.#workflowEntries.clear();
|
||||
}
|
||||
|
||||
|
||||
@@ -136,6 +136,16 @@ const trackCleanupFailures = (
|
||||
};
|
||||
};
|
||||
|
||||
/** Provider metadata cannot confer authority to an uncommitted Evidence ID. */
|
||||
const unrecordedExecution = (
|
||||
result: Result<AnalysisExecution, AnalysisError>,
|
||||
): Result<AnalysisExecution, AnalysisError> => {
|
||||
if (!result.ok || !("evidenceId" in result.value)) return result;
|
||||
const observation = { ...result.value };
|
||||
delete observation.evidenceId;
|
||||
return ok(observation);
|
||||
};
|
||||
|
||||
/**
|
||||
* Owns the single active target shared by CLI and MCP adapters.
|
||||
*
|
||||
@@ -1180,7 +1190,9 @@ export class BinarySession
|
||||
"This operation requires an executable analysis target; the active session contains only app resource access.",
|
||||
),
|
||||
);
|
||||
const result = await resources.client.execute(name, arguments_, options);
|
||||
const result = unrecordedExecution(
|
||||
await resources.client.execute(name, arguments_, options),
|
||||
);
|
||||
if (!result.ok) return result;
|
||||
const execution = { ...result.value, subject: null };
|
||||
return ok(execution);
|
||||
@@ -1207,7 +1219,7 @@ export class BinarySession
|
||||
// Stop replay before dispatch, including reads overlapping the write.
|
||||
if (capability.effects.mutatesArtifact) this.invalidateSnapshot();
|
||||
const call = active.client.execute(name, arguments_, options);
|
||||
const result = await call;
|
||||
const result = unrecordedExecution(await call);
|
||||
const profiled = bindExecutionTarget(
|
||||
commitExecutionProfile(name, result, profile),
|
||||
name,
|
||||
@@ -1266,14 +1278,16 @@ export class BinarySession
|
||||
);
|
||||
const recorded = this.recordEvidence(evidence);
|
||||
if (!recorded.ok) return recorded;
|
||||
const retained = { ...execution.value, evidenceId: evidence.evidence_id };
|
||||
if (execution.value.cachePolicy !== "live")
|
||||
this.recordSnapshot({
|
||||
target: active.target,
|
||||
profile,
|
||||
operation: name,
|
||||
parameters: arguments_,
|
||||
execution: execution.value,
|
||||
execution: retained,
|
||||
});
|
||||
return ok(retained);
|
||||
}
|
||||
return execution;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user