mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 09:50:42 +00:00
fix: harden cleanup and finalize retry keys
This commit is contained in:
@@ -245,4 +245,52 @@ describe("agent harness lifecycle hook helpers", () => {
|
||||
}),
|
||||
).resolves.toEqual({ action: "continue" });
|
||||
});
|
||||
|
||||
it("does not collide fallback retry keys for long instructions with shared prefixes", async () => {
|
||||
const sharedPrefix = "x".repeat(180);
|
||||
const firstInstruction = `${sharedPrefix} first`;
|
||||
const secondInstruction = `${sharedPrefix} second`;
|
||||
const hookRunner = {
|
||||
hasHooks: () => true,
|
||||
runBeforeAgentFinalize: vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
action: "revise",
|
||||
retry: {
|
||||
instruction: firstInstruction,
|
||||
idempotencyKey: { invalid: true },
|
||||
maxAttempts: 1,
|
||||
},
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
action: "revise",
|
||||
retry: {
|
||||
instruction: secondInstruction,
|
||||
idempotencyKey: { invalid: true },
|
||||
maxAttempts: 1,
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
await expect(
|
||||
runAgentHarnessBeforeAgentFinalizeHook({
|
||||
event: EVENT,
|
||||
ctx: { runId: "run-1", sessionKey: "agent:main:session-1" },
|
||||
hookRunner: hookRunner as never,
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
action: "revise",
|
||||
reason: firstInstruction,
|
||||
});
|
||||
await expect(
|
||||
runAgentHarnessBeforeAgentFinalizeHook({
|
||||
event: EVENT,
|
||||
ctx: { runId: "run-1", sessionKey: "agent:main:session-1" },
|
||||
hookRunner: hookRunner as never,
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
action: "revise",
|
||||
reason: secondInstruction,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { createSubsystemLogger } from "../../logging/subsystem.js";
|
||||
import { getGlobalHookRunner } from "../../plugins/hook-runner-global.js";
|
||||
import type {
|
||||
@@ -46,6 +47,10 @@ function pruneFinalizeRetryBudget(budget: FinalizeRetryBudget): void {
|
||||
}
|
||||
}
|
||||
|
||||
function buildFinalizeRetryInstructionKey(instruction: string): string {
|
||||
return `instruction:${createHash("sha256").update(instruction).digest("hex")}`;
|
||||
}
|
||||
|
||||
export function clearAgentHarnessFinalizeRetryBudget(params?: { runId?: string }): void {
|
||||
const budget = getFinalizeRetryBudget();
|
||||
if (!params?.runId) {
|
||||
@@ -149,7 +154,8 @@ function normalizeBeforeAgentFinalizeResult(
|
||||
: 1;
|
||||
const retryRunId = event?.runId ?? event?.sessionId ?? "unknown-run";
|
||||
const retryKey =
|
||||
normalizeTrimmedString(result.retry?.idempotencyKey) || retryInstruction.slice(0, 160);
|
||||
normalizeTrimmedString(result.retry?.idempotencyKey) ||
|
||||
buildFinalizeRetryInstructionKey(retryInstruction);
|
||||
const budget = getFinalizeRetryBudget();
|
||||
const runBudget = budget.get(retryRunId) ?? new Map<string, number>();
|
||||
const nextCount = (runBudget.get(retryKey) ?? 0) + 1;
|
||||
|
||||
52
src/plugins/host-hook-cleanup.config.test.ts
Normal file
52
src/plugins/host-hook-cleanup.config.test.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { runPluginHostCleanup } from "./host-hook-cleanup.js";
|
||||
import { createEmptyPluginRegistry } from "./registry-empty.js";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
getRuntimeConfig: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../config/config.js", () => ({
|
||||
getRuntimeConfig: mocks.getRuntimeConfig,
|
||||
}));
|
||||
|
||||
describe("plugin host cleanup config fallback", () => {
|
||||
afterEach(() => {
|
||||
mocks.getRuntimeConfig.mockReset();
|
||||
});
|
||||
|
||||
it("records session store config failures while continuing runtime cleanup", async () => {
|
||||
const registry = createEmptyPluginRegistry();
|
||||
const cleanup = vi.fn();
|
||||
registry.runtimeLifecycles.push({
|
||||
pluginId: "cleanup-plugin",
|
||||
pluginName: "Cleanup Plugin",
|
||||
lifecycle: {
|
||||
id: "runtime-cleanup",
|
||||
cleanup,
|
||||
},
|
||||
});
|
||||
mocks.getRuntimeConfig.mockImplementation(() => {
|
||||
throw new Error("invalid config");
|
||||
});
|
||||
|
||||
const result = await runPluginHostCleanup({
|
||||
registry,
|
||||
pluginId: "cleanup-plugin",
|
||||
reason: "disable",
|
||||
});
|
||||
|
||||
expect(cleanup).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
reason: "disable",
|
||||
}),
|
||||
);
|
||||
expect(result.cleanupCount).toBe(1);
|
||||
expect(result.failures).toEqual([
|
||||
expect.objectContaining({
|
||||
pluginId: "cleanup-plugin",
|
||||
hookId: "session-store",
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -115,16 +115,24 @@ export async function runPluginHostCleanup(params: {
|
||||
runId?: string;
|
||||
preserveSchedulerJobIds?: ReadonlySet<string>;
|
||||
}): Promise<PluginHostCleanupResult> {
|
||||
const persistentCleanupCount =
|
||||
params.reason === "restart"
|
||||
? 0
|
||||
: await clearPluginOwnedSessionStores({
|
||||
cfg: params.cfg ?? getRuntimeConfig(),
|
||||
pluginId: params.pluginId,
|
||||
sessionKey: params.sessionKey,
|
||||
});
|
||||
const registry = params.registry;
|
||||
const failures: PluginHostCleanupFailure[] = [];
|
||||
let persistentCleanupCount = 0;
|
||||
if (params.reason !== "restart") {
|
||||
try {
|
||||
persistentCleanupCount = await clearPluginOwnedSessionStores({
|
||||
cfg: params.cfg ?? getRuntimeConfig(),
|
||||
pluginId: params.pluginId,
|
||||
sessionKey: params.sessionKey,
|
||||
});
|
||||
} catch (error) {
|
||||
failures.push({
|
||||
pluginId: params.pluginId ?? "plugin-host",
|
||||
hookId: "session-store",
|
||||
error,
|
||||
});
|
||||
}
|
||||
}
|
||||
const registry = params.registry;
|
||||
let cleanupCount = persistentCleanupCount;
|
||||
if (registry) {
|
||||
for (const registration of registry.sessionExtensions ?? []) {
|
||||
|
||||
Reference in New Issue
Block a user