























@@ -3,7 +3,7 @@ import os from "node:os";
33import path from "node:path";
44import type { AgentMessage } from "@mariozechner/pi-agent-core";
55import { SessionManager } from "@mariozechner/pi-coding-agent";
6-import { describe, expect, it, afterEach } from "vitest";
6+import { describe, expect, it, afterEach, vi } from "vitest";
77import {
88initializeGlobalHookRunner,
99resetGlobalHookRunner,
@@ -88,6 +88,14 @@ function expectPersistedToolResultTextCapped(sm: ReturnType<typeof SessionManage
8888expect(text).toContain("truncated");
8989}
909091+function expectPersistedToolResultDetailsCapped(sm: ReturnType<typeof SessionManager.inMemory>) {
92+const toolResult = getPersistedToolResult(sm);
93+const details = toolResult.details as Record<string, unknown>;
94+expect(details.persistedDetailsTruncated).toBe(true);
95+expect(details.aggregated).toBeUndefined();
96+expect(Buffer.byteLength(JSON.stringify(details), "utf-8")).toBeLessThan(8_192);
97+}
98+9199afterEach(() => {
92100resetGlobalHookRunner();
93101if (originalBundledPluginsDir === undefined) {
@@ -109,6 +117,189 @@ describe("tool_result_persist hook", () => {
109117expect(toolResult.details).toBeTruthy();
110118});
111119120+it("caps oversized toolResult details before persistence", () => {
121+const sm = guardSessionManager(SessionManager.inMemory(), {
122+agentId: "main",
123+sessionKey: "main",
124+});
125+const appendMessage = sm.appendMessage.bind(sm) as unknown as (message: AgentMessage) => void;
126+appendMessage({
127+role: "assistant",
128+content: [{ type: "toolCall", id: "call_1", name: "exec", arguments: {} }],
129+} as AgentMessage);
130+appendMessage({
131+role: "toolResult",
132+toolCallId: "call_1",
133+isError: false,
134+content: [{ type: "text", text: "visible output stays small" }],
135+details: {
136+status: "completed",
137+sessionId: "exec-1",
138+aggregated: "x".repeat(120_000),
139+tail: "t".repeat(6_000),
140+sessions: [
141+{
142+sessionId: "proc-1",
143+status: "completed",
144+command: "node noisy-script.js ".repeat(2_000),
145+aggregated: "a".repeat(80_000),
146+tail: "z".repeat(8_000),
147+},
148+],
149+},
150+} as any);
151+152+const toolResult = getPersistedToolResult(sm);
153+expect(toolResult.content[0]?.text).toBe("visible output stays small");
154+expectPersistedToolResultDetailsCapped(sm);
155+});
156+157+it("caps oversized toolResult details without serializing the original payload", () => {
158+const sm = guardSessionManager(SessionManager.inMemory(), {
159+agentId: "main",
160+sessionKey: "main",
161+});
162+const appendMessage = sm.appendMessage.bind(sm) as unknown as (message: AgentMessage) => void;
163+const oversizedDetails = {
164+status: "completed",
165+sessionId: "exec-large",
166+aggregated: "x".repeat(200_000),
167+sessions: [
168+{
169+sessionId: "proc-large",
170+command: "node noisy-script.js ".repeat(2_000),
171+tail: "z".repeat(20_000),
172+},
173+],
174+};
175+const originalStringify = JSON.stringify;
176+const stringifySpy = vi.spyOn(JSON, "stringify").mockImplementation((value, ...args) => {
177+if (value === oversizedDetails) {
178+throw new Error("unbounded original details stringify");
179+}
180+return originalStringify(value, ...args);
181+});
182+183+try {
184+appendMessage({
185+role: "assistant",
186+content: [{ type: "toolCall", id: "call_1", name: "exec", arguments: {} }],
187+} as AgentMessage);
188+appendMessage({
189+role: "toolResult",
190+toolCallId: "call_1",
191+isError: false,
192+content: [{ type: "text", text: "visible output stays small" }],
193+details: oversizedDetails,
194+} as any);
195+} finally {
196+stringifySpy.mockRestore();
197+}
198+199+const toolResult = getPersistedToolResult(sm);
200+expect(toolResult.content[0]?.text).toBe("visible output stays small");
201+expectPersistedToolResultDetailsCapped(sm);
202+expect(stringifySpy).not.toHaveBeenCalledWith(oversizedDetails);
203+});
204+205+it("caps wide toolResult details without materializing every entry up front", () => {
206+const sm = guardSessionManager(SessionManager.inMemory(), {
207+agentId: "main",
208+sessionKey: "main",
209+});
210+const appendMessage = sm.appendMessage.bind(sm) as unknown as (message: AgentMessage) => void;
211+const wideDetails: Record<string, unknown> = {
212+status: "completed",
213+sessionId: "exec-wide",
214+};
215+for (let index = 0; index < 20_000; index += 1) {
216+wideDetails[`debug_${index}`] = `value-${index}`;
217+}
218+const originalEntries = Object.entries;
219+const originalKeys = Object.keys;
220+const entriesSpy = vi.spyOn(Object, "entries").mockImplementation((value) => {
221+if (value === wideDetails) {
222+throw new Error("wide details entries materialized");
223+}
224+return originalEntries(value);
225+});
226+const keysSpy = vi.spyOn(Object, "keys").mockImplementation((value) => {
227+if (value === wideDetails) {
228+throw new Error("wide details keys materialized");
229+}
230+return originalKeys(value);
231+});
232+233+try {
234+appendMessage({
235+role: "assistant",
236+content: [{ type: "toolCall", id: "call_1", name: "exec", arguments: {} }],
237+} as AgentMessage);
238+appendMessage({
239+role: "toolResult",
240+toolCallId: "call_1",
241+isError: false,
242+content: [{ type: "text", text: "visible output stays small" }],
243+details: wideDetails,
244+} as any);
245+} finally {
246+entriesSpy.mockRestore();
247+keysSpy.mockRestore();
248+}
249+250+const toolResult = getPersistedToolResult(sm);
251+const details = toolResult.details as Record<string, unknown>;
252+expect(details.persistedDetailsTruncated).toBe(true);
253+expect(details.originalDetailKeys).toEqual(
254+expect.arrayContaining(["status", "sessionId", "debug_0"]),
255+);
256+});
257+258+it("falls back to a compact summary when sanitized details still exceed the cap", () => {
259+const sm = guardSessionManager(SessionManager.inMemory(), {
260+agentId: "main",
261+sessionKey: "main",
262+});
263+const appendMessage = sm.appendMessage.bind(sm) as unknown as (message: AgentMessage) => void;
264+appendMessage({
265+role: "assistant",
266+content: [{ type: "toolCall", id: "call_1", name: "exec", arguments: {} }],
267+} as AgentMessage);
268+appendMessage({
269+role: "toolResult",
270+toolCallId: "call_1",
271+isError: false,
272+content: [{ type: "text", text: "visible output stays small" }],
273+details: {
274+status: "completed".repeat(250),
275+sessionId: "exec-oversized",
276+cwd: "/tmp/very-long-working-directory".repeat(250),
277+name: "noisy process".repeat(250),
278+fullOutputPath: "/tmp/output.log".repeat(250),
279+truncation: "truncated".repeat(250),
280+tail: "t".repeat(20_000),
281+aggregated: "a".repeat(120_000),
282+sessions: Array.from({ length: 10 }, (_, index) => ({
283+sessionId: `proc-${index}`,
284+status: "completed".repeat(100),
285+cwd: "/tmp/session".repeat(100),
286+name: "child process".repeat(100),
287+command: "node noisy-script.js ".repeat(200),
288+aggregated: "x".repeat(50_000),
289+tail: "z".repeat(10_000),
290+})),
291+},
292+} as any);
293+294+const toolResult = getPersistedToolResult(sm);
295+const details = toolResult.details as Record<string, unknown>;
296+expect(details.persistedDetailsTruncated).toBe(true);
297+expect(details.finalDetailsTruncated).toBe(true);
298+expect(details.aggregated).toBeUndefined();
299+expect(details.tail).toBeUndefined();
300+expect(Buffer.byteLength(JSON.stringify(details), "utf-8")).toBeLessThan(8_192);
301+});
302+112303it("loads tool_result_persist hooks without breaking persistence", () => {
113304const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-toolpersist-"));
114305process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = "/nonexistent/bundled/plugins";
@@ -189,6 +380,35 @@ describe("tool_result_persist hook", () => {
189380appendToolCallAndResult(sm);
190381expectPersistedToolResultTextCapped(sm);
191382});
383+384+it("reapplies the details cap after tool_result_persist expands details", () => {
385+initializeTempPlugin({
386+tmpPrefix: "openclaw-toolpersist-details-expand-",
387+id: "persist-details-expand",
388+body: `export default { id: "persist-details-expand", register(api) {
389+ api.on("tool_result_persist", (event) => {
390+ return {
391+ message: {
392+ ...event.message,
393+ details: {
394+ status: "completed",
395+ aggregated: "x".repeat(150000),
396+ sessions: [{ sessionId: "proc-1", command: "y".repeat(50000), tail: "z".repeat(10000) }],
397+ },
398+ },
399+ };
400+ }, { priority: 10 });
401+} };`,
402+});
403+404+const sm = guardSessionManager(SessionManager.inMemory(), {
405+agentId: "main",
406+sessionKey: "main",
407+});
408+409+appendToolCallAndResult(sm);
410+expectPersistedToolResultDetailsCapped(sm);
411+});
192412});
193413194414describe("before_message_write hook", () => {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。