diff --git a/src/config/sessions/transcript.test.ts b/src/config/sessions/transcript.test.ts index c8d70e19ad0..ff8890d3225 100644 --- a/src/config/sessions/transcript.test.ts +++ b/src/config/sessions/transcript.test.ts @@ -118,6 +118,52 @@ describe("appendAssistantMessageToSessionTranscript", () => { expect(messageLine.message.content[0].text).toBe("Hello from delivery mirror!"); }); + it("does not append a duplicate delivery mirror when the latest assistant message already matches", async () => { + writeTranscriptStore(); + + const exactResult = await appendExactAssistantMessageToSessionTranscript({ + sessionKey, + storePath: fixture.storePath(), + message: { + role: "assistant", + content: [{ type: "text", text: "Hello from Codex!" }], + api: "openai-responses", + provider: "codex", + model: "gpt-5.4", + usage: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + totalTokens: 0, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, + }, + stopReason: "stop", + timestamp: Date.now(), + }, + }); + + expect(exactResult.ok).toBe(true); + + const mirrorResult = await appendAssistantMessageToSessionTranscript({ + sessionKey, + text: "Hello from Codex!", + storePath: fixture.storePath(), + }); + + expect(mirrorResult.ok).toBe(true); + if (exactResult.ok && mirrorResult.ok) { + expect(mirrorResult.messageId).toBe(exactResult.messageId); + const lines = fs.readFileSync(mirrorResult.sessionFile, "utf-8").trim().split("\n"); + expect(lines.length).toBe(2); + + const messageLine = JSON.parse(lines[1]); + expect(messageLine.message.provider).toBe("codex"); + expect(messageLine.message.model).toBe("gpt-5.4"); + expect(messageLine.message.content[0].text).toBe("Hello from Codex!"); + } + }); + it("finds session entry using normalized (lowercased) key", async () => { const storeKey = "agent:main:bluebubbles:direct:+15551234567"; const store = { diff --git a/src/config/sessions/transcript.ts b/src/config/sessions/transcript.ts index 80410dc93da..24770698174 100644 --- a/src/config/sessions/transcript.ts +++ b/src/config/sessions/transcript.ts @@ -201,6 +201,13 @@ export async function appendExactAssistantMessageToSessionTranscript(params: { return { ok: true, sessionFile, messageId: existingMessageId }; } + const latestEquivalentAssistantId = isRedundantDeliveryMirror(params.message) + ? await findLatestEquivalentAssistantMessageId(sessionFile, params.message) + : undefined; + if (latestEquivalentAssistantId) { + return { ok: true, sessionFile, messageId: latestEquivalentAssistantId }; + } + const message = { ...params.message, ...(explicitIdempotencyKey ? { idempotencyKey: explicitIdempotencyKey } : {}), @@ -252,3 +259,73 @@ async function transcriptHasIdempotencyKey( } return undefined; } + +function isRedundantDeliveryMirror(message: SessionTranscriptAssistantMessage): boolean { + return message.provider === "openclaw" && message.model === "delivery-mirror"; +} + +function extractAssistantMessageText(message: SessionTranscriptAssistantMessage): string | null { + if (typeof message.text === "string" && message.text.trim()) { + return message.text.trim(); + } + if (!Array.isArray(message.content)) { + return null; + } + + const parts = message.content + .filter( + ( + part, + ): part is { + type: "text"; + text: string; + } => part.type === "text" && typeof part.text === "string" && part.text.trim().length > 0, + ) + .map((part) => part.text.trim()); + + return parts.length > 0 ? parts.join("\n").trim() : null; +} + +async function findLatestEquivalentAssistantMessageId( + transcriptPath: string, + message: SessionTranscriptAssistantMessage, +): Promise { + const expectedText = extractAssistantMessageText(message); + if (!expectedText) { + return undefined; + } + + try { + const raw = await fs.promises.readFile(transcriptPath, "utf-8"); + const lines = raw.split(/\r?\n/); + for (let index = lines.length - 1; index >= 0; index -= 1) { + const line = lines[index]; + if (!line.trim()) { + continue; + } + try { + const parsed = JSON.parse(line) as { + id?: unknown; + message?: SessionTranscriptAssistantMessage; + }; + const candidate = parsed.message; + if (!candidate || candidate.role !== "assistant") { + continue; + } + const candidateText = extractAssistantMessageText(candidate); + if (candidateText !== expectedText) { + continue; + } + if (typeof parsed.id === "string" && parsed.id) { + return parsed.id; + } + } catch { + continue; + } + } + } catch { + return undefined; + } + + return undefined; +}