diff --git a/apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift b/apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift index 59511dddf28..10fd84d7584 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift @@ -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 diff --git a/packages/sdk/src/client.ts b/packages/sdk/src/client.ts index d1b770ce824..6666c8a9f35 100644 --- a/packages/sdk/src/client.ts +++ b/packages/sdk/src/client.ts @@ -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 diff --git a/packages/sdk/src/index.test.ts b/packages/sdk/src/index.test.ts index a239c090e70..a0bfbc03dd3 100644 --- a/packages/sdk/src/index.test.ts +++ b/packages/sdk/src/index.test.ts @@ -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 | 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" },