Skip to content

Commit 02e9031

Browse files
author
OpenClaw Agent
committed
fix(compaction): preserve assistant messages before surviving users in rotated transcript
When compaction rotation creates a new session file via buildSuccessorEntries, all messages before firstKeptEntryId are marked for removal. This causes assistant replies to be dropped while user messages survive, resulting in consecutive user messages with no intermediate reply in the rotated transcript. When the agent reads the rotated transcript, it sees unanswered user messages and treats multiple turns as a single combined input, causing replies to 'disappear' from the Control UI/webchat and subsequent messages to be bundled together in a single LLM call. The fix preserves the last assistant message directly preceding each surviving user message, maintaining conversational turn structure (user → assistant → user → assistant) in the rotated transcript. Fixes #76729
1 parent d3d28e3 commit 02e9031

2 files changed

Lines changed: 51 additions & 2 deletions

File tree

src/agents/pi-embedded-runner/compaction-successor-transcript.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,11 @@ function buildSuccessorEntries(params: {
151151
removedIds.add(entry.id);
152152
}
153153
}
154+
// Preserve assistant messages that precede surviving user messages.
155+
// Otherwise the rotated transcript may show consecutive user messages
156+
// with no intermediate reply, causing the agent to treat multiple turns
157+
// as one combined input.
158+
preserveLastAssistantBeforeSurvivingUser(allEntries, removedIds);
154159

155160
const entryById = new Map(allEntries.map((entry) => [entry.id, entry]));
156161
const activeBranchIds = new Set(branch.map((entry) => entry.id));
@@ -178,6 +183,52 @@ function buildSuccessorEntries(params: {
178183
});
179184
}
180185

186+
/**
187+
* Preserve assistant messages that directly precede surviving user messages.
188+
* Without this, the rotated transcript may contain consecutive user messages
189+
* with no intermediate replies, causing the agent to treat multiple turns
190+
* as a single combined input (since it sees unanswered user messages).
191+
*/
192+
function preserveLastAssistantBeforeSurvivingUser(
193+
allEntries: SessionEntry[],
194+
removedIds: Set<string>,
195+
): void {
196+
// Collect surviving user message ids
197+
const survivingUserIds = new Set<string>();
198+
for (const entry of allEntries) {
199+
if (
200+
entry.type === "message" &&
201+
typeof entry.message === "object" &&
202+
entry.message !== null &&
203+
(entry.message as { role?: string }).role === "user" &&
204+
!removedIds.has(entry.id)
205+
) {
206+
survivingUserIds.add(entry.id);
207+
}
208+
}
209+
210+
if (survivingUserIds.size === 0) return;
211+
212+
// Scan for removed assistant messages; if the next surviving user
213+
// message directly follows (no intervening surviving user), keep it.
214+
let pendingAssistantId: string | undefined;
215+
for (const entry of allEntries) {
216+
if (entry.type === "message" && typeof entry.message === "object" && entry.message !== null) {
217+
const role = (entry.message as { role?: string }).role;
218+
if (role === "assistant" && removedIds.has(entry.id)) {
219+
pendingAssistantId = entry.id;
220+
} else if (role === "user") {
221+
if (survivingUserIds.has(entry.id) && pendingAssistantId) {
222+
removedIds.delete(pendingAssistantId);
223+
}
224+
pendingAssistantId = undefined;
225+
} else if (role !== "toolResult") {
226+
pendingAssistantId = undefined;
227+
}
228+
}
229+
}
230+
}
231+
181232
function collectLatestStateEntryIds(entries: SessionEntry[]): Set<string> {
182233
const latestByType = new Map<string, SessionEntry>();
183234
for (const entry of entries) {

src/auto-reply/reply/reply-run-registry.compaction-regression.test.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,8 @@ import {
33
__testing,
44
createReplyOperation,
55
forceClearReplyRunBySessionId,
6-
isReplyRunActiveForSessionId,
76
queueReplyRunMessage,
87
replyRunRegistry,
9-
waitForReplyRunEndBySessionId,
108
} from "./reply-run-registry.js";
119

1210
describe("reply run registry – preflight compaction regression", () => {

0 commit comments

Comments
 (0)