Cap sessions_history payloads to prevent context overflow (#10000)

* Cap sessions_history payloads to prevent context overflow

* fix: harden sessions_history payload caps

* fix: cap sessions_history payloads to prevent context overflow (#10000) (thanks @gut-puncture)

---------

Co-authored-by: Shailesh Rana <shaileshrana@ShaileshMM.local>
Co-authored-by: George Pickett <gpickett00@gmail.com>
This commit is contained in:
Shailesh
2026-02-06 07:20:57 +05:30
committed by GitHub
parent 328b69be17
commit bccdc95a9b
4 changed files with 271 additions and 3 deletions

View File

@@ -181,6 +181,128 @@ describe("sessions tools", () => {
expect(withToolsDetails.messages).toHaveLength(2);
});
it("sessions_history caps oversized payloads and strips heavy fields", async () => {
callGatewayMock.mockReset();
const oversized = Array.from({ length: 80 }, (_, idx) => ({
role: "assistant",
content: [
{
type: "text",
text: `${String(idx)}:${"x".repeat(5000)}`,
},
{
type: "thinking",
thinking: "y".repeat(7000),
thinkingSignature: "sig".repeat(4000),
},
],
details: {
giant: "z".repeat(12000),
},
usage: {
input: 1,
output: 1,
},
}));
callGatewayMock.mockImplementation(async (opts: unknown) => {
const request = opts as { method?: string };
if (request.method === "chat.history") {
return { messages: oversized };
}
return {};
});
const tool = createOpenClawTools().find((candidate) => candidate.name === "sessions_history");
expect(tool).toBeDefined();
if (!tool) {
throw new Error("missing sessions_history tool");
}
const result = await tool.execute("call4b", {
sessionKey: "main",
includeTools: true,
});
const details = result.details as {
messages?: Array<Record<string, unknown>>;
truncated?: boolean;
droppedMessages?: boolean;
contentTruncated?: boolean;
bytes?: number;
};
expect(details.truncated).toBe(true);
expect(details.droppedMessages).toBe(true);
expect(details.contentTruncated).toBe(true);
expect(typeof details.bytes).toBe("number");
expect((details.bytes ?? 0) <= 80 * 1024).toBe(true);
expect(details.messages && details.messages.length > 0).toBe(true);
const first = details.messages?.[0] as
| {
details?: unknown;
usage?: unknown;
content?: Array<{
type?: string;
text?: string;
thinking?: string;
thinkingSignature?: string;
}>;
}
| undefined;
expect(first?.details).toBeUndefined();
expect(first?.usage).toBeUndefined();
const textBlock = first?.content?.find((block) => block.type === "text");
expect(typeof textBlock?.text).toBe("string");
expect((textBlock?.text ?? "").length <= 4015).toBe(true);
const thinkingBlock = first?.content?.find((block) => block.type === "thinking");
expect(thinkingBlock?.thinkingSignature).toBeUndefined();
});
it("sessions_history enforces a hard byte cap even when a single message is huge", async () => {
callGatewayMock.mockReset();
callGatewayMock.mockImplementation(async (opts: unknown) => {
const request = opts as { method?: string };
if (request.method === "chat.history") {
return {
messages: [
{
role: "assistant",
content: [{ type: "text", text: "ok" }],
extra: "x".repeat(200_000),
},
],
};
}
return {};
});
const tool = createOpenClawTools().find((candidate) => candidate.name === "sessions_history");
expect(tool).toBeDefined();
if (!tool) {
throw new Error("missing sessions_history tool");
}
const result = await tool.execute("call4c", {
sessionKey: "main",
includeTools: true,
});
const details = result.details as {
messages?: Array<Record<string, unknown>>;
truncated?: boolean;
droppedMessages?: boolean;
contentTruncated?: boolean;
bytes?: number;
};
expect(details.truncated).toBe(true);
expect(details.droppedMessages).toBe(true);
expect(details.contentTruncated).toBe(false);
expect(typeof details.bytes).toBe("number");
expect((details.bytes ?? 0) <= 80 * 1024).toBe(true);
expect(details.messages).toHaveLength(1);
expect(details.messages?.[0]?.content).toContain(
"[sessions_history omitted: message too large]",
);
});
it("sessions_history resolves sessionId inputs", async () => {
callGatewayMock.mockReset();
const sessionId = "sess-group";