fix: reject dispatch plan retry mismatches

This commit is contained in:
kris
2026-03-30 01:40:52 +08:00
parent 1cd3a15a5d
commit 74ea7151ad

View File

@@ -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);