mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-30 01:06:11 +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.
|
||||
- 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.
|
||||
- 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
|
||||
|
||||
|
||||
@@ -153,8 +153,10 @@ describe("runMessageAction threading auto-injection", () => {
|
||||
});
|
||||
|
||||
const call = mocks.executeSendAction.mock.calls[0]?.[0] as {
|
||||
threadId?: string;
|
||||
ctx?: { params?: Record<string, unknown> };
|
||||
};
|
||||
expect(call?.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 {
|
||||
threadId?: string;
|
||||
ctx?: { params?: Record<string, unknown> };
|
||||
};
|
||||
expect(call?.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,
|
||||
gifPlayback,
|
||||
bestEffort: bestEffort ?? undefined,
|
||||
replyToId: replyToId ?? undefined,
|
||||
threadId: resolvedThreadId ?? undefined,
|
||||
});
|
||||
|
||||
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", () => {
|
||||
beforeEach(async () => {
|
||||
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: {
|
||||
aliases?: string[];
|
||||
outbound: ChannelOutboundAdapter;
|
||||
|
||||
@@ -36,6 +36,8 @@ type MessageSendParams = {
|
||||
mediaUrls?: string[];
|
||||
gifPlayback?: boolean;
|
||||
accountId?: string;
|
||||
replyToId?: string;
|
||||
threadId?: string | number;
|
||||
dryRun?: boolean;
|
||||
bestEffort?: boolean;
|
||||
deps?: OutboundSendDeps;
|
||||
@@ -165,6 +167,8 @@ export async function sendMessage(params: MessageSendParams): Promise<MessageSen
|
||||
to: resolvedTarget.to,
|
||||
accountId: params.accountId,
|
||||
payloads: normalizedPayloads,
|
||||
replyToId: params.replyToId,
|
||||
threadId: params.threadId,
|
||||
gifPlayback: params.gifPlayback,
|
||||
deps: params.deps,
|
||||
bestEffort: params.bestEffort,
|
||||
|
||||
@@ -68,6 +68,8 @@ export async function executeSendAction(params: {
|
||||
mediaUrls?: string[];
|
||||
gifPlayback?: boolean;
|
||||
bestEffort?: boolean;
|
||||
replyToId?: string;
|
||||
threadId?: string | number;
|
||||
}): Promise<{
|
||||
handledBy: "plugin" | "core";
|
||||
payload: unknown;
|
||||
@@ -117,6 +119,8 @@ export async function executeSendAction(params: {
|
||||
mediaUrls: params.mediaUrls,
|
||||
channel: params.ctx.channel || undefined,
|
||||
accountId: params.ctx.accountId ?? undefined,
|
||||
replyToId: params.replyToId,
|
||||
threadId: params.threadId,
|
||||
gifPlayback: params.gifPlayback,
|
||||
dryRun: params.ctx.dryRun,
|
||||
bestEffort: params.bestEffort ?? undefined,
|
||||
|
||||
Reference in New Issue
Block a user