Auto-reply: bound abort memory map growth

This commit is contained in:
Vignesh Natarajan
2026-02-14 17:52:19 -08:00
parent 377bb9073e
commit 414b7db8af
2 changed files with 74 additions and 4 deletions

View File

@@ -1,9 +1,16 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { describe, expect, it, vi } from "vitest";
import { afterEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../../config/config.js";
import { isAbortTrigger, tryFastAbortFromMessage } from "./abort.js";
import {
getAbortMemory,
getAbortMemorySizeForTest,
isAbortTrigger,
resetAbortMemoryForTest,
setAbortMemory,
tryFastAbortFromMessage,
} from "./abort.js";
import { enqueueFollowupRun, getFollowupQueueDepth, type FollowupRun } from "./queue.js";
import { initSessionState } from "./session.js";
import { buildTestCtx } from "./test-ctx.js";
@@ -28,6 +35,10 @@ vi.mock("../../agents/subagent-registry.js", () => ({
}));
describe("abort detection", () => {
afterEach(() => {
resetAbortMemoryForTest();
});
it("triggerBodyNormalized extracts /stop from RawBody for abort detection", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-abort-"));
const storePath = path.join(root, "sessions.json");
@@ -62,6 +73,24 @@ describe("abort detection", () => {
expect(isAbortTrigger("/stop")).toBe(false);
});
it("removes abort memory entry when flag is reset", () => {
setAbortMemory("session-1", true);
expect(getAbortMemory("session-1")).toBe(true);
setAbortMemory("session-1", false);
expect(getAbortMemory("session-1")).toBeUndefined();
expect(getAbortMemorySizeForTest()).toBe(0);
});
it("caps abort memory tracking to a bounded max size", () => {
for (let i = 0; i < 2105; i += 1) {
setAbortMemory(`session-${i}`, true);
}
expect(getAbortMemorySizeForTest()).toBe(2000);
expect(getAbortMemory("session-0")).toBeUndefined();
expect(getAbortMemory("session-2104")).toBe(true);
});
it("fast-aborts even when text commands are disabled", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-abort-"));
const storePath = path.join(root, "sessions.json");

View File

@@ -22,6 +22,7 @@ import { clearSessionQueues } from "./queue.js";
const ABORT_TRIGGERS = new Set(["stop", "esc", "abort", "wait", "exit", "interrupt"]);
const ABORT_MEMORY = new Map<string, boolean>();
const ABORT_MEMORY_MAX = 2000;
export function isAbortTrigger(text?: string): boolean {
if (!text) {
@@ -32,11 +33,51 @@ export function isAbortTrigger(text?: string): boolean {
}
export function getAbortMemory(key: string): boolean | undefined {
return ABORT_MEMORY.get(key);
const normalized = key.trim();
if (!normalized) {
return undefined;
}
return ABORT_MEMORY.get(normalized);
}
function pruneAbortMemory(): void {
if (ABORT_MEMORY.size <= ABORT_MEMORY_MAX) {
return;
}
const excess = ABORT_MEMORY.size - ABORT_MEMORY_MAX;
let removed = 0;
for (const entryKey of ABORT_MEMORY.keys()) {
ABORT_MEMORY.delete(entryKey);
removed += 1;
if (removed >= excess) {
break;
}
}
}
export function setAbortMemory(key: string, value: boolean): void {
ABORT_MEMORY.set(key, value);
const normalized = key.trim();
if (!normalized) {
return;
}
if (!value) {
ABORT_MEMORY.delete(normalized);
return;
}
// Refresh insertion order so active keys are less likely to be evicted.
if (ABORT_MEMORY.has(normalized)) {
ABORT_MEMORY.delete(normalized);
}
ABORT_MEMORY.set(normalized, true);
pruneAbortMemory();
}
export function getAbortMemorySizeForTest(): number {
return ABORT_MEMORY.size;
}
export function resetAbortMemoryForTest(): void {
ABORT_MEMORY.clear();
}
export function formatAbortReplyText(stoppedSubagents?: number): string {