mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 16:34:33 +00:00
fix: thread replyToId and threadId through message tool send action (#14948)
* fix: thread replyToId and threadId through message tool send action * fix: omit replyToId/threadId from gateway send params * fix: add threading seam regression coverage (#14948) (thanks @mcaxtr) --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
@@ -13,6 +13,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
- Heartbeat: prevent scheduler silent-death races during runner reloads, preserve retry cooldown backoff under wake bursts, and prioritize user/action wake causes over interval/retry reasons when coalescing. (#15108) Thanks @joeykrug.
|
- Heartbeat: prevent scheduler silent-death races during runner reloads, preserve retry cooldown backoff under wake bursts, and prioritize user/action wake causes over interval/retry reasons when coalescing. (#15108) Thanks @joeykrug.
|
||||||
- Exec/Allowlist: allow multiline heredoc bodies (`<<`, `<<-`) while keeping multiline non-heredoc shell commands blocked, so exec approval parsing permits heredoc input safely without allowing general newline command chaining. (#13811) Thanks @mcaxtr.
|
- Exec/Allowlist: allow multiline heredoc bodies (`<<`, `<<-`) while keeping multiline non-heredoc shell commands blocked, so exec approval parsing permits heredoc input safely without allowing general newline command chaining. (#13811) Thanks @mcaxtr.
|
||||||
- Docs/Mermaid: remove hardcoded Mermaid init theme blocks from four docs diagrams so dark mode inherits readable theme defaults. (#15157) Thanks @heytulsiprasad.
|
- Docs/Mermaid: remove hardcoded Mermaid init theme blocks from four docs diagrams so dark mode inherits readable theme defaults. (#15157) Thanks @heytulsiprasad.
|
||||||
|
- Outbound/Threading: pass `replyTo` and `threadId` from `message send` tool actions through the core outbound send path to channel adapters, preserving thread/reply routing. (#14948) Thanks @mcaxtr.
|
||||||
|
|
||||||
## 2026.2.12
|
## 2026.2.12
|
||||||
|
|
||||||
|
|||||||
@@ -153,8 +153,10 @@ describe("runMessageAction threading auto-injection", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const call = mocks.executeSendAction.mock.calls[0]?.[0] as {
|
const call = mocks.executeSendAction.mock.calls[0]?.[0] as {
|
||||||
|
threadId?: string;
|
||||||
ctx?: { params?: Record<string, unknown> };
|
ctx?: { params?: Record<string, unknown> };
|
||||||
};
|
};
|
||||||
|
expect(call?.threadId).toBe("42");
|
||||||
expect(call?.ctx?.params?.threadId).toBe("42");
|
expect(call?.ctx?.params?.threadId).toBe("42");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -235,8 +237,40 @@ describe("runMessageAction threading auto-injection", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const call = mocks.executeSendAction.mock.calls[0]?.[0] as {
|
const call = mocks.executeSendAction.mock.calls[0]?.[0] as {
|
||||||
|
threadId?: string;
|
||||||
ctx?: { params?: Record<string, unknown> };
|
ctx?: { params?: Record<string, unknown> };
|
||||||
};
|
};
|
||||||
|
expect(call?.threadId).toBe("999");
|
||||||
expect(call?.ctx?.params?.threadId).toBe("999");
|
expect(call?.ctx?.params?.threadId).toBe("999");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("threads explicit replyTo through executeSendAction", async () => {
|
||||||
|
mocks.executeSendAction.mockResolvedValue({
|
||||||
|
handledBy: "plugin",
|
||||||
|
payload: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
await runMessageAction({
|
||||||
|
cfg: telegramConfig,
|
||||||
|
action: "send",
|
||||||
|
params: {
|
||||||
|
channel: "telegram",
|
||||||
|
target: "telegram:123",
|
||||||
|
message: "hi",
|
||||||
|
replyTo: "777",
|
||||||
|
},
|
||||||
|
toolContext: {
|
||||||
|
currentChannelId: "telegram:123",
|
||||||
|
currentThreadTs: "42",
|
||||||
|
},
|
||||||
|
agentId: "main",
|
||||||
|
});
|
||||||
|
|
||||||
|
const call = mocks.executeSendAction.mock.calls[0]?.[0] as {
|
||||||
|
replyToId?: string;
|
||||||
|
ctx?: { params?: Record<string, unknown> };
|
||||||
|
};
|
||||||
|
expect(call?.replyToId).toBe("777");
|
||||||
|
expect(call?.ctx?.params?.replyTo).toBe("777");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -891,6 +891,8 @@ async function handleSendAction(ctx: ResolvedActionContext): Promise<MessageActi
|
|||||||
mediaUrls: mergedMediaUrls.length ? mergedMediaUrls : undefined,
|
mediaUrls: mergedMediaUrls.length ? mergedMediaUrls : undefined,
|
||||||
gifPlayback,
|
gifPlayback,
|
||||||
bestEffort: bestEffort ?? undefined,
|
bestEffort: bestEffort ?? undefined,
|
||||||
|
replyToId: replyToId ?? undefined,
|
||||||
|
threadId: resolvedThreadId ?? undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -80,6 +80,62 @@ describe("sendMessage channel normalization", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("sendMessage replyToId threading", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
callGatewayMock.mockReset();
|
||||||
|
vi.resetModules();
|
||||||
|
await setRegistry(emptyRegistry);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await setRegistry(emptyRegistry);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("passes replyToId through to the outbound adapter", async () => {
|
||||||
|
const { sendMessage } = await loadMessage();
|
||||||
|
const capturedCtx: Record<string, unknown>[] = [];
|
||||||
|
const plugin = createMattermostLikePlugin({
|
||||||
|
onSendText: (ctx) => {
|
||||||
|
capturedCtx.push(ctx);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await setRegistry(createTestRegistry([{ pluginId: "mattermost", source: "test", plugin }]));
|
||||||
|
|
||||||
|
await sendMessage({
|
||||||
|
cfg: {},
|
||||||
|
to: "channel:town-square",
|
||||||
|
content: "thread reply",
|
||||||
|
channel: "mattermost",
|
||||||
|
replyToId: "post123",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(capturedCtx).toHaveLength(1);
|
||||||
|
expect(capturedCtx[0]?.replyToId).toBe("post123");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("passes threadId through to the outbound adapter", async () => {
|
||||||
|
const { sendMessage } = await loadMessage();
|
||||||
|
const capturedCtx: Record<string, unknown>[] = [];
|
||||||
|
const plugin = createMattermostLikePlugin({
|
||||||
|
onSendText: (ctx) => {
|
||||||
|
capturedCtx.push(ctx);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await setRegistry(createTestRegistry([{ pluginId: "mattermost", source: "test", plugin }]));
|
||||||
|
|
||||||
|
await sendMessage({
|
||||||
|
cfg: {},
|
||||||
|
to: "channel:town-square",
|
||||||
|
content: "topic reply",
|
||||||
|
channel: "mattermost",
|
||||||
|
threadId: "topic456",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(capturedCtx).toHaveLength(1);
|
||||||
|
expect(capturedCtx[0]?.threadId).toBe("topic456");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("sendPoll channel normalization", () => {
|
describe("sendPoll channel normalization", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
callGatewayMock.mockReset();
|
callGatewayMock.mockReset();
|
||||||
@@ -151,6 +207,32 @@ const createMSTeamsOutbound = (opts?: { includePoll?: boolean }): ChannelOutboun
|
|||||||
: {}),
|
: {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const createMattermostLikePlugin = (opts: {
|
||||||
|
onSendText: (ctx: Record<string, unknown>) => void;
|
||||||
|
}): ChannelPlugin => ({
|
||||||
|
id: "mattermost",
|
||||||
|
meta: {
|
||||||
|
id: "mattermost",
|
||||||
|
label: "Mattermost",
|
||||||
|
selectionLabel: "Mattermost",
|
||||||
|
docsPath: "/channels/mattermost",
|
||||||
|
blurb: "Mattermost test stub.",
|
||||||
|
},
|
||||||
|
capabilities: { chatTypes: ["direct", "channel"] },
|
||||||
|
config: {
|
||||||
|
listAccountIds: () => ["default"],
|
||||||
|
resolveAccount: () => ({}),
|
||||||
|
},
|
||||||
|
outbound: {
|
||||||
|
deliveryMode: "direct",
|
||||||
|
sendText: async (ctx) => {
|
||||||
|
opts.onSendText(ctx as unknown as Record<string, unknown>);
|
||||||
|
return { channel: "mattermost", messageId: "m1" };
|
||||||
|
},
|
||||||
|
sendMedia: async () => ({ channel: "mattermost", messageId: "m2" }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const createMSTeamsPlugin = (params: {
|
const createMSTeamsPlugin = (params: {
|
||||||
aliases?: string[];
|
aliases?: string[];
|
||||||
outbound: ChannelOutboundAdapter;
|
outbound: ChannelOutboundAdapter;
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ type MessageSendParams = {
|
|||||||
mediaUrls?: string[];
|
mediaUrls?: string[];
|
||||||
gifPlayback?: boolean;
|
gifPlayback?: boolean;
|
||||||
accountId?: string;
|
accountId?: string;
|
||||||
|
replyToId?: string;
|
||||||
|
threadId?: string | number;
|
||||||
dryRun?: boolean;
|
dryRun?: boolean;
|
||||||
bestEffort?: boolean;
|
bestEffort?: boolean;
|
||||||
deps?: OutboundSendDeps;
|
deps?: OutboundSendDeps;
|
||||||
@@ -165,6 +167,8 @@ export async function sendMessage(params: MessageSendParams): Promise<MessageSen
|
|||||||
to: resolvedTarget.to,
|
to: resolvedTarget.to,
|
||||||
accountId: params.accountId,
|
accountId: params.accountId,
|
||||||
payloads: normalizedPayloads,
|
payloads: normalizedPayloads,
|
||||||
|
replyToId: params.replyToId,
|
||||||
|
threadId: params.threadId,
|
||||||
gifPlayback: params.gifPlayback,
|
gifPlayback: params.gifPlayback,
|
||||||
deps: params.deps,
|
deps: params.deps,
|
||||||
bestEffort: params.bestEffort,
|
bestEffort: params.bestEffort,
|
||||||
|
|||||||
@@ -68,6 +68,8 @@ export async function executeSendAction(params: {
|
|||||||
mediaUrls?: string[];
|
mediaUrls?: string[];
|
||||||
gifPlayback?: boolean;
|
gifPlayback?: boolean;
|
||||||
bestEffort?: boolean;
|
bestEffort?: boolean;
|
||||||
|
replyToId?: string;
|
||||||
|
threadId?: string | number;
|
||||||
}): Promise<{
|
}): Promise<{
|
||||||
handledBy: "plugin" | "core";
|
handledBy: "plugin" | "core";
|
||||||
payload: unknown;
|
payload: unknown;
|
||||||
@@ -117,6 +119,8 @@ export async function executeSendAction(params: {
|
|||||||
mediaUrls: params.mediaUrls,
|
mediaUrls: params.mediaUrls,
|
||||||
channel: params.ctx.channel || undefined,
|
channel: params.ctx.channel || undefined,
|
||||||
accountId: params.ctx.accountId ?? undefined,
|
accountId: params.ctx.accountId ?? undefined,
|
||||||
|
replyToId: params.replyToId,
|
||||||
|
threadId: params.threadId,
|
||||||
gifPlayback: params.gifPlayback,
|
gifPlayback: params.gifPlayback,
|
||||||
dryRun: params.ctx.dryRun,
|
dryRun: params.ctx.dryRun,
|
||||||
bestEffort: params.bestEffort ?? undefined,
|
bestEffort: params.bestEffort ?? undefined,
|
||||||
|
|||||||
Reference in New Issue
Block a user