feat(orchestrator): enforce Page Data validation gate

This commit is contained in:
john
2026-07-25 14:27:07 +08:00
parent 417aa9c78b
commit 721db19a92
10 changed files with 432 additions and 43 deletions
+124 -42
View File
@@ -539,6 +539,10 @@ export function createAgentRunGateway({
process.env.MEMIND_ORCHESTRATOR_SHADOW_MAX_QUEUE,
100,
),
enforcePageDataWorkflowValidation = envFlag(
process.env.MEMIND_ORCHESTRATOR_PAGE_DATA_VALIDATION_GATE_ENABLED,
false,
),
workerIdentity = null,
}) {
const worker = normalizeAgentRunWorkerIdentity(workerIdentity ?? {});
@@ -721,7 +725,7 @@ export function createAgentRunGateway({
});
}
function dispatchPageDataValidationObservation({
function buildPageDataValidationObservationInput({
row,
runId,
checks,
@@ -729,8 +733,7 @@ export function createAgentRunGateway({
}) {
const userMessage = parseDbJsonColumn(row?.user_message_json, {}) ?? {};
const runOptions = getRunOptionsFromMessage(userMessage);
if (!isPageDataWorkflowRequested(userMessage, runOptions.taskType)) return false;
return validationDispatcher.dispatch({
return {
runId,
requestId: row?.request_id ?? null,
userId: row?.user_id ?? null,
@@ -747,7 +750,74 @@ export function createAgentRunGateway({
source: 'portal-agent-run',
observedAt: nowMs(),
},
});
};
}
function dispatchPageDataValidationObservation(input) {
return validationDispatcher.dispatch(input);
}
async function enforcePageDataValidationGate(input) {
const startedAt = nowMs();
let result = null;
let gateError = null;
try {
if (typeof observeWorkflowValidation !== 'function') {
gateError = new Error('Page Data Orchestrator validation observer is unavailable');
gateError.code = 'WORKFLOW_VALIDATION_GATE_UNAVAILABLE';
throw gateError;
}
result = await observeWorkflowValidation(input);
const verdict = result?.validation?.verdict ?? null;
if (!result?.observed) {
gateError = new Error(
`Page Data Orchestrator validation was not observed: ${result?.reason ?? 'not_selected'}`,
);
gateError.code = 'WORKFLOW_VALIDATION_GATE_UNAVAILABLE';
throw gateError;
}
if (verdict !== 'passed') {
gateError = new Error(
`Page Data Orchestrator validation did not pass: ${verdict ?? 'unknown'}`,
);
gateError.code = verdict === 'failed'
? 'WORKFLOW_VALIDATION_GATE_REJECTED'
: 'WORKFLOW_VALIDATION_GATE_INCONCLUSIVE';
throw gateError;
}
await appendEvent(input.runId, 'workflow_validation_gate_passed', {
engine: result.engine ?? 'langgraph',
mode: result.mode ?? 'shadow',
configVersion: result.configVersion ?? null,
kind: result.validation?.kind ?? 'page-data-delivery',
verdict,
latencyMs: Math.max(0, nowMs() - startedAt),
});
return result;
} catch (error) {
const resolved = gateError ?? error;
const code = String(
gateError?.code ?? 'WORKFLOW_VALIDATION_GATE_UNAVAILABLE',
).slice(0, 128);
const failure = resolved instanceof Error
? resolved
: new Error(String(resolved ?? 'Page Data Orchestrator validation failed'));
await appendEvent(input.runId, 'workflow_validation_gate_failed', {
code,
upstreamCode: gateError
? null
: String(error?.code ?? 'WORKFLOW_VALIDATION_OBSERVER_FAILED').slice(0, 128),
message: String(
failure.message,
).slice(0, 1000),
verdict: result?.validation?.verdict ?? null,
latencyMs: Math.max(0, nowMs() - startedAt),
}).catch(() => {});
failure.code = code;
failure.retryable = false;
failure.validationObservationAttempted = true;
throw failure;
}
}
async function appendRunSnapshot(runId) {
@@ -1662,40 +1732,46 @@ export function createAgentRunGateway({
throw error;
}
}
const validationInput = pageDataIntent
? buildPageDataValidationObservationInput({
row,
runId,
checks: [
{ id: 'agent_run_completion', status: 'passed' },
{
id: 'page_data_binding',
status: typeof syncUserPagesOnSuccess === 'function' ? 'passed' : 'skipped',
},
{
id: 'page_data_storage_policy',
status: typeof validateRunDeliverables === 'function' ? 'passed' : 'skipped',
},
{
id: 'page_data_deliverable',
status: requiresPageDeliverable ? 'passed' : 'skipped',
},
{
id: 'independent_review',
status: runOptions.reviewExecutor ? 'passed' : 'skipped',
},
],
metrics: {
pageCount: deliverables?.pageCount ?? 0,
publicationCount: deliverables?.publicationCount ?? 0,
},
})
: null;
if (validationInput && enforcePageDataWorkflowValidation) {
await enforcePageDataValidationGate(validationInput);
}
const marked = await markRun(runId, 'succeeded', {
agent_session_id: sessionId,
completed_at: nowMs(),
error_message: null,
}, { expectedStatus: 'running' });
if (!marked) return false;
if (pageDataIntent) {
dispatchPageDataValidationObservation({
row,
runId,
checks: [
{ id: 'agent_run_completion', status: 'passed' },
{
id: 'page_data_binding',
status: typeof syncUserPagesOnSuccess === 'function' ? 'passed' : 'skipped',
},
{
id: 'page_data_storage_policy',
status: typeof validateRunDeliverables === 'function' ? 'passed' : 'skipped',
},
{
id: 'page_data_deliverable',
status: requiresPageDeliverable ? 'passed' : 'skipped',
},
{
id: 'independent_review',
status: runOptions.reviewExecutor ? 'passed' : 'skipped',
},
],
metrics: {
pageCount: deliverables?.pageCount ?? 0,
publicationCount: deliverables?.publicationCount ?? 0,
},
});
if (validationInput && !enforcePageDataWorkflowValidation) {
dispatchPageDataValidationObservation(validationInput);
}
if (typeof observePersonalMemoryOnSuccess === 'function') {
await observePersonalMemoryOnSuccess({
@@ -1779,16 +1855,22 @@ export function createAgentRunGateway({
}
: {}),
}, { expectedStatus: 'running' });
if (!retryable) {
dispatchPageDataValidationObservation({
row,
runId,
checks: [{
id: pageDataFailureCheckId(err?.code),
status: 'failed',
codes: [String(err?.code ?? 'AGENT_RUN_FAILED')],
}],
});
if (!retryable && err?.validationObservationAttempted !== true) {
const userMessage = parseDbJsonColumn(row?.user_message_json, {}) ?? {};
const runOptions = getRunOptionsFromMessage(userMessage);
if (isPageDataWorkflowRequested(userMessage, runOptions.taskType)) {
dispatchPageDataValidationObservation(
buildPageDataValidationObservationInput({
row,
runId,
checks: [{
id: pageDataFailureCheckId(err?.code),
status: 'failed',
codes: [String(err?.code ?? 'AGENT_RUN_FAILED')],
}],
}),
);
}
}
if (retryable && autoDispatch) {
setTimeout(() => dispatchRun(runId), retryDelaysMs[nextAttempt - 1]);