Memory/QMD: cap qmd command output buffering
This commit is contained in:
parent
9b9dc65a22
commit
f9f816d139
@ -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)
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
## 2026.2.14
|
||||
|
||||
@ -829,6 +829,30 @@ describe("QmdMemoryManager", () => {
|
||||
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 () => {
|
||||
spawnMock.mockImplementation((_cmd: string, args: string[]) => {
|
||||
if (args[0] === "search") {
|
||||
|
||||
@ -30,6 +30,7 @@ const log = createSubsystemLogger("memory");
|
||||
|
||||
const SNIPPET_HEADER_RE = /@@\s*-([0-9]+),([0-9]+)/;
|
||||
const SEARCH_PENDING_UPDATE_WAIT_MS = 500;
|
||||
const MAX_QMD_OUTPUT_CHARS = 200_000;
|
||||
|
||||
type CollectionRoot = {
|
||||
path: string;
|
||||
@ -74,6 +75,7 @@ export class QmdMemoryManager implements MemorySearchManager {
|
||||
string,
|
||||
{ rel: string; abs: string; source: MemorySource }
|
||||
>();
|
||||
private readonly maxQmdOutputChars = MAX_QMD_OUTPUT_CHARS;
|
||||
private readonly sessionExporter: SessionExporterConfig | null;
|
||||
private updateTimer: NodeJS.Timeout | null = null;
|
||||
private pendingUpdate: Promise<void> | null = null;
|
||||
@ -562,6 +564,8 @@ export class QmdMemoryManager implements MemorySearchManager {
|
||||
});
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
let stdoutTruncated = false;
|
||||
let stderrTruncated = false;
|
||||
const timer = opts?.timeoutMs
|
||||
? setTimeout(() => {
|
||||
child.kill("SIGKILL");
|
||||
@ -569,10 +573,14 @@ export class QmdMemoryManager implements MemorySearchManager {
|
||||
}, opts.timeoutMs)
|
||||
: null;
|
||||
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) => {
|
||||
stderr += data.toString();
|
||||
const next = appendOutputWithCap(stderr, data.toString("utf8"), this.maxQmdOutputChars);
|
||||
stderr = next.text;
|
||||
stderrTruncated = stderrTruncated || next.truncated;
|
||||
});
|
||||
child.on("error", (err) => {
|
||||
if (timer) {
|
||||
@ -584,6 +592,14 @@ export class QmdMemoryManager implements MemorySearchManager {
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
if (stdoutTruncated || stderrTruncated) {
|
||||
reject(
|
||||
new Error(
|
||||
`qmd ${args.join(" ")} produced too much output (limit ${this.maxQmdOutputChars} chars)`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (code === 0) {
|
||||
resolve({ stdout, stderr });
|
||||
} else {
|
||||
@ -951,3 +967,15 @@ export class QmdMemoryManager implements MemorySearchManager {
|
||||
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 };
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user