feat(orchestrator): harden zero-impact shadow rollout

Gate and bound Portal shadow observations while preserving Native execution. Add fail-closed service boundaries, terminal retention controls, Canary readiness telemetry, ops visibility, and isolated regression coverage.
This commit is contained in:
john
2026-07-25 07:28:37 +08:00
parent 08a48e4849
commit 6df82818c5
33 changed files with 1569 additions and 108 deletions
+103
View File
@@ -11,6 +11,8 @@ import {
import { createExecutorWorkerCoordinator } from './executor-worker-protocol.mjs';
import { createExecutorAdmissionPolicy } from './executor-admission-policy.mjs';
const TERMINAL_RUN_STATUSES = new Set(['succeeded', 'failed', 'cancelled']);
function graphConfig(runId) {
return {
configurable: {
@@ -134,6 +136,35 @@ export function createLangGraphOrchestratorRuntime({
);
}
async function deleteRun(runId) {
const normalizedRunId = String(runId ?? '').trim();
if (!normalizedRunId) return null;
const state = await getState(normalizedRunId);
if (!state) return null;
if (!TERMINAL_RUN_STATUSES.has(state.status)) {
const error = new Error('Workflow run must be terminal before deletion');
error.code = 'WORKFLOW_RUN_NOT_TERMINAL';
error.status = 409;
throw error;
}
if (typeof checkpointer.deleteThread !== 'function') {
const error = new Error('Checkpoint backend does not support run deletion');
error.code = 'WORKFLOW_DELETE_UNSUPPORTED';
error.status = 501;
throw error;
}
const executorJobId = state.plan?.executorJob?.id ?? null;
const executorJobDeleted = executorJobId
? await executorGateway.deleteJob(executorJobId)
: false;
await checkpointer.deleteThread(normalizedRunId);
return {
runId: normalizedRunId,
deleted: true,
executorJobDeleted,
};
}
function health() {
return {
status: 'ok',
@@ -215,6 +246,77 @@ export function createLangGraphOrchestratorRuntime({
throw error;
},
deleteRun,
async purgeTerminalRuns({
before,
limit = 100,
dryRun = true,
} = {}) {
const cutoff = Number(before);
if (!Number.isFinite(cutoff) || cutoff <= 0) {
const error = new Error('Retention purge requires a positive before timestamp');
error.code = 'WORKFLOW_PURGE_CUTOFF_REQUIRED';
error.status = 422;
throw error;
}
const pageSize = Math.min(500, Math.max(1, Number(limit) || 100));
const candidates = await executorGateway.listRetentionCandidates({
before: cutoff,
limit: pageSize,
});
const uniqueCandidates = [...new Map(
candidates.map((candidate) => [candidate.workflowRunId, candidate]),
).values()];
if (dryRun) {
return {
version: 'orchestrator-retention-purge-v1',
dryRun: true,
before: cutoff,
candidates: uniqueCandidates,
deleted: [],
failures: [],
};
}
const deleted = [];
const failures = [];
for (const candidate of uniqueCandidates) {
try {
const result = await deleteRun(candidate.workflowRunId);
if (result) {
deleted.push({
runId: candidate.workflowRunId,
jobId: candidate.jobId,
});
continue;
}
const executorJobDeleted = await executorGateway.deleteJob(candidate.jobId);
if (executorJobDeleted) {
deleted.push({
runId: candidate.workflowRunId,
jobId: candidate.jobId,
orphanedCheckpoint: true,
});
}
} catch (error) {
failures.push({
runId: candidate.workflowRunId,
jobId: candidate.jobId,
code: String(error?.code ?? 'WORKFLOW_PURGE_FAILED').slice(0, 128),
message: String(error instanceof Error ? error.message : error).slice(0, 500),
});
}
}
return {
version: 'orchestrator-retention-purge-v1',
dryRun: false,
before: cutoff,
candidates: uniqueCandidates,
deleted,
failures,
};
},
getExecutorJob(jobId) {
return executorGateway.getJob(jobId);
},
@@ -294,4 +396,5 @@ export function createLangGraphOrchestratorRuntime({
export const orchestratorRuntimeInternals = {
graphConfig,
projectSnapshot,
TERMINAL_RUN_STATUSES,
};