diff --git a/src/agents/harness/lifecycle-hook-helpers.test.ts b/src/agents/harness/lifecycle-hook-helpers.test.ts index 8b27996baf9..210130747cd 100644 --- a/src/agents/harness/lifecycle-hook-helpers.test.ts +++ b/src/agents/harness/lifecycle-hook-helpers.test.ts @@ -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" }); + }); }); diff --git a/src/agents/harness/lifecycle-hook-helpers.ts b/src/agents/harness/lifecycle-hook-helpers.ts index 12e9b0f2845..5f39e70bf26 100644 --- a/src/agents/harness/lifecycle-hook-helpers.ts +++ b/src/agents/harness/lifecycle-hook-helpers.ts @@ -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(); 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; +}