diff --git a/src/commitments/runtime.test.ts b/src/commitments/runtime.test.ts index ddec4cf8b56..e970e0b20f9 100644 --- a/src/commitments/runtime.test.ts +++ b/src/commitments/runtime.test.ts @@ -293,6 +293,49 @@ describe("commitment extraction runtime", () => { ).toBe(true); }); + it("uses the queued item timestamp for terminal failure cooldowns", async () => { + const cfg = await createConfig(); + const extractBatch = vi.fn(async () => { + throw new Error("OAuth token refresh failed"); + }); + const dateNow = vi.spyOn(Date, "now").mockReturnValue(Number.NaN); + configureCommitmentExtractionRuntime({ + forceInTests: true, + extractBatch, + setTimer: () => ({ unref() {} }) as ReturnType, + clearTimer: () => undefined, + }); + + expect( + enqueueCommitmentExtraction({ + cfg, + nowMs, + agentId: "main", + sessionKey: "agent:main:discord:channel-1", + channel: "discord", + userText: "I have an interview tomorrow.", + assistantText: "Good luck.", + }), + ).toBe(true); + + try { + await expect(drainCommitmentExtractionQueue()).rejects.toThrow("OAuth token refresh failed"); + expect( + enqueueCommitmentExtraction({ + cfg, + nowMs: nowMs + 1, + agentId: "main", + sessionKey: "agent:main:discord:channel-1", + channel: "discord", + userText: "The interview is tomorrow.", + assistantText: "I hope it goes well.", + }), + ).toBe(false); + } finally { + dateNow.mockRestore(); + } + }); + it("bounds hidden extraction queue growth before spending extractor tokens", async () => { const cfg = await createConfig(); const extractBatch = vi.fn( diff --git a/src/commitments/runtime.ts b/src/commitments/runtime.ts index 6ced378210b..f75c93885f5 100644 --- a/src/commitments/runtime.ts +++ b/src/commitments/runtime.ts @@ -4,6 +4,7 @@ import { resolveAgentWorkspaceDir } from "../agents/agent-scope.js"; import type { OpenClawConfig } from "../config/config.js"; import { resolveStateDir } from "../config/paths.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; +import { resolveExpiresAtMsFromDurationMs } from "../shared/number-coercion.js"; import { normalizeOptionalString } from "../shared/string-coerce.js"; import { resolveCommitmentTimezone, resolveCommitmentsConfig } from "./config.js"; import { @@ -168,11 +169,20 @@ function isTerminalExtractionError(error: unknown): boolean { ); } -function openTerminalFailureCooldown(agentId: string, error: unknown): void { - terminalFailureCooldownUntilByAgent.set( - agentId, - Date.now() + TERMINAL_EXTRACTION_FAILURE_COOLDOWN_MS, - ); +function openTerminalFailureCooldown( + agentId: string, + error: unknown, + nowMs: number, + fallbackNowMs: number, +): void { + const cooldownUntil = + resolveExpiresAtMsFromDurationMs(TERMINAL_EXTRACTION_FAILURE_COOLDOWN_MS, { nowMs }) ?? + resolveExpiresAtMsFromDurationMs(TERMINAL_EXTRACTION_FAILURE_COOLDOWN_MS, { + nowMs: fallbackNowMs, + }); + if (cooldownUntil !== undefined) { + terminalFailureCooldownUntilByAgent.set(agentId, cooldownUntil); + } queue = queue.filter((item) => item.agentId !== agentId); log.warn("commitment extraction disabled temporarily after terminal model/auth failure", { agentId, @@ -281,7 +291,12 @@ export async function drainCommitmentExtractionQueue(): Promise { result = await extractor({ cfg: firstCfg, items }); } catch (error) { if (isTerminalExtractionError(error)) { - openTerminalFailureCooldown(items[0]?.agentId ?? "", error); + openTerminalFailureCooldown( + items[0]?.agentId ?? "", + error, + Date.now(), + items[0]?.nowMs ?? Date.now(), + ); } throw error; }