fix(sdk): preserve replayed chat snapshots

This commit is contained in:
samzong
2026-05-12 01:49:29 +08:00
committed by Peter Steinberger
parent 10315ce215
commit 63724ddcfd
3 changed files with 65 additions and 1 deletions

View File

@@ -6251,6 +6251,7 @@ public struct ChatEvent: Codable, Sendable {
public let seq: Int
public let state: AnyCodable
public let message: AnyCodable?
public let deltatext: String?
public let errormessage: String?
public let errorkind: AnyCodable?
public let usage: AnyCodable?
@@ -6263,6 +6264,7 @@ public struct ChatEvent: Codable, Sendable {
seq: Int,
state: AnyCodable,
message: AnyCodable?,
deltatext: String?,
errormessage: String?,
errorkind: AnyCodable?,
usage: AnyCodable?,
@@ -6274,6 +6276,7 @@ public struct ChatEvent: Codable, Sendable {
self.seq = seq
self.state = state
self.message = message
self.deltatext = deltatext
self.errormessage = errormessage
self.errorkind = errorkind
self.usage = usage
@@ -6287,6 +6290,7 @@ public struct ChatEvent: Codable, Sendable {
case seq
case state
case message
case deltatext = "deltaText"
case errormessage = "errorMessage"
case errorkind = "errorKind"
case usage

View File

@@ -264,6 +264,7 @@ function normalizeChatProjectionEvent(
): OpenClawEvent {
const text = readChatProjectionText(projection.payload);
const deltaText = readChatProjectionDeltaText(projection.payload);
const hasPreviousText = previousText !== undefined;
const isReplacement = Boolean(
deltaText === undefined && previousText && text !== undefined && !text.startsWith(previousText),
);
@@ -275,7 +276,12 @@ function normalizeChatProjectionEvent(
? text !== undefined
? {
text,
delta: deltaText ?? (isReplacement ? text : text.slice(previousText?.length ?? 0)),
delta:
deltaText !== undefined && hasPreviousText
? deltaText
: isReplacement
? text
: text.slice(previousText?.length ?? 0),
...(isReplacement ? { replace: true } : {}),
}
: event.data

View File

@@ -847,6 +847,60 @@ describe("OpenClaw SDK", () => {
}
});
it("uses cumulative text for the first replayed chat projection", async () => {
const transport = new FakeTransport({});
const oc = new OpenClaw({ transport });
const runId = "run_chat_delta_text_replay";
let text = "";
let iterator: AsyncIterator<OpenClawEvent> | undefined;
try {
await oc.connect();
const observedLast = (async () => {
for await (const event of oc.events(
(event) => event.raw?.event === "chat" && event.raw.seq === 501,
)) {
return event;
}
throw new Error("expected final replay setup event");
})();
for (let index = 0; index <= 500; index += 1) {
const deltaText = index === 0 ? "hello" : ` ${index}`;
text += deltaText;
transport.emit({
event: "chat",
seq: index + 1,
payload: {
runId,
sessionKey: "chat-delta-text-replay",
state: "delta",
deltaText,
message: {
role: "assistant",
content: [{ type: "text", text }],
timestamp: 1_777_000_000_300 + index,
},
},
});
}
await observedLast;
const run = await oc.runs.get(runId);
iterator = run.events()[Symbol.asyncIterator]();
const first = await iterator.next();
expect(first.done).toBe(false);
if (first.done !== false) {
throw new Error("expected first replayed chat projection event");
}
expect(first.value.type).toBe("assistant.delta");
expect(first.value.data).toEqual({ text: "hello 1", delta: "hello 1" });
} finally {
await iterator?.return?.();
await oc.close();
}
});
it("creates a session and sends a message as a run", async () => {
const transport = new FakeTransport({
"sessions.create": { key: "session-main", label: "Main" },