fix: recognize attachment message sends

This commit is contained in:
Peter Steinberger
2026-05-05 22:06:39 +01:00
parent a36981a2c5
commit a0ea07e462
5 changed files with 151 additions and 3 deletions

View File

@@ -86,6 +86,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Agents/generated media: treat attachment-style message tool actions as completed chat sends, preventing duplicate fallback media posts when generated files were already uploaded.
- Discord/streaming: show live reasoning text in progress drafts instead of a bare `Reasoning` status line.
- Doctor/status: warn when `OPENCLAW_GATEWAY_TOKEN` would shadow a different active `gateway.auth.token` source for local CLI commands, while avoiding false positives when config points at the same env token. Fixes #74271. Thanks @yelog.
- Gateway/HTTP: avoid loading managed outgoing-image media handlers for unrelated requests, so disabled OpenAI-compatible routes return 404 without waiting on lazy media sidecars. Thanks @vincentkoc.

View File

@@ -2,6 +2,18 @@ import { getChannelPlugin, normalizeChannelId } from "../channels/plugins/index.
import { normalizeOptionalString } from "../shared/string-coerce.js";
const CORE_MESSAGING_TOOLS = new Set(["sessions_send", "message"]);
const MESSAGE_TOOL_SEND_ACTIONS = new Set([
"send",
"thread-reply",
"sendWithEffect",
"sendAttachment",
"upload-file",
]);
export function isMessageToolSendActionName(action: unknown): boolean {
const normalized = normalizeOptionalString(action) ?? "";
return MESSAGE_TOOL_SEND_ACTIONS.has(normalized);
}
// Provider docking: any plugin with `actions` opts into messaging tool handling.
export function isMessagingTool(toolName: string): boolean {
@@ -21,7 +33,7 @@ export function isMessagingToolSendAction(
return true;
}
if (toolName === "message") {
return action === "send" || action === "thread-reply";
return isMessageToolSendActionName(action);
}
const providerId = normalizeChannelId(toolName);
if (!providerId) {

View File

@@ -941,6 +941,85 @@ describe("messaging tool media URL tracking", () => {
]);
});
it("commits upload-file args as message delivery evidence", async () => {
const { ctx } = createTestContext();
const startEvt: ToolExecutionStartEvent = {
type: "tool_execution_start",
toolName: "message",
toolCallId: "tool-upload-file",
args: {
action: "upload-file",
channel: "discord",
to: "channel:123",
message: "track ready",
path: "/tmp/generated-song.mp3",
},
};
await handleToolExecutionStart(ctx, startEvt);
expect(ctx.state.pendingMessagingMediaUrls.get("tool-upload-file")).toEqual([
"/tmp/generated-song.mp3",
]);
const endEvt: ToolExecutionEndEvent = {
type: "tool_execution_end",
toolName: "message",
toolCallId: "tool-upload-file",
isError: false,
result: { ok: true },
};
await handleToolExecutionEnd(ctx, endEvt);
expect(ctx.state.messagingToolSentMediaUrls).toEqual(["/tmp/generated-song.mp3"]);
expect(ctx.state.messagingToolSentTargets).toEqual([
expect.objectContaining({
provider: "discord",
to: "channel:123",
text: "track ready",
mediaUrls: ["/tmp/generated-song.mp3"],
}),
]);
expect(ctx.state.pendingMessagingMediaUrls.has("tool-upload-file")).toBe(false);
});
it("commits sendAttachment args as message delivery evidence", async () => {
const { ctx } = createTestContext();
const startEvt: ToolExecutionStartEvent = {
type: "tool_execution_start",
toolName: "message",
toolCallId: "tool-send-attachment",
args: {
action: "sendAttachment",
provider: "discord",
to: "channel:123",
content: "track ready",
filePath: "/tmp/generated-song.mp3",
},
};
await handleToolExecutionStart(ctx, startEvt);
const endEvt: ToolExecutionEndEvent = {
type: "tool_execution_end",
toolName: "message",
toolCallId: "tool-send-attachment",
isError: false,
result: { ok: true },
};
await handleToolExecutionEnd(ctx, endEvt);
expect(ctx.state.messagingToolSentMediaUrls).toEqual(["/tmp/generated-song.mp3"]);
expect(ctx.state.messagingToolSentTargets).toEqual([
expect.objectContaining({
provider: "discord",
to: "channel:123",
text: "track ready",
mediaUrls: ["/tmp/generated-song.mp3"],
}),
]);
});
it("trims messagingToolSentMediaUrls to 200 on commit (FIFO)", async () => {
const { ctx } = createTestContext();

View File

@@ -60,4 +60,58 @@ describe("extractMessagingToolSend", () => {
expect(result?.provider).toBe("telegram");
expect(result?.to).toBe("telegram:123");
});
it("recognizes attachment-style message tool sends", () => {
const upload = extractMessagingToolSend("message", {
action: "upload-file",
channel: "discord",
to: "channel:123",
path: "/tmp/song.mp3",
});
const attachment = extractMessagingToolSend("message", {
action: "sendAttachment",
provider: "discord",
to: "channel:123",
filePath: "/tmp/song.mp3",
});
const effect = extractMessagingToolSend("message", {
action: "sendWithEffect",
provider: "discord",
to: "channel:123",
content: "done",
});
expect(upload).toMatchObject({
tool: "message",
provider: "discord",
to: "channel:123",
});
expect(attachment).toMatchObject({
tool: "message",
provider: "discord",
to: "channel:123",
});
expect(effect).toMatchObject({
tool: "message",
provider: "discord",
to: "channel:123",
});
});
it("keeps thread id evidence for thread replies", () => {
const result = extractMessagingToolSend("message", {
action: "thread-reply",
provider: "discord",
to: "channel:123",
threadId: "456",
content: "done",
});
expect(result).toMatchObject({
tool: "message",
provider: "discord",
to: "channel:123",
threadId: "456",
});
});
});

View File

@@ -10,6 +10,7 @@ import {
} from "../shared/string-coerce.js";
import { truncateUtf16Safe } from "../utils.js";
import { collectTextContentBlocks } from "./content-blocks.js";
import { isMessageToolSendActionName } from "./pi-embedded-messaging.js";
import type { MessagingToolSend } from "./pi-embedded-messaging.types.js";
import { normalizeToolName } from "./tool-policy.js";
@@ -539,7 +540,7 @@ export function extractMessagingToolSend(
const action = normalizeOptionalString(args.action) ?? "";
const accountId = normalizeOptionalString(args.accountId);
if (toolName === "message") {
if (action !== "send" && action !== "thread-reply") {
if (!isMessageToolSendActionName(action)) {
return undefined;
}
const toRaw = resolveMessageToolTarget(args);
@@ -552,7 +553,8 @@ export function extractMessagingToolSend(
const providerId = providerHint ? normalizeChannelId(providerHint) : null;
const provider = providerId ?? normalizeOptionalLowercaseString(providerHint) ?? "message";
const to = normalizeTargetForProvider(provider, toRaw);
return to ? { tool: toolName, provider, accountId, to } : undefined;
const threadId = normalizeOptionalString(args.threadId);
return to ? { tool: toolName, provider, accountId, to, threadId } : undefined;
}
const providerId = normalizeChannelId(toolName);
if (!providerId) {