fix: reject dispatch plan retry mismatches
This commit is contained in:
@@ -1548,6 +1548,46 @@ function sameStringSet(a: string[] | undefined, b: string[] | undefined) {
|
||||
return left.length === right.length && left.every((value, index) => value === right[index]);
|
||||
}
|
||||
|
||||
function dispatchPlanTargetSignature(target: DispatchPlanTarget) {
|
||||
return [
|
||||
target.projectId,
|
||||
target.deviceId,
|
||||
target.threadId,
|
||||
target.threadDisplayName,
|
||||
target.folderName,
|
||||
target.codexFolderRef ?? "",
|
||||
target.codexThreadRef ?? "",
|
||||
target.reason,
|
||||
].join("\u001f");
|
||||
}
|
||||
|
||||
function sameDispatchPlanTargets(a: DispatchPlanTarget[], b: DispatchPlanTarget[]) {
|
||||
return sameStringSet(
|
||||
a.map((target) => dispatchPlanTargetSignature(target)),
|
||||
b.map((target) => dispatchPlanTargetSignature(target)),
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeDispatchPlanTargetsForCreate(
|
||||
state: BossState,
|
||||
targets: DispatchPlanTarget[],
|
||||
) {
|
||||
if (targets.length === 0) {
|
||||
throw new Error("DISPATCH_PLAN_TARGETS_REQUIRED");
|
||||
}
|
||||
|
||||
const validatedTargets = targets.map((target) =>
|
||||
validateDispatchTargetAgainstState(state, normalizedDispatchPlanTarget(target, undefined, { allowInvalid: false }) as DispatchPlanTarget),
|
||||
);
|
||||
|
||||
const uniqueProjectIds = normalizeStringSet(validatedTargets.map((target) => target.projectId));
|
||||
if (uniqueProjectIds.length !== validatedTargets.length) {
|
||||
throw new Error("DISPATCH_PLAN_TARGET_DUPLICATE");
|
||||
}
|
||||
|
||||
return validatedTargets;
|
||||
}
|
||||
|
||||
function normalizedDispatchPlanTarget(
|
||||
raw: Partial<DispatchPlanTarget>,
|
||||
fallback?: DispatchPlanTarget,
|
||||
@@ -3774,26 +3814,29 @@ export async function createDispatchPlan(input: {
|
||||
const groupProjectId = input.groupProjectId.trim();
|
||||
const requestMessageId = input.requestMessageId.trim();
|
||||
const requestedBy = input.requestedBy.trim();
|
||||
const summary = input.summary?.trim() ?? "";
|
||||
|
||||
if (!groupProjectId) throw new Error("DISPATCH_PLAN_GROUP_PROJECT_REQUIRED");
|
||||
if (!requestMessageId) throw new Error("DISPATCH_PLAN_REQUEST_MESSAGE_REQUIRED");
|
||||
if (!requestedBy) throw new Error("DISPATCH_PLAN_REQUESTED_BY_REQUIRED");
|
||||
|
||||
const validatedTargets = normalizeDispatchPlanTargetsForCreate(state, input.targets);
|
||||
const existing = state.dispatchPlans.find(
|
||||
(plan) =>
|
||||
plan.groupProjectId === groupProjectId &&
|
||||
plan.requestMessageId === requestMessageId,
|
||||
);
|
||||
if (existing) {
|
||||
const payloadMatches =
|
||||
existing.requestedBy === requestedBy &&
|
||||
existing.summary === summary &&
|
||||
sameDispatchPlanTargets(existing.targets, validatedTargets);
|
||||
if (!payloadMatches) {
|
||||
throw new Error("DISPATCH_PLAN_RETRY_MISMATCH");
|
||||
}
|
||||
return existing;
|
||||
}
|
||||
|
||||
if (!requestedBy) throw new Error("DISPATCH_PLAN_REQUESTED_BY_REQUIRED");
|
||||
const targets = input.targets.map((target) =>
|
||||
normalizedDispatchPlanTarget(target, undefined, { allowInvalid: false }),
|
||||
) as DispatchPlanTarget[];
|
||||
if (targets.length === 0) throw new Error("DISPATCH_PLAN_TARGETS_REQUIRED");
|
||||
const validatedTargets = targets.map((target) => validateDispatchTargetAgainstState(state, target));
|
||||
|
||||
const plan: DispatchPlan = {
|
||||
planId: randomToken("dispatch-plan"),
|
||||
groupProjectId,
|
||||
@@ -3801,7 +3844,7 @@ export async function createDispatchPlan(input: {
|
||||
requestedBy,
|
||||
status: "pending_user_confirmation",
|
||||
targets: validatedTargets,
|
||||
summary: input.summary?.trim() ?? "",
|
||||
summary,
|
||||
createdAt: nowIso(),
|
||||
};
|
||||
state.dispatchPlans.unshift(plan);
|
||||
|
||||
Reference in New Issue
Block a user