mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 19:24:31 +00:00
Memory/QMD: cap qmd command output buffering
This commit is contained in:
@@ -67,6 +67,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
- Cron: repair missing/corrupt `nextRunAtMs` for the updated job without globally recomputing unrelated due jobs during `cron update`. (#15750)
|
- Cron: repair missing/corrupt `nextRunAtMs` for the updated job without globally recomputing unrelated due jobs during `cron update`. (#15750)
|
||||||
- Discord: prefer gateway guild id when logging inbound messages so cached-miss guilds do not appear as `guild=dm`. Thanks @thewilloftheshadow.
|
- Discord: prefer gateway guild id when logging inbound messages so cached-miss guilds do not appear as `guild=dm`. Thanks @thewilloftheshadow.
|
||||||
- TUI: refactor searchable select list description layout and add regression coverage for ANSI-highlight width bounds.
|
- TUI: refactor searchable select list description layout and add regression coverage for ANSI-highlight width bounds.
|
||||||
|
- Memory/QMD: cap QMD command output buffering to prevent memory exhaustion from pathological `qmd` command output.
|
||||||
- Models/CLI: guard `models status` string trimming paths to prevent crashes from malformed non-string config values. (#16395) Thanks @BinHPdev.
|
- Models/CLI: guard `models status` string trimming paths to prevent crashes from malformed non-string config values. (#16395) Thanks @BinHPdev.
|
||||||
|
|
||||||
## 2026.2.14
|
## 2026.2.14
|
||||||
|
|||||||
@@ -829,6 +829,30 @@ describe("QmdMemoryManager", () => {
|
|||||||
await manager.close();
|
await manager.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("errors when qmd output exceeds command output safety cap", async () => {
|
||||||
|
const noisyPayload = "x".repeat(240_000);
|
||||||
|
spawnMock.mockImplementation((_cmd: string, args: string[]) => {
|
||||||
|
if (args[0] === "search") {
|
||||||
|
const child = createMockChild({ autoClose: false });
|
||||||
|
emitAndClose(child, "stdout", noisyPayload);
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
return createMockChild();
|
||||||
|
});
|
||||||
|
|
||||||
|
const resolved = resolveMemoryBackendConfig({ cfg, agentId });
|
||||||
|
const manager = await QmdMemoryManager.create({ cfg, agentId, resolved });
|
||||||
|
expect(manager).toBeTruthy();
|
||||||
|
if (!manager) {
|
||||||
|
throw new Error("manager missing");
|
||||||
|
}
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
manager.search("noise", { sessionKey: "agent:main:slack:dm:u123" }),
|
||||||
|
).rejects.toThrow(/too much output/);
|
||||||
|
await manager.close();
|
||||||
|
});
|
||||||
|
|
||||||
it("treats plain-text no-results stdout as an empty result set", async () => {
|
it("treats plain-text no-results stdout as an empty result set", async () => {
|
||||||
spawnMock.mockImplementation((_cmd: string, args: string[]) => {
|
spawnMock.mockImplementation((_cmd: string, args: string[]) => {
|
||||||
if (args[0] === "search") {
|
if (args[0] === "search") {
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ const log = createSubsystemLogger("memory");
|
|||||||
|
|
||||||
const SNIPPET_HEADER_RE = /@@\s*-([0-9]+),([0-9]+)/;
|
const SNIPPET_HEADER_RE = /@@\s*-([0-9]+),([0-9]+)/;
|
||||||
const SEARCH_PENDING_UPDATE_WAIT_MS = 500;
|
const SEARCH_PENDING_UPDATE_WAIT_MS = 500;
|
||||||
|
const MAX_QMD_OUTPUT_CHARS = 200_000;
|
||||||
|
|
||||||
type CollectionRoot = {
|
type CollectionRoot = {
|
||||||
path: string;
|
path: string;
|
||||||
@@ -74,6 +75,7 @@ export class QmdMemoryManager implements MemorySearchManager {
|
|||||||
string,
|
string,
|
||||||
{ rel: string; abs: string; source: MemorySource }
|
{ rel: string; abs: string; source: MemorySource }
|
||||||
>();
|
>();
|
||||||
|
private readonly maxQmdOutputChars = MAX_QMD_OUTPUT_CHARS;
|
||||||
private readonly sessionExporter: SessionExporterConfig | null;
|
private readonly sessionExporter: SessionExporterConfig | null;
|
||||||
private updateTimer: NodeJS.Timeout | null = null;
|
private updateTimer: NodeJS.Timeout | null = null;
|
||||||
private pendingUpdate: Promise<void> | null = null;
|
private pendingUpdate: Promise<void> | null = null;
|
||||||
@@ -562,6 +564,8 @@ export class QmdMemoryManager implements MemorySearchManager {
|
|||||||
});
|
});
|
||||||
let stdout = "";
|
let stdout = "";
|
||||||
let stderr = "";
|
let stderr = "";
|
||||||
|
let stdoutTruncated = false;
|
||||||
|
let stderrTruncated = false;
|
||||||
const timer = opts?.timeoutMs
|
const timer = opts?.timeoutMs
|
||||||
? setTimeout(() => {
|
? setTimeout(() => {
|
||||||
child.kill("SIGKILL");
|
child.kill("SIGKILL");
|
||||||
@@ -569,10 +573,14 @@ export class QmdMemoryManager implements MemorySearchManager {
|
|||||||
}, opts.timeoutMs)
|
}, opts.timeoutMs)
|
||||||
: null;
|
: null;
|
||||||
child.stdout.on("data", (data) => {
|
child.stdout.on("data", (data) => {
|
||||||
stdout += data.toString();
|
const next = appendOutputWithCap(stdout, data.toString("utf8"), this.maxQmdOutputChars);
|
||||||
|
stdout = next.text;
|
||||||
|
stdoutTruncated = stdoutTruncated || next.truncated;
|
||||||
});
|
});
|
||||||
child.stderr.on("data", (data) => {
|
child.stderr.on("data", (data) => {
|
||||||
stderr += data.toString();
|
const next = appendOutputWithCap(stderr, data.toString("utf8"), this.maxQmdOutputChars);
|
||||||
|
stderr = next.text;
|
||||||
|
stderrTruncated = stderrTruncated || next.truncated;
|
||||||
});
|
});
|
||||||
child.on("error", (err) => {
|
child.on("error", (err) => {
|
||||||
if (timer) {
|
if (timer) {
|
||||||
@@ -584,6 +592,14 @@ export class QmdMemoryManager implements MemorySearchManager {
|
|||||||
if (timer) {
|
if (timer) {
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
}
|
}
|
||||||
|
if (stdoutTruncated || stderrTruncated) {
|
||||||
|
reject(
|
||||||
|
new Error(
|
||||||
|
`qmd ${args.join(" ")} produced too much output (limit ${this.maxQmdOutputChars} chars)`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (code === 0) {
|
if (code === 0) {
|
||||||
resolve({ stdout, stderr });
|
resolve({ stdout, stderr });
|
||||||
} else {
|
} else {
|
||||||
@@ -951,3 +967,15 @@ export class QmdMemoryManager implements MemorySearchManager {
|
|||||||
return [command, query, "--json"];
|
return [command, query, "--json"];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function appendOutputWithCap(
|
||||||
|
current: string,
|
||||||
|
chunk: string,
|
||||||
|
maxChars: number,
|
||||||
|
): { text: string; truncated: boolean } {
|
||||||
|
const appended = current + chunk;
|
||||||
|
if (appended.length <= maxChars) {
|
||||||
|
return { text: appended, truncated: false };
|
||||||
|
}
|
||||||
|
return { text: appended.slice(-maxChars), truncated: true };
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user