mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 03:45:46 -06:00
fix(gateway): dedupe delivery mirror history rows
This commit is contained in:
@@ -18,6 +18,7 @@ import {
|
||||
hasInterSessionUserProvenance,
|
||||
normalizeInputProvenance,
|
||||
} from "../../sessions/input-provenance.js";
|
||||
import { isTranscriptOnlyOpenClawAssistantMessage } from "../../shared/transcript-only-openclaw-assistant.js";
|
||||
import {
|
||||
downgradeOpenAIFunctionCallReasoningPairs,
|
||||
downgradeOpenAIReasoningBlocks,
|
||||
@@ -235,15 +236,6 @@ function stripStaleAssistantUsageBeforeLatestCompaction(messages: AgentMessage[]
|
||||
return touched ? out : messages;
|
||||
}
|
||||
|
||||
// `provider:"openclaw"` assistant entries written by the channel-delivery
|
||||
// transcript mirror (`model:"delivery-mirror"`, see config/sessions/transcript.ts)
|
||||
// and by the Gateway transcript-inject helper (`model:"gateway-injected"`, see
|
||||
// gateway/server-methods/chat-transcript-inject.ts) are user-visible transcript
|
||||
// records, not model output. Replaying them to the actual provider duplicates
|
||||
// content and, on Bedrock or strict OpenAI-compatible providers, can also
|
||||
// trigger turn-ordering rejections.
|
||||
const TRANSCRIPT_ONLY_OPENCLAW_MODELS = new Set<string>(["delivery-mirror", "gateway-injected"]);
|
||||
|
||||
function sanitizeUserReplayContent(message: AgentMessage): AgentMessage | null {
|
||||
if (!message || message.role !== "user") {
|
||||
return message;
|
||||
@@ -277,19 +269,6 @@ function sanitizeUserReplayContent(message: AgentMessage): AgentMessage | null {
|
||||
return touched ? ({ ...message, content: sanitizedContent } as AgentMessage) : message;
|
||||
}
|
||||
|
||||
function isTranscriptOnlyOpenclawAssistant(message: AgentMessage): boolean {
|
||||
if (!message || message.role !== "assistant") {
|
||||
return false;
|
||||
}
|
||||
const provider = (message as { provider?: unknown }).provider;
|
||||
const model = (message as { model?: unknown }).model;
|
||||
return (
|
||||
provider === "openclaw" &&
|
||||
typeof model === "string" &&
|
||||
TRANSCRIPT_ONLY_OPENCLAW_MODELS.has(model)
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeAssistantReplayTextContent(message: AgentMessage, replayContent: string) {
|
||||
const strippedText = stripInternalMetadataForDisplay(replayContent);
|
||||
const trimmed = strippedText.trim();
|
||||
@@ -357,7 +336,7 @@ export function normalizeAssistantReplayContent(messages: AgentMessage[]): Agent
|
||||
out.push(message);
|
||||
continue;
|
||||
}
|
||||
if (isTranscriptOnlyOpenclawAssistant(message)) {
|
||||
if (isTranscriptOnlyOpenClawAssistantMessage(message)) {
|
||||
// Drop from the in-memory replay copy; the persisted JSONL keeps the
|
||||
// entry so user-facing transcript surfaces are unchanged.
|
||||
touched = true;
|
||||
|
||||
@@ -712,6 +712,18 @@ describe("sessions tools", () => {
|
||||
return {
|
||||
messages: [
|
||||
{ role: "toolResult", content: [] },
|
||||
{
|
||||
role: "assistant",
|
||||
provider: "openclaw",
|
||||
model: "delivery-mirror",
|
||||
content: [{ type: "text", text: "mirrored" }],
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
provider: "openclaw",
|
||||
model: "gateway-injected",
|
||||
content: [{ type: "text", text: "injected" }],
|
||||
},
|
||||
{ role: "assistant", content: [{ type: "text", text: "ok" }] },
|
||||
],
|
||||
};
|
||||
@@ -725,16 +737,27 @@ describe("sessions tools", () => {
|
||||
}
|
||||
|
||||
const result = await tool.execute("call3", { sessionKey: "main" });
|
||||
const details = result.details as { messages?: Array<{ role?: string }> };
|
||||
expect(details.messages).toHaveLength(1);
|
||||
expect(details.messages?.[0]?.role).toBe("assistant");
|
||||
const details = result.details as { messages?: unknown[] };
|
||||
expect(details.messages).toHaveLength(3);
|
||||
expect(details.messages).toContainEqual(
|
||||
expect.objectContaining({ provider: "openclaw", model: "gateway-injected" }),
|
||||
);
|
||||
expect(details.messages).toContainEqual(
|
||||
expect.objectContaining({ provider: "openclaw", model: "delivery-mirror" }),
|
||||
);
|
||||
|
||||
const withTools = await tool.execute("call4", {
|
||||
sessionKey: "main",
|
||||
includeTools: true,
|
||||
});
|
||||
const withToolsDetails = withTools.details as { messages?: unknown[] };
|
||||
expect(withToolsDetails.messages).toHaveLength(2);
|
||||
expect(withToolsDetails.messages).toHaveLength(4);
|
||||
expect(withToolsDetails.messages).toContainEqual(
|
||||
expect.objectContaining({ provider: "openclaw", model: "delivery-mirror" }),
|
||||
);
|
||||
expect(withToolsDetails.messages).toContainEqual(
|
||||
expect.objectContaining({ provider: "openclaw", model: "gateway-injected" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("sessions_history caps oversized payloads and strips heavy fields", async () => {
|
||||
|
||||
@@ -21,6 +21,7 @@ import type {
|
||||
PluginHookBeforeMessageWriteResult,
|
||||
} from "../plugins/types.js";
|
||||
import { emitSessionTranscriptUpdate } from "../sessions/transcript-events.js";
|
||||
import { isTranscriptOnlyOpenClawAssistantModel } from "../shared/transcript-only-openclaw-assistant.js";
|
||||
import { formatContextLimitTruncationNotice } from "./embedded-agent-runner/context-truncation-notice.js";
|
||||
import {
|
||||
DEFAULT_MAX_LIVE_TOOL_RESULT_CHARS,
|
||||
@@ -533,7 +534,7 @@ function isTranscriptOnlyOpenClawAssistantMessage(message: AgentMessage): boolea
|
||||
}
|
||||
const provider = normalizeOptionalString((message as { provider?: unknown }).provider) ?? "";
|
||||
const model = normalizeOptionalString((message as { model?: unknown }).model) ?? "";
|
||||
return provider === "openclaw" && (model === "delivery-mirror" || model === "gateway-injected");
|
||||
return isTranscriptOnlyOpenClawAssistantModel(provider, model);
|
||||
}
|
||||
|
||||
export { getRawSessionAppendMessage };
|
||||
|
||||
@@ -7,6 +7,7 @@ import { redactTranscriptMessage } from "../../agents/transcript-redact.js";
|
||||
import { formatErrorMessage } from "../../infra/errors.js";
|
||||
import { emitSessionTranscriptUpdate } from "../../sessions/transcript-events.js";
|
||||
import { extractAssistantVisibleText } from "../../shared/chat-message-content.js";
|
||||
import { isTranscriptOnlyOpenClawAssistantModel } from "../../shared/transcript-only-openclaw-assistant.js";
|
||||
import type { OpenClawConfig } from "../types.openclaw.js";
|
||||
import {
|
||||
resolveDefaultSessionStorePath,
|
||||
@@ -97,10 +98,7 @@ function isTranscriptOnlyOpenClawAssistantMessage(message: {
|
||||
provider?: unknown;
|
||||
model?: unknown;
|
||||
}): boolean {
|
||||
return (
|
||||
message.provider === "openclaw" &&
|
||||
(message.model === "delivery-mirror" || message.model === "gateway-injected")
|
||||
);
|
||||
return isTranscriptOnlyOpenClawAssistantModel(message.provider, message.model);
|
||||
}
|
||||
|
||||
export async function resolveSessionTranscriptFile(params: {
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
parseAssistantTextSignature,
|
||||
resolveAssistantMessagePhase,
|
||||
} from "../shared/chat-message-content.js";
|
||||
import { isOpenClawDeliveryMirrorAssistantMessage } from "../shared/transcript-only-openclaw-assistant.js";
|
||||
import { stripInlineDirectiveTagsForDisplay } from "../utils/directive-tags.js";
|
||||
import { stripEnvelopeFromMessages } from "./chat-sanitize.js";
|
||||
import { isSuppressedControlReplyText } from "./control-reply-text.js";
|
||||
@@ -34,6 +35,8 @@ type PendingMessageToolVisibleReply = {
|
||||
text: string;
|
||||
anchor: Record<string, unknown>;
|
||||
completionAnchor?: Record<string, unknown>;
|
||||
deliveryMirrorAnchor?: Record<string, unknown>;
|
||||
deliveryMirrorIndex?: number;
|
||||
succeeded: boolean;
|
||||
};
|
||||
|
||||
@@ -929,6 +932,16 @@ function buildMessageToolVisibleReplyMirror(
|
||||
return mirror;
|
||||
}
|
||||
|
||||
function readMessageToolDeliveryMirrorText(message: Record<string, unknown>): string | undefined {
|
||||
// Delivery mirrors can arrive between a successful message-tool result and
|
||||
// the final NO_REPLY. The pending mirror is the display row; the raw mirror
|
||||
// would duplicate that same send.
|
||||
if (!isOpenClawDeliveryMirrorAssistantMessage(message)) {
|
||||
return undefined;
|
||||
}
|
||||
return displayTextForDuplicateCheck(message);
|
||||
}
|
||||
|
||||
function mirrorMessageToolVisibleReplies(messages: unknown[]): unknown[] {
|
||||
if (messages.length === 0) {
|
||||
return messages;
|
||||
@@ -957,6 +970,24 @@ function mirrorMessageToolVisibleReplies(messages: unknown[]): unknown[] {
|
||||
clearPending();
|
||||
};
|
||||
|
||||
const flushSelectedMirrors = (items: PendingMessageToolVisibleReply[]) => {
|
||||
if (items.length === 0) {
|
||||
return;
|
||||
}
|
||||
const selected = new Set(items);
|
||||
const remaining: PendingMessageToolVisibleReply[] = [];
|
||||
for (const item of pending) {
|
||||
if (selected.has(item) && item.succeeded) {
|
||||
next.push(buildMessageToolVisibleReplyMirror(item));
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
remaining.push(item);
|
||||
}
|
||||
pending.length = 0;
|
||||
pending.push(...remaining);
|
||||
};
|
||||
|
||||
for (const message of messages) {
|
||||
const record = readRecord(message);
|
||||
if (!record) {
|
||||
@@ -978,6 +1009,12 @@ function mirrorMessageToolVisibleReplies(messages: unknown[]): unknown[] {
|
||||
continue;
|
||||
}
|
||||
|
||||
const flushAfterCurrentMessage: PendingMessageToolVisibleReply[] = [];
|
||||
const deliveryMirrorText = readMessageToolDeliveryMirrorText(record);
|
||||
const matchingDeliveryMirrorPending = deliveryMirrorText
|
||||
? pending.filter((item) => item.text.trim() === deliveryMirrorText)
|
||||
: [];
|
||||
const duplicateDeliveryMirror = matchingDeliveryMirrorPending.some((item) => item.succeeded);
|
||||
const visibleReplies = extractMessageToolVisibleReplies(record);
|
||||
if (visibleReplies.length > 0) {
|
||||
for (const reply of visibleReplies) {
|
||||
@@ -987,7 +1024,10 @@ function mirrorMessageToolVisibleReplies(messages: unknown[]): unknown[] {
|
||||
succeeded: false,
|
||||
});
|
||||
}
|
||||
} else if (isRenderableAssistantDisplayMessage(record)) {
|
||||
} else if (
|
||||
matchingDeliveryMirrorPending.length === 0 &&
|
||||
isRenderableAssistantDisplayMessage(record)
|
||||
) {
|
||||
clearPending();
|
||||
}
|
||||
|
||||
@@ -995,7 +1035,13 @@ function mirrorMessageToolVisibleReplies(messages: unknown[]): unknown[] {
|
||||
for (const item of pending) {
|
||||
if (!item.succeeded && isSuccessfulMessageToolResult(record, item)) {
|
||||
item.succeeded = true;
|
||||
item.completionAnchor = record;
|
||||
item.completionAnchor = item.deliveryMirrorAnchor ?? record;
|
||||
if (item.deliveryMirrorAnchor) {
|
||||
if (typeof item.deliveryMirrorIndex === "number") {
|
||||
next[item.deliveryMirrorIndex] = { ...item.deliveryMirrorAnchor, display: false };
|
||||
}
|
||||
flushAfterCurrentMessage.push(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isAssistantSilentControlReplyOnly(record)) {
|
||||
@@ -1003,7 +1049,21 @@ function mirrorMessageToolVisibleReplies(messages: unknown[]): unknown[] {
|
||||
}
|
||||
}
|
||||
|
||||
if (duplicateDeliveryMirror) {
|
||||
for (const item of matchingDeliveryMirrorPending) {
|
||||
item.completionAnchor = record;
|
||||
}
|
||||
flushSelectedMirrors(matchingDeliveryMirrorPending);
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const item of matchingDeliveryMirrorPending) {
|
||||
item.deliveryMirrorAnchor = record;
|
||||
item.deliveryMirrorIndex = next.length;
|
||||
}
|
||||
next.push(message);
|
||||
flushSelectedMirrors(flushAfterCurrentMessage);
|
||||
}
|
||||
|
||||
return changed ? next : messages;
|
||||
|
||||
@@ -746,6 +746,246 @@ describe("gateway server chat", () => {
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("chat.history hides raw delivery-mirror rows but keeps message-tool mirrors", async () => {
|
||||
const replyText = "One visible send.";
|
||||
const historyMessages = await loadChatHistoryWithMessages([
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "send once" }],
|
||||
timestamp: 1,
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "toolCall",
|
||||
id: "call-message-transcript-only",
|
||||
name: "message",
|
||||
arguments: {
|
||||
action: "send",
|
||||
message: replyText,
|
||||
},
|
||||
},
|
||||
],
|
||||
timestamp: 2,
|
||||
},
|
||||
{
|
||||
role: "toolResult",
|
||||
toolName: "message",
|
||||
toolCallId: "call-message-transcript-only",
|
||||
content: { ok: true, messageId: "24271", chatId: "current-run" },
|
||||
timestamp: 3,
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
provider: "openclaw",
|
||||
model: "delivery-mirror",
|
||||
content: [{ type: "text", text: replyText }],
|
||||
timestamp: 4,
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "NO_REPLY" }],
|
||||
timestamp: 5,
|
||||
},
|
||||
]);
|
||||
|
||||
expect(collectHistoryTextValues(historyMessages)).toEqual(["send once", replyText]);
|
||||
expect(
|
||||
historyMessages.some(
|
||||
(message) =>
|
||||
Boolean(message) &&
|
||||
typeof message === "object" &&
|
||||
Boolean((message as { openclawMessageToolMirror?: unknown }).openclawMessageToolMirror),
|
||||
),
|
||||
).toBe(true);
|
||||
expect(historyMessages).not.toContainEqual(
|
||||
expect.objectContaining({ provider: "openclaw", model: "delivery-mirror" }),
|
||||
);
|
||||
});
|
||||
|
||||
test("chat.history keeps message-tool mirrors before silent completion rows", async () => {
|
||||
const replyText = "Visible before completion.";
|
||||
const historyMessages = await loadChatHistoryWithMessages([
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "toolCall",
|
||||
id: "call-message-before-completion",
|
||||
name: "message",
|
||||
arguments: {
|
||||
action: "send",
|
||||
message: replyText,
|
||||
},
|
||||
},
|
||||
],
|
||||
timestamp: 1,
|
||||
},
|
||||
{
|
||||
role: "toolResult",
|
||||
toolName: "message",
|
||||
toolCallId: "call-message-before-completion",
|
||||
content: { ok: true, messageId: "24272", chatId: "current-run" },
|
||||
timestamp: 2,
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
provider: "openclaw",
|
||||
model: "delivery-mirror",
|
||||
content: [{ type: "text", text: replyText }],
|
||||
timestamp: 3,
|
||||
},
|
||||
]);
|
||||
|
||||
expect(collectHistoryTextValues(historyMessages)).toEqual([replyText]);
|
||||
expect(
|
||||
historyMessages.some(
|
||||
(message) =>
|
||||
Boolean(message) &&
|
||||
typeof message === "object" &&
|
||||
Boolean((message as { openclawMessageToolMirror?: unknown }).openclawMessageToolMirror),
|
||||
),
|
||||
).toBe(true);
|
||||
expect(historyMessages).not.toContainEqual(
|
||||
expect.objectContaining({ provider: "openclaw", model: "delivery-mirror" }),
|
||||
);
|
||||
});
|
||||
|
||||
test("chat.history hides delivery mirrors that precede successful tool results", async () => {
|
||||
const replyText = "Visible after result.";
|
||||
const historyMessages = await loadChatHistoryWithMessages([
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "toolCall",
|
||||
id: "call-message-before-result",
|
||||
name: "message",
|
||||
arguments: {
|
||||
action: "send",
|
||||
message: replyText,
|
||||
},
|
||||
},
|
||||
],
|
||||
timestamp: 1,
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
provider: "openclaw",
|
||||
model: "delivery-mirror",
|
||||
content: [{ type: "text", text: replyText }],
|
||||
timestamp: 2,
|
||||
},
|
||||
{
|
||||
role: "toolResult",
|
||||
toolName: "message",
|
||||
toolCallId: "call-message-before-result",
|
||||
content: { ok: true, messageId: "24273", chatId: "current-run" },
|
||||
timestamp: 3,
|
||||
},
|
||||
]);
|
||||
|
||||
expect(collectHistoryTextValues(historyMessages)).toEqual([replyText]);
|
||||
expect(
|
||||
historyMessages.some(
|
||||
(message) =>
|
||||
Boolean(message) &&
|
||||
typeof message === "object" &&
|
||||
Boolean((message as { openclawMessageToolMirror?: unknown }).openclawMessageToolMirror),
|
||||
),
|
||||
).toBe(true);
|
||||
expect(historyMessages).not.toContainEqual(
|
||||
expect.objectContaining({ provider: "openclaw", model: "delivery-mirror" }),
|
||||
);
|
||||
});
|
||||
|
||||
test("chat.history preserves other pending message-tool mirrors while deduping one send", async () => {
|
||||
const firstText = "First visible send.";
|
||||
const secondText = "Second visible send.";
|
||||
const historyMessages = await loadChatHistoryWithMessages([
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "toolCall",
|
||||
id: "call-message-first",
|
||||
name: "message",
|
||||
arguments: {
|
||||
action: "send",
|
||||
message: firstText,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "toolCall",
|
||||
id: "call-message-second",
|
||||
name: "message",
|
||||
arguments: {
|
||||
action: "send",
|
||||
message: secondText,
|
||||
},
|
||||
},
|
||||
],
|
||||
timestamp: 1,
|
||||
},
|
||||
{
|
||||
role: "toolResult",
|
||||
toolName: "message",
|
||||
toolCallId: "call-message-first",
|
||||
content: { ok: true, messageId: "24274", chatId: "current-run" },
|
||||
timestamp: 2,
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
provider: "openclaw",
|
||||
model: "delivery-mirror",
|
||||
content: [{ type: "text", text: firstText }],
|
||||
timestamp: 3,
|
||||
},
|
||||
{
|
||||
role: "toolResult",
|
||||
toolName: "message",
|
||||
toolCallId: "call-message-second",
|
||||
content: { ok: true, messageId: "24275", chatId: "current-run" },
|
||||
timestamp: 4,
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
provider: "openclaw",
|
||||
model: "delivery-mirror",
|
||||
content: [{ type: "text", text: secondText }],
|
||||
timestamp: 5,
|
||||
},
|
||||
]);
|
||||
|
||||
expect(collectHistoryTextValues(historyMessages)).toEqual([firstText, secondText]);
|
||||
expect(
|
||||
historyMessages.filter(
|
||||
(message) =>
|
||||
Boolean(message) &&
|
||||
typeof message === "object" &&
|
||||
Boolean((message as { openclawMessageToolMirror?: unknown }).openclawMessageToolMirror),
|
||||
),
|
||||
).toHaveLength(2);
|
||||
expect(historyMessages).not.toContainEqual(
|
||||
expect.objectContaining({ provider: "openclaw", model: "delivery-mirror" }),
|
||||
);
|
||||
});
|
||||
|
||||
test("chat.history keeps standalone delivery-mirror rows", async () => {
|
||||
const historyMessages = await loadChatHistoryWithMessages([
|
||||
{
|
||||
role: "assistant",
|
||||
provider: "openclaw",
|
||||
model: "delivery-mirror",
|
||||
content: [{ type: "text", text: "standalone delivered reply" }],
|
||||
timestamp: 1,
|
||||
},
|
||||
]);
|
||||
|
||||
expect(collectHistoryTextValues(historyMessages)).toEqual(["standalone delivered reply"]);
|
||||
});
|
||||
|
||||
test("chat.history mirrors current-session message tool sends with channel hints", async () => {
|
||||
const replyText = "Still the current chat.";
|
||||
const historyMessages = await loadChatHistoryWithMessages([
|
||||
|
||||
@@ -56,10 +56,10 @@ async function seedSession(params?: { text?: string }) {
|
||||
storePath,
|
||||
});
|
||||
if (params?.text) {
|
||||
const appended = await appendAssistantMessageToSessionTranscript({
|
||||
const appended = await appendExactAssistantMessageToSessionTranscript({
|
||||
sessionKey: "agent:main:main",
|
||||
text: params.text,
|
||||
storePath,
|
||||
message: makeTranscriptAssistantMessage({ text: params.text }),
|
||||
});
|
||||
expect(appended.ok).toBe(true);
|
||||
}
|
||||
@@ -69,13 +69,15 @@ async function seedSession(params?: { text?: string }) {
|
||||
function makeTranscriptAssistantMessage(params: {
|
||||
text: string;
|
||||
content?: AssistantMessage["content"];
|
||||
provider?: string;
|
||||
model?: string;
|
||||
}): AssistantMessage {
|
||||
return {
|
||||
role: "assistant" as const,
|
||||
content: params.content ?? [{ type: "text", text: params.text }],
|
||||
api: "openai-responses",
|
||||
provider: "openclaw",
|
||||
model: "delivery-mirror",
|
||||
provider: params.provider ?? "openai",
|
||||
model: params.model ?? "gpt-5.5",
|
||||
usage: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
@@ -95,6 +97,16 @@ function makeTranscriptAssistantMessage(params: {
|
||||
};
|
||||
}
|
||||
|
||||
function makeDeliveryMirrorAssistantMessage(
|
||||
params: Parameters<typeof makeTranscriptAssistantMessage>[0],
|
||||
): AssistantMessage {
|
||||
return makeTranscriptAssistantMessage({
|
||||
...params,
|
||||
provider: "openclaw",
|
||||
model: "delivery-mirror",
|
||||
});
|
||||
}
|
||||
|
||||
async function appendTranscriptMessage(params: {
|
||||
sessionKey: string;
|
||||
message: AssistantMessage;
|
||||
@@ -119,7 +131,11 @@ async function appendVisibleAssistantMessage(params: {
|
||||
text: string;
|
||||
storePath: string;
|
||||
}) {
|
||||
const appended = await appendAssistantMessageToSessionTranscript(params);
|
||||
const appended = await appendExactAssistantMessageToSessionTranscript({
|
||||
sessionKey: params.sessionKey,
|
||||
storePath: params.storePath,
|
||||
message: makeTranscriptAssistantMessage({ text: params.text }),
|
||||
});
|
||||
expect(appended.ok).toBe(true);
|
||||
if (!appended.ok) {
|
||||
throw new Error(`append failed: ${appended.reason}`);
|
||||
@@ -363,6 +379,24 @@ describe("session history HTTP endpoints", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("keeps standalone delivery-mirror rows in direct REST history", async () => {
|
||||
const { storePath } = await seedSession({ text: "visible history" });
|
||||
await appendTranscriptMessage({
|
||||
sessionKey: "agent:main:main",
|
||||
storePath,
|
||||
message: makeDeliveryMirrorAssistantMessage({ text: "raw delivery mirror" }),
|
||||
emitInlineMessage: false,
|
||||
});
|
||||
|
||||
await withGatewayHarness(async (harness) => {
|
||||
const body = await readSessionHistoryBody(harness.port, "agent:main:main");
|
||||
expect(body.messages?.map((message) => message.content?.[0]?.text)).toEqual([
|
||||
"visible history",
|
||||
"raw delivery mirror",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
test("returns 404 for unknown sessions", async () => {
|
||||
await createSessionStoreFile();
|
||||
await withGatewayHarness(async (harness) => {
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
// 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 TRANSCRIPT_ONLY_OPENCLAW_ASSISTANT_MODELS = new Set<string>([
|
||||
"delivery-mirror",
|
||||
"gateway-injected",
|
||||
]);
|
||||
|
||||
export function isTranscriptOnlyOpenClawAssistantModel(provider: unknown, model: unknown): boolean {
|
||||
return (
|
||||
provider === "openclaw" &&
|
||||
typeof model === "string" &&
|
||||
TRANSCRIPT_ONLY_OPENCLAW_ASSISTANT_MODELS.has(model)
|
||||
);
|
||||
}
|
||||
|
||||
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 };
|
||||
return (
|
||||
entry.role === "assistant" &&
|
||||
isTranscriptOnlyOpenClawAssistantModel(entry.provider, entry.model)
|
||||
);
|
||||
}
|
||||
|
||||
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" && entry.model === "delivery-mirror"
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user