Skip to content

fix(sessions): dedupe redundant delivery mirrors#67185

Merged
steipete merged 2 commits into
openclaw:mainfrom
andyylin:fix/codex-delivery-mirror-dedupe
Apr 16, 2026
Merged

fix(sessions): dedupe redundant delivery mirrors#67185
steipete merged 2 commits into
openclaw:mainfrom
andyylin:fix/codex-delivery-mirror-dedupe

Conversation

@andyylin

Copy link
Copy Markdown
Contributor

Summary

  • dedupe delivery-mirror transcript appends when the latest assistant message already has the same visible text
  • keep the existing idempotency-key dedupe path unchanged
  • add a regression test that reproduces the Codex case where the real assistant transcript entry already exists before the generic delivery mirror append runs

Problem

When replies go through the Codex harness, OpenClaw already persists the real assistant message into the session transcript. The generic outbound delivery path still appends a synthetic provider:"openclaw", model:"delivery-mirror" assistant entry with the same text.

In Mattermost this can surface as duplicate visible replies for a single turn.

Root Cause

Two transcript writers become active on the same turn:

  • the Codex harness mirrors the real assistant transcript into the session file
  • the generic delivery path appends delivery-mirror

The old idempotency-key guard does not help here because the real assistant message and the synthetic mirror do not share an idempotency key.

Fix

Before appending a delivery-mirror message, scan backward through the transcript and reuse the latest assistant message id when its visible text already matches. This keeps the mirror path intact for channels that still need it, but avoids creating a second assistant artifact when the transcript already contains the real reply.

Repro

  1. Configure a channel session that routes through the Codex harness.
  2. Send a prompt that produces a normal assistant reply.
  3. Observe the session transcript contains:
    • a real assistant message from provider:"codex"
    • a second assistant message from provider:"openclaw", model:"delivery-mirror" with the same text
  4. In Mattermost, both artifacts can surface as duplicate replies.

Test Plan

  • Added a unit test in src/config/sessions/transcript.test.ts that appends a real codex assistant message first, then attempts to append the matching delivery-mirror, and asserts that no second transcript line is written.
  • I could not run Vitest in this environment because the checkout does not have node_modules installed, so the PR currently has code-level validation plus the focused regression test only.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a814c8f564

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +315 to +319
const candidateText = extractAssistantMessageText(candidate);
if (candidateText !== expectedText) {
continue;
}
if (typeof parsed.id === "string" && parsed.id) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Restrict mirror dedupe to latest assistant message

This dedupe returns an existing ID for any prior assistant message with matching text, so a repeated assistant reply on a later turn (with no idempotency key) will be treated as a duplicate and not appended. In practice, common repeated outputs like "Done."/"OK" can collapse distinct turns into one transcript entry, which drops history and breaks message sequencing for consumers. The scan should only dedupe against the immediately latest assistant artifact for the current turn (or another turn-scoped signal), not any historical text match.

Useful? React with 👍 / 👎.

@greptile-apps

greptile-apps Bot commented Apr 15, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a text-based dedup guard to appendExactAssistantMessageToSessionTranscript so that a delivery-mirror entry is skipped when the transcript already contains a real assistant message with matching visible text (the Codex harness case).

  • The new findLatestEquivalentAssistantMessageId helper scans the transcript backwards but uses continue on non-matching assistant entries, meaning it will scan all the way back and can match an older turn's reply instead of stopping at the first non-matching assistant message. This risks silently suppressing a legitimate new delivery mirror for the current turn when a previous turn had identical reply text.

Confidence Score: 3/5

Not safe to merge as-is: the dedup scan can match across conversation turns, silently dropping delivery mirrors when reply text repeats.

One P1 logic bug: findLatestEquivalentAssistantMessageId continues backward past non-matching assistant messages rather than stopping, meaning any earlier turn with identical reply text triggers a false-positive suppression. The fix is a one-line change (replace continue with return undefined on a non-matching assistant candidate), but the current code has a real correctness hole on repeated assistant responses.

src/config/sessions/transcript.ts — specifically the findLatestEquivalentAssistantMessageId loop body at line 301.

Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/config/sessions/transcript.ts
Line: 301-325

Comment:
**Scan continues past non-matching assistant messages — allows cross-turn false positives**

The loop `continue`s on both non-assistant entries and non-matching assistant entries, so it searches the entire transcript backward. If the most-recent assistant message doesn't match the delivery mirror text but an older one (from a previous turn) does, that stale ID is returned and the delivery mirror is silently suppressed for the current turn.

Concrete failure: a repeated response such as "I'm sorry, I can't help with that" appended by the non-Codex path will match an earlier occurrence from a different turn. The delivery mirror is dropped, `mirrorResult.messageId` points to the old turn's entry, and downstream code relying on per-turn IDs (e.g. Mattermost dedup tracking) may silently lose the current reply.

The fix is to stop scanning as soon as the first non-matching assistant message is encountered, rather than continuing backward:

```suggestion
        const candidateText = extractAssistantMessageText(candidate);
        if (candidateText !== expectedText) {
          return undefined;
        }
```

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "fix(sessions): dedupe redundant delivery..." | Re-trigger Greptile

Comment on lines +301 to +325
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;
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Scan continues past non-matching assistant messages — allows cross-turn false positives

The loop continues on both non-assistant entries and non-matching assistant entries, so it searches the entire transcript backward. If the most-recent assistant message doesn't match the delivery mirror text but an older one (from a previous turn) does, that stale ID is returned and the delivery mirror is silently suppressed for the current turn.

Concrete failure: a repeated response such as "I'm sorry, I can't help with that" appended by the non-Codex path will match an earlier occurrence from a different turn. The delivery mirror is dropped, mirrorResult.messageId points to the old turn's entry, and downstream code relying on per-turn IDs (e.g. Mattermost dedup tracking) may silently lose the current reply.

The fix is to stop scanning as soon as the first non-matching assistant message is encountered, rather than continuing backward:

Suggested change
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;
}
}
const candidateText = extractAssistantMessageText(candidate);
if (candidateText !== expectedText) {
return undefined;
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/config/sessions/transcript.ts
Line: 301-325

Comment:
**Scan continues past non-matching assistant messages — allows cross-turn false positives**

The loop `continue`s on both non-assistant entries and non-matching assistant entries, so it searches the entire transcript backward. If the most-recent assistant message doesn't match the delivery mirror text but an older one (from a previous turn) does, that stale ID is returned and the delivery mirror is silently suppressed for the current turn.

Concrete failure: a repeated response such as "I'm sorry, I can't help with that" appended by the non-Codex path will match an earlier occurrence from a different turn. The delivery mirror is dropped, `mirrorResult.messageId` points to the old turn's entry, and downstream code relying on per-turn IDs (e.g. Mattermost dedup tracking) may silently lose the current reply.

The fix is to stop scanning as soon as the first non-matching assistant message is encountered, rather than continuing backward:

```suggestion
        const candidateText = extractAssistantMessageText(candidate);
        if (candidateText !== expectedText) {
          return undefined;
        }
```

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Contributor Author

Live repro evidence from a real Mattermost install using codex/*:

  • Session transcript showed two assistant artifacts for a single user turn:
    • real assistant reply from provider:"codex"
    • synthetic assistant reply from provider:"openclaw", model:"delivery-mirror"
  • The duplicate behavior started only after routing switched from openai-codex/* to codex/*.
  • In the affected session, recent duplicated turns consistently had the same visible text in both artifacts.

Why this lines up with the source:

  • generic outbound delivery appends delivery-mirror through appendAssistantMessageToSessionTranscript(...)
  • the Codex harness separately mirrors the real app-server transcript into the same session file
  • that means Codex turns can hit both writers on the same reply, while the old idempotency-key guard does not apply because the two messages do not share an idempotency key

Local verification before opening this PR:

  • I patched the installed runtime with the same dedupe logic proposed here
  • re-appending the same mirror text against the live session returned the existing assistant message id instead of increasing transcript line count
  • transcript line count stayed unchanged during that verification

So this is not just a Mattermost delivery quirk. The duplicate visible reply is downstream of a real redundant transcript append on Codex-backed turns.

Copy link
Copy Markdown
Contributor Author

Reviewer note on the added test:

The new transcript.test.ts case is intentionally Codex-shaped, not a generic duplicate-text test.

It first appends a real assistant transcript entry with provider:"codex", then tries to append the matching synthetic delivery-mirror text. That matches the live failure mode from Mattermost:

  • Codex harness has already written the real assistant reply
  • generic delivery then tries to append the same visible reply again as delivery-mirror

So the assertion is specifically guarding the redundant second write that appears on Codex-backed turns.

@steipete steipete force-pushed the fix/codex-delivery-mirror-dedupe branch from a814c8f to 1a10d95 Compare April 16, 2026 15:51

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1a10d95fac

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +309 to +310
if (!candidate || candidate.role !== "assistant") {
continue;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Abort mirror dedupe when trailing entry is not assistant

Fresh evidence: the reverse scan skips non-assistant entries, so in a normal turn boundary (assistant from prior turn, then user, then new mirror send) it can walk past the latest user line and reuse the older assistant ID if the text repeats (for example "OK"), which drops the current turn’s assistant artifact. This still collapses repeated answers across turns; dedupe should only apply when the most recent transcript message itself is the matching assistant.

Useful? React with 👍 / 👎.

Comment on lines +204 to +206
const latestEquivalentAssistantId = isRedundantDeliveryMirror(params.message)
? await findLatestEquivalentAssistantMessageId(sessionFile, params.message)
: undefined;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Respect explicit idempotency keys before text dedupe

After an explicit idempotencyKey lookup misses, the new text-based dedupe still runs for delivery-mirror and can return an older message ID based only on text. That suppresses appending a new transcript entry and never records the new key, so distinct sends with different idempotency keys can collapse into one message. The text dedupe path should be gated off when a caller supplies an explicit key (or include the key in the equivalence check).

Useful? React with 👍 / 👎.

@steipete steipete force-pushed the fix/codex-delivery-mirror-dedupe branch from 1a10d95 to 1fea15d Compare April 16, 2026 16:02

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1fea15ddd6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +272 to +280
const parts = message.content
.filter(
(
part,
): part is {
type: "text";
text: string;
} => part.type === "text" && typeof part.text === "string" && part.text.trim().length > 0,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Ignore commentary phases in mirror text comparison

The new text matcher concatenates all assistant text blocks, so phased messages that include hidden commentary are compared as commentary + final instead of the user-visible final answer. In Codex/OpenAI phased transcripts, appendExactAssistantMessageToSessionTranscript can store both commentary and final_answer blocks, while the delivery mirror text is only the visible final output; this makes candidateText !== expectedText and the dedupe path appends a duplicate mirror anyway. This means the duplicate-prevention fix misses a real production shape of assistant messages.

Useful? React with 👍 / 👎.

@steipete steipete force-pushed the fix/codex-delivery-mirror-dedupe branch from 1fea15d to b40ec19 Compare April 16, 2026 16:10
@steipete steipete merged commit d842ec4 into openclaw:main Apr 16, 2026
43 checks passed
@steipete

Copy link
Copy Markdown
Contributor

Landed via rebase merge.

Source/head: b40ec19213718b390944bc2294740066eb724a30
Main: d842ec417924c3eb886c9a5ba76680aa18b3ea5c

Review follow-up included:

  • Tightened delivery-mirror dedupe so stale repeated assistant text cannot suppress a new turn.
  • Added a regression test for repeated matching text separated by a newer nonmatching assistant message.

Gates:

  • pnpm test src/config/sessions/transcript.test.ts
  • pnpm check
  • PR CI accepted by branch protection; all substantive matrix checks green at merge time

xudaiyanzi pushed a commit to xudaiyanzi/openclaw that referenced this pull request Apr 17, 2026
kvnkho pushed a commit to kvnkho/openclaw that referenced this pull request Apr 17, 2026
Mquarmoc pushed a commit to Mquarmoc/openclaw that referenced this pull request Apr 20, 2026
lovewanwan pushed a commit to lovewanwan/openclaw that referenced this pull request Apr 28, 2026
ogt-redknie pushed a commit to ogt-redknie/OPENX that referenced this pull request May 2, 2026
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 9, 2026
globalcaos pushed a commit to globalcaos/tinkerclaw that referenced this pull request May 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants