mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 08:00:42 +00:00
fix(auto-reply): normalize replay tail roles
This commit is contained in:
@@ -77,4 +77,39 @@ describe("replayRecentUserAssistantMessages", () => {
|
||||
expect(records[0]).toMatchObject({ id: "existing" });
|
||||
expect(records[1].message.role).toBe("user");
|
||||
});
|
||||
|
||||
it("coalesces same-role runs so replayed records strictly alternate", async () => {
|
||||
const source = path.join(root, "prev.jsonl");
|
||||
const target = path.join(root, "next.jsonl");
|
||||
await fs.writeFile(
|
||||
source,
|
||||
[
|
||||
j({ message: { role: "user", content: "older user" } }),
|
||||
j({ message: { role: "user", content: "latest user" } }),
|
||||
j({ message: { role: "assistant", content: "older assistant" } }),
|
||||
j({ message: { role: "assistant", content: "latest assistant" } }),
|
||||
j({ message: { role: "user", content: "follow-up" } }),
|
||||
j({ message: { role: "assistant", content: "answer" } }),
|
||||
].join(""),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
expect(await call(source, target)).toBe(4);
|
||||
const records = (await fs.readFile(target, "utf8"))
|
||||
.split(/\r?\n/)
|
||||
.filter((line) => line.trim().length > 0)
|
||||
.map((line) => JSON.parse(line));
|
||||
expect(records.slice(1).map((r) => r.message.role)).toEqual([
|
||||
"user",
|
||||
"assistant",
|
||||
"user",
|
||||
"assistant",
|
||||
]);
|
||||
expect(records.slice(1).map((r) => r.message.content)).toEqual([
|
||||
"latest user",
|
||||
"latest assistant",
|
||||
"follow-up",
|
||||
"answer",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,10 +12,10 @@ type KeptRecord = { role: "user" | "assistant"; line: string };
|
||||
/**
|
||||
* Copy the tail of user/assistant JSONL records from a prior transcript into a
|
||||
* freshly-rotated one. Tool, system, and compaction records are skipped so
|
||||
* replay cannot reshape tool/role ordering, and the tail is aligned to start
|
||||
* with a user turn so role-ordering resets cannot immediately recur. Uses
|
||||
* async I/O so long transcripts do not block the event loop. Returns 0 on
|
||||
* any error.
|
||||
* replay cannot reshape tool/role ordering, and the tail is aligned and
|
||||
* coalesced into alternating user/assistant turns so role-ordering resets
|
||||
* cannot immediately recur. Uses async I/O so long transcripts do not block
|
||||
* the event loop. Returns 0 on any error.
|
||||
*/
|
||||
export async function replayRecentUserAssistantMessages(params: {
|
||||
sourceTranscript?: string;
|
||||
@@ -55,7 +55,7 @@ export async function replayRecentUserAssistantMessages(params: {
|
||||
// role-ordering hazard this reset path is recovering from.
|
||||
return 0;
|
||||
}
|
||||
const tail = kept.slice(startIdx).map((entry) => entry.line);
|
||||
const tail = coalesceAlternatingReplayTail(kept.slice(startIdx)).map((entry) => entry.line);
|
||||
if (!fs.existsSync(params.targetTranscript)) {
|
||||
await fsp.mkdir(path.dirname(params.targetTranscript), { recursive: true });
|
||||
const header = JSON.stringify({
|
||||
@@ -76,3 +76,18 @@ export async function replayRecentUserAssistantMessages(params: {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Keep the newest record from each same-role run, preserving original JSONL bytes
|
||||
// for replay while ensuring strict provider alternation.
|
||||
function coalesceAlternatingReplayTail(entries: KeptRecord[]): KeptRecord[] {
|
||||
const tail: KeptRecord[] = [];
|
||||
for (const entry of entries) {
|
||||
const lastIdx = tail.length - 1;
|
||||
if (lastIdx >= 0 && tail[lastIdx]?.role === entry.role) {
|
||||
tail[lastIdx] = entry;
|
||||
continue;
|
||||
}
|
||||
tail.push(entry);
|
||||
}
|
||||
return tail;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user