diff --git a/hindsight-integrations/openclaw/src/index.test.ts b/hindsight-integrations/openclaw/src/index.test.ts index 96d31bca1..399ed2199 100644 --- a/hindsight-integrations/openclaw/src/index.test.ts +++ b/hindsight-integrations/openclaw/src/index.test.ts @@ -1041,6 +1041,45 @@ describe("sliceLastTurnsByUserBoundary", () => { const messages = [{ role: "user", content: "Hello" }]; expect(sliceLastTurnsByUserBoundary(messages, 0)).toEqual([]); }); + + it("skips synthetic user messages that contain only tool_result blocks", () => { + const messages = [ + { role: "user", content: [{ type: "text", text: "Real user input 1" }] }, + { role: "assistant", content: [{ type: "text", text: "Assistant reply 1" }] }, + { + role: "user", + content: [{ type: "tool_result", content: "Tool output for call 1" }], + }, + { + role: "user", + content: [{ type: "tool_result", content: "Tool output for call 2" }], + }, + { role: "assistant", content: [{ type: "text", text: "Assistant after tools" }] }, + { role: "user", content: [{ type: "text", text: "Real user input 2" }] }, + { role: "assistant", content: [{ type: "text", text: "Assistant reply 2" }] }, + ]; + // 3 user turns requested, but only 2 have real text content. + // Should fall back to returning all messages. + const result = sliceLastTurnsByUserBoundary(messages, 3); + expect(result).toEqual(messages); + }); + + it("uses real user turns when tool_result synthetic messages are present", () => { + const messages = [ + { role: "user", content: [{ type: "text", text: "Real user input 1" }] }, + { role: "assistant", content: [{ type: "text", text: "Assistant reply 1" }] }, + { + role: "user", + content: [{ type: "tool_result", content: "Tool output 1" }], + }, + { role: "assistant", content: [{ type: "text", text: "Assistant after tool 1" }] }, + { role: "user", content: [{ type: "text", text: "Real user input 2" }] }, + { role: "assistant", content: [{ type: "text", text: "Assistant reply 2" }] }, + ]; + // 2 real user turns. Should start at message 0 (first real user). + const result = sliceLastTurnsByUserBoundary(messages, 2); + expect(result).toEqual(messages); + }); }); // --------------------------------------------------------------------------- diff --git a/hindsight-integrations/openclaw/src/index.ts b/hindsight-integrations/openclaw/src/index.ts index db25123d7..054c7e11c 100644 --- a/hindsight-integrations/openclaw/src/index.ts +++ b/hindsight-integrations/openclaw/src/index.ts @@ -3032,11 +3032,31 @@ export function sliceLastTurnsByUserBoundary(messages: any[], turns: number): an return []; } + // Count only user messages that contain actual text content. + // OpenClaw normalizes tool_result blocks into role:"user" messages with a + // tool_result content block. Without this filter those synthetic messages + // would be counted as real user turns, causing the window to exclude actual + // user input from the retained transcript. + function hasRealTextContent(msg: any): boolean { + if (msg?.role !== "user") return false; + const content = msg.content; + if (typeof content === "string") return content.trim().length > 0; + if (Array.isArray(content)) { + return content.some( + (b: any) => + b?.type === "text" && + typeof b?.text === "string" && + b.text.trim().length > 0, + ); + } + return false; + } + let userTurnsSeen = 0; let startIndex = -1; for (let i = messages.length - 1; i >= 0; i--) { - if (messages[i]?.role === "user") { + if (messages[i]?.role === "user" && hasRealTextContent(messages[i])) { userTurnsSeen += 1; if (userTurnsSeen >= turns) { startIndex = i;