fix: prevent delivery-mirror re-delivery and raise Slack chunk limit (#45489)

Merged via squash.

Prepared head SHA: c7664c7b6e
Co-authored-by: theo674 <261068216+theo674@users.noreply.github.com>
Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com>
Reviewed-by: @altaywtf
This commit is contained in:
theo674
2026-03-23 17:11:19 -04:00
committed by GitHub
parent a53715e9d0
commit dbe7da7684
17 changed files with 174 additions and 18 deletions

View File

@@ -38,6 +38,15 @@ const stripTrailingDirective = (text: string): string => {
return text.slice(0, openIndex);
};
function isTranscriptOnlyOpenClawAssistantMessage(message: AgentMessage | undefined): boolean {
if (!message || message.role !== "assistant") {
return false;
}
const provider = typeof message.provider === "string" ? message.provider.trim() : "";
const model = typeof message.model === "string" ? message.model.trim() : "";
return provider === "openclaw" && (model === "delivery-mirror" || model === "gateway-injected");
}
function emitReasoningEnd(ctx: EmbeddedPiSubscribeContext) {
if (!ctx.state.reasoningStreamOpen) {
return;
@@ -134,7 +143,7 @@ export function handleMessageStart(
evt: AgentEvent & { message: AgentMessage },
) {
const msg = evt.message;
if (msg?.role !== "assistant") {
if (msg?.role !== "assistant" || isTranscriptOnlyOpenClawAssistantMessage(msg)) {
return;
}
@@ -153,7 +162,7 @@ export function handleMessageUpdate(
evt: AgentEvent & { message: AgentMessage; assistantMessageEvent?: unknown },
) {
const msg = evt.message;
if (msg?.role !== "assistant") {
if (msg?.role !== "assistant" || isTranscriptOnlyOpenClawAssistantMessage(msg)) {
return;
}
@@ -323,7 +332,7 @@ export function handleMessageEnd(
evt: AgentEvent & { message: AgentMessage },
) {
const msg = evt.message;
if (msg?.role !== "assistant") {
if (msg?.role !== "assistant" || isTranscriptOnlyOpenClawAssistantMessage(msg)) {
return;
}

View File

@@ -42,10 +42,15 @@ async function emitMessageToolLifecycle(params: {
});
}
function emitAssistantMessageEnd(emit: (evt: unknown) => void, text: string) {
function emitAssistantMessageEnd(
emit: (evt: unknown) => void,
text: string,
overrides?: Partial<AssistantMessage>,
) {
const assistantMessage = {
role: "assistant",
content: [{ type: "text", text }],
...overrides,
} as AssistantMessage;
emit({ type: "message_end", message: assistantMessage });
}
@@ -68,6 +73,7 @@ describe("subscribeEmbeddedPiSession", () => {
result: "ok",
});
emitAssistantMessageEnd(emit, messageText);
await Promise.resolve();
expect(onBlockReply).not.toHaveBeenCalled();
});
@@ -82,16 +88,44 @@ describe("subscribeEmbeddedPiSession", () => {
result: { details: { status: "error" } },
});
emitAssistantMessageEnd(emit, messageText);
await Promise.resolve();
expect(onBlockReply).toHaveBeenCalledTimes(1);
});
it("clears block reply state on message_start", () => {
it("ignores delivery-mirror assistant messages", async () => {
const { emit, onBlockReply } = createBlockReplyHarness("message_end");
emitAssistantMessageEnd(emit, "Mirrored transcript text", {
provider: "openclaw",
model: "delivery-mirror",
});
await Promise.resolve();
expect(onBlockReply).not.toHaveBeenCalled();
});
it("ignores gateway-injected assistant messages", async () => {
const { emit, onBlockReply } = createBlockReplyHarness("message_end");
emitAssistantMessageEnd(emit, "Injected transcript text", {
provider: "openclaw",
model: "gateway-injected",
});
await Promise.resolve();
expect(onBlockReply).not.toHaveBeenCalled();
});
it("clears block reply state on message_start", async () => {
const { emit, onBlockReply } = createBlockReplyHarness("text_end");
emitAssistantTextEndBlock(emit, "OK");
await Promise.resolve();
expect(onBlockReply).toHaveBeenCalledTimes(1);
// New assistant message with identical output should still emit.
emitAssistantTextEndBlock(emit, "OK");
await Promise.resolve();
expect(onBlockReply).toHaveBeenCalledTimes(2);
});
});

View File

@@ -5,6 +5,7 @@ import path from "node:path";
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { upsertAcpSessionMeta } from "../../acp/runtime/session-meta.js";
import * as jsonFiles from "../../infra/json-files.js";
import * as transcriptEvents from "../../sessions/transcript-events.js";
import type { OpenClawConfig } from "../config.js";
import {
clearSessionStoreCacheForTest,
@@ -429,6 +430,42 @@ describe("appendAssistantMessageToSessionTranscript", () => {
}
});
it("emits transcript update events for delivery mirrors", async () => {
const sessionId = "test-session-id";
const sessionKey = "test-session";
const store = {
[sessionKey]: {
sessionId,
chatType: "direct",
channel: "discord",
},
};
fs.writeFileSync(fixture.storePath(), JSON.stringify(store), "utf-8");
const emitSpy = vi.spyOn(transcriptEvents, "emitSessionTranscriptUpdate");
await appendAssistantMessageToSessionTranscript({
sessionKey,
text: "Hello from delivery mirror!",
storePath: fixture.storePath(),
});
const sessionFile = resolveSessionTranscriptPathInDir(sessionId, fixture.sessionsDir());
expect(emitSpy).toHaveBeenCalledWith(
expect.objectContaining({
sessionFile,
sessionKey,
messageId: expect.any(String),
message: expect.objectContaining({
role: "assistant",
provider: "openclaw",
model: "delivery-mirror",
content: [{ type: "text", text: "Hello from delivery mirror!" }],
}),
}),
);
emitSpy.mockRestore();
});
it("does not append a duplicate delivery mirror for the same idempotency key", async () => {
writeTranscriptStore();

View File

@@ -1,10 +1,11 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
const resolveProviderUsageAuthWithPluginMock = vi.fn();
const resolveProviderUsageAuthWithPluginMock = vi.fn(
async (..._args: unknown[]): Promise<unknown> => null,
);
vi.mock("../plugins/provider-runtime.js", () => ({
resolveProviderUsageAuthWithPlugin: (...args: unknown[]) =>
resolveProviderUsageAuthWithPluginMock(...args),
resolveProviderUsageAuthWithPlugin: resolveProviderUsageAuthWithPluginMock,
}));
let resolveProviderAuths: typeof import("./provider-usage.auth.js").resolveProviderAuths;