Files
openclaw/src/shared/transcript-only-openclaw-assistant.ts
maweibin b8c7f31894 fix(sessions): prevent delivery-mirror prompt contamination with adjacent dedup and identity fallback (#99470) (#99504)
* fix(sessions): prevent delivery-mirror prompt contamination with adjacent dedup and identity fallback (#99470)

Two-layer defence against delivery-mirror entries leaking into provider
prompts after session rebuild / side-branch merge strips metadata:

Layer 1 (identity fallback): isTranscriptOnlyOpenClawAssistantMessage
gains an openclawDeliveryMirror field check to catch stripped-metadata
survivors that lost provider/model during rebuild.

Layer 2 (adjacent dedup): normalizeAssistantReplayContent collapses
adjacent byte-identical no-tool-call assistant messages, catching
fully-bare {role,content,usage} survivors invisible to any metadata
filter.

P1 fix: added 'tool_use' (native Anthropic snake_case) to
TOOL_CALL_TYPES in extractToolCallsFromAssistant so adjacent dedup
never collapses tool-call-bearing turns.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(sessions): narrow delivery mirror replay filtering

* test(sessions): preserve tool-call replay duplicates

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-07-16 15:19:49 -07:00

94 lines
3.4 KiB
TypeScript

// Identifies OpenClaw-authored assistant rows that are transcript bookkeeping,
// not provider model output. Some history surfaces keep gateway-injected rows
// visible, so use the narrower delivery-mirror predicate when visibility matters.
export const OPENCLAW_TRANSCRIPT_ARTIFACT_API = "openclaw-transcript" as const;
export const OPENCLAW_TRANSCRIPT_ARTIFACT_PROVIDER = "openclaw" as const;
export const OPENCLAW_DELIVERY_MIRROR_MODEL = "delivery-mirror" as const;
const OPENCLAW_GATEWAY_INJECTED_MODEL = "gateway-injected" as const;
const TRANSCRIPT_ONLY_OPENCLAW_ASSISTANT_MODELS = new Set<string>([
OPENCLAW_DELIVERY_MIRROR_MODEL,
OPENCLAW_GATEWAY_INJECTED_MODEL,
]);
const OPENCLAW_DELIVERY_MIRROR_KINDS = new Set([
"channel-final",
"channel-final-suppressed",
"message-tool-source-reply",
]);
function isOpenClawDeliveryMirrorMarker(value: unknown): boolean {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return false;
}
const kind = (value as { kind?: unknown }).kind;
return typeof kind === "string" && OPENCLAW_DELIVERY_MIRROR_KINDS.has(kind);
}
export function isTranscriptOnlyOpenClawAssistantModel(provider: unknown, model: unknown): boolean {
return (
provider === OPENCLAW_TRANSCRIPT_ARTIFACT_PROVIDER &&
typeof model === "string" &&
TRANSCRIPT_ONLY_OPENCLAW_ASSISTANT_MODELS.has(model)
);
}
/**
* Returns true when the message is an OpenClaw-authored transcript artifact
* that must not be replayed to providers.
*
* Primary check: provider="openclaw" + model in known transcript-only set.
* Fallback: a valid openclawDeliveryMirror marker catches observed historical
* rows whose provider/model provenance was stripped (#99470).
*/
export function isTranscriptOnlyOpenClawAssistantMessage(message: unknown): boolean {
if (!message || typeof message !== "object" || Array.isArray(message)) {
return false;
}
const entry = message as {
role?: unknown;
provider?: unknown;
model?: unknown;
openclawDeliveryMirror?: unknown;
};
if (entry.role !== "assistant") {
return false;
}
if (isTranscriptOnlyOpenClawAssistantModel(entry.provider, entry.model)) {
return true;
}
return isOpenClawDeliveryMirrorMarker(entry.openclawDeliveryMirror);
}
export function isOpenClawMessageToolMirrorAssistantMessage(message: unknown): boolean {
if (!message || typeof message !== "object" || Array.isArray(message)) {
return false;
}
const entry = message as { role?: unknown; openclawMessageToolMirror?: unknown };
return entry.role === "assistant" && entry.openclawMessageToolMirror !== undefined;
}
export function isOpenClawInternalSourceReplyMirrorAssistantMessage(message: unknown): boolean {
if (!isOpenClawMessageToolMirrorAssistantMessage(message)) {
return false;
}
const marker = (message as { openclawMessageToolMirror?: unknown }).openclawMessageToolMirror;
return (
Boolean(marker) &&
typeof marker === "object" &&
!Array.isArray(marker) &&
(marker as { sourceReplySink?: unknown }).sourceReplySink === "internal-ui"
);
}
export function isOpenClawDeliveryMirrorAssistantMessage(message: unknown): boolean {
if (!message || typeof message !== "object" || Array.isArray(message)) {
return false;
}
const entry = message as { role?: unknown; provider?: unknown; model?: unknown };
return (
entry.role === "assistant" &&
entry.provider === OPENCLAW_TRANSCRIPT_ARTIFACT_PROVIDER &&
entry.model === OPENCLAW_DELIVERY_MIRROR_MODEL
);
}