Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions hindsight-integrations/openclaw/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});

// ---------------------------------------------------------------------------
Expand Down
22 changes: 21 additions & 1 deletion hindsight-integrations/openclaw/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down