fix: guard finalize retry metadata

This commit is contained in:
Eva
2026-05-01 20:45:37 +07:00
committed by Josh Lehman
parent 0c04253f0e
commit e45121f3c5
2 changed files with 47 additions and 7 deletions

View File

@@ -213,4 +213,36 @@ describe("agent harness lifecycle hook helpers", () => {
reason: "fix generated baseline\n\nrerun the focused tests",
});
});
it("falls back to retry instruction keys when retry idempotency keys are malformed", async () => {
const hookRunner = {
hasHooks: () => true,
runBeforeAgentFinalize: vi.fn().mockResolvedValue({
action: "revise",
retry: {
instruction: "retry with a safe key",
idempotencyKey: { invalid: true },
maxAttempts: 1,
} as never,
}),
};
await expect(
runAgentHarnessBeforeAgentFinalizeHook({
event: EVENT,
ctx: { runId: "run-1", sessionKey: "agent:main:session-1" },
hookRunner: hookRunner as never,
}),
).resolves.toEqual({
action: "revise",
reason: "retry with a safe key",
});
await expect(
runAgentHarnessBeforeAgentFinalizeHook({
event: EVENT,
ctx: { runId: "run-1", sessionKey: "agent:main:session-1" },
hookRunner: hookRunner as never,
}),
).resolves.toEqual({ action: "continue" });
});
});

View File

@@ -137,19 +137,19 @@ function normalizeBeforeAgentFinalizeResult(
event?: PluginHookBeforeAgentFinalizeEvent,
): AgentHarnessBeforeAgentFinalizeOutcome {
if (result?.action === "finalize") {
return result.reason?.trim()
? { action: "finalize", reason: result.reason.trim() }
: { action: "finalize" };
const reason = normalizeTrimmedString(result.reason);
return reason ? { action: "finalize", reason } : { action: "finalize" };
}
if (result?.action === "revise") {
const retryInstruction = result.retry?.instruction?.trim();
const retryInstruction = normalizeTrimmedString(result.retry?.instruction);
if (retryInstruction) {
const maxAttempts =
typeof result.retry?.maxAttempts === "number" && Number.isFinite(result.retry.maxAttempts)
? Math.max(1, Math.floor(result.retry.maxAttempts))
: 1;
const retryRunId = event?.runId ?? event?.sessionId ?? "unknown-run";
const retryKey = result.retry?.idempotencyKey?.trim() || retryInstruction.slice(0, 160);
const retryKey =
normalizeTrimmedString(result.retry?.idempotencyKey) || retryInstruction.slice(0, 160);
const budget = getFinalizeRetryBudget();
const runBudget = budget.get(retryRunId) ?? new Map<string, number>();
const nextCount = (runBudget.get(retryKey) ?? 0) + 1;
@@ -161,15 +161,23 @@ function normalizeBeforeAgentFinalizeResult(
if (nextCount > maxAttempts) {
return { action: "continue" };
}
const reason = result.reason?.trim();
const reason = normalizeTrimmedString(result.reason);
const revisedReason =
reason && reason.includes(retryInstruction)
? reason
: [reason, retryInstruction].filter(Boolean).join("\n\n");
return { action: "revise", reason: revisedReason };
}
const reason = result.reason?.trim();
const reason = normalizeTrimmedString(result.reason);
return reason ? { action: "revise", reason } : { action: "continue" };
}
return { action: "continue" };
}
function normalizeTrimmedString(value: unknown): string | undefined {
if (typeof value !== "string") {
return undefined;
}
const trimmed = value.trim();
return trimmed ? trimmed : undefined;
}