Skip to content

Commit e95efa4

Browse files
andyylinsteipete
authored andcommitted
fix(sessions): dedupe redundant delivery mirrors
1 parent 86f1084 commit e95efa4

2 files changed

Lines changed: 123 additions & 0 deletions

File tree

src/config/sessions/transcript.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,52 @@ describe("appendAssistantMessageToSessionTranscript", () => {
118118
expect(messageLine.message.content[0].text).toBe("Hello from delivery mirror!");
119119
});
120120

121+
it("does not append a duplicate delivery mirror when the latest assistant message already matches", async () => {
122+
writeTranscriptStore();
123+
124+
const exactResult = await appendExactAssistantMessageToSessionTranscript({
125+
sessionKey,
126+
storePath: fixture.storePath(),
127+
message: {
128+
role: "assistant",
129+
content: [{ type: "text", text: "Hello from Codex!" }],
130+
api: "openai-responses",
131+
provider: "codex",
132+
model: "gpt-5.4",
133+
usage: {
134+
input: 0,
135+
output: 0,
136+
cacheRead: 0,
137+
cacheWrite: 0,
138+
totalTokens: 0,
139+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
140+
},
141+
stopReason: "stop",
142+
timestamp: Date.now(),
143+
},
144+
});
145+
146+
expect(exactResult.ok).toBe(true);
147+
148+
const mirrorResult = await appendAssistantMessageToSessionTranscript({
149+
sessionKey,
150+
text: "Hello from Codex!",
151+
storePath: fixture.storePath(),
152+
});
153+
154+
expect(mirrorResult.ok).toBe(true);
155+
if (exactResult.ok && mirrorResult.ok) {
156+
expect(mirrorResult.messageId).toBe(exactResult.messageId);
157+
const lines = fs.readFileSync(mirrorResult.sessionFile, "utf-8").trim().split("\n");
158+
expect(lines.length).toBe(2);
159+
160+
const messageLine = JSON.parse(lines[1]);
161+
expect(messageLine.message.provider).toBe("codex");
162+
expect(messageLine.message.model).toBe("gpt-5.4");
163+
expect(messageLine.message.content[0].text).toBe("Hello from Codex!");
164+
}
165+
});
166+
121167
it("finds session entry using normalized (lowercased) key", async () => {
122168
const storeKey = "agent:main:bluebubbles:direct:+15551234567";
123169
const store = {

src/config/sessions/transcript.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,13 @@ export async function appendExactAssistantMessageToSessionTranscript(params: {
201201
return { ok: true, sessionFile, messageId: existingMessageId };
202202
}
203203

204+
const latestEquivalentAssistantId = isRedundantDeliveryMirror(params.message)
205+
? await findLatestEquivalentAssistantMessageId(sessionFile, params.message)
206+
: undefined;
207+
if (latestEquivalentAssistantId) {
208+
return { ok: true, sessionFile, messageId: latestEquivalentAssistantId };
209+
}
210+
204211
const message = {
205212
...params.message,
206213
...(explicitIdempotencyKey ? { idempotencyKey: explicitIdempotencyKey } : {}),
@@ -252,3 +259,73 @@ async function transcriptHasIdempotencyKey(
252259
}
253260
return undefined;
254261
}
262+
263+
function isRedundantDeliveryMirror(message: SessionTranscriptAssistantMessage): boolean {
264+
return message.provider === "openclaw" && message.model === "delivery-mirror";
265+
}
266+
267+
function extractAssistantMessageText(message: SessionTranscriptAssistantMessage): string | null {
268+
if (typeof message.text === "string" && message.text.trim()) {
269+
return message.text.trim();
270+
}
271+
if (!Array.isArray(message.content)) {
272+
return null;
273+
}
274+
275+
const parts = message.content
276+
.filter(
277+
(
278+
part,
279+
): part is {
280+
type: "text";
281+
text: string;
282+
} => part.type === "text" && typeof part.text === "string" && part.text.trim().length > 0,
283+
)
284+
.map((part) => part.text.trim());
285+
286+
return parts.length > 0 ? parts.join("\n").trim() : null;
287+
}
288+
289+
async function findLatestEquivalentAssistantMessageId(
290+
transcriptPath: string,
291+
message: SessionTranscriptAssistantMessage,
292+
): Promise<string | undefined> {
293+
const expectedText = extractAssistantMessageText(message);
294+
if (!expectedText) {
295+
return undefined;
296+
}
297+
298+
try {
299+
const raw = await fs.promises.readFile(transcriptPath, "utf-8");
300+
const lines = raw.split(/\r?\n/);
301+
for (let index = lines.length - 1; index >= 0; index -= 1) {
302+
const line = lines[index];
303+
if (!line.trim()) {
304+
continue;
305+
}
306+
try {
307+
const parsed = JSON.parse(line) as {
308+
id?: unknown;
309+
message?: SessionTranscriptAssistantMessage;
310+
};
311+
const candidate = parsed.message;
312+
if (!candidate || candidate.role !== "assistant") {
313+
continue;
314+
}
315+
const candidateText = extractAssistantMessageText(candidate);
316+
if (candidateText !== expectedText) {
317+
continue;
318+
}
319+
if (typeof parsed.id === "string" && parsed.id) {
320+
return parsed.id;
321+
}
322+
} catch {
323+
continue;
324+
}
325+
}
326+
} catch {
327+
return undefined;
328+
}
329+
330+
return undefined;
331+
}

0 commit comments

Comments
 (0)