diff --git a/src/discord/send.outbound.ts b/src/discord/send.outbound.ts index 782abad147c..20d7567d472 100644 --- a/src/discord/send.outbound.ts +++ b/src/discord/send.outbound.ts @@ -21,6 +21,7 @@ import { resolveChannelId, sendDiscordMedia, sendDiscordText, + SUPPRESS_NOTIFICATIONS_FLAG, } from "./send.shared.js"; import { ensureOggOpus, @@ -273,9 +274,11 @@ export async function sendPollDiscord( const recipient = await parseAndResolveRecipient(to, opts.accountId); const { channelId } = await resolveChannelId(rest, recipient, request); const content = opts.content?.trim(); + if (poll.durationSeconds !== undefined) { + throw new Error("Discord polls do not support durationSeconds; use durationHours"); + } const payload = normalizeDiscordPollInput(poll); - // Discord message flag for silent/suppress notifications (matches send.shared.ts) - const flags = opts.silent ? 1 << 12 : undefined; + const flags = opts.silent ? SUPPRESS_NOTIFICATIONS_FLAG : undefined; const res = (await request( () => rest.post(Routes.channelMessages(channelId), { diff --git a/src/discord/send.shared.ts b/src/discord/send.shared.ts index 2f62c754e3f..30b9377b009 100644 --- a/src/discord/send.shared.ts +++ b/src/discord/send.shared.ts @@ -232,7 +232,7 @@ async function resolveChannelId( } // Discord message flag for silent/suppress notifications -const SUPPRESS_NOTIFICATIONS_FLAG = 1 << 12; +export const SUPPRESS_NOTIFICATIONS_FLAG = 1 << 12; export function buildDiscordTextChunks( text: string, diff --git a/src/gateway/protocol/schema/agent.ts b/src/gateway/protocol/schema/agent.ts index f9f24f836d0..9eba8b83594 100644 --- a/src/gateway/protocol/schema/agent.ts +++ b/src/gateway/protocol/schema/agent.ts @@ -36,7 +36,7 @@ export const PollParamsSchema = Type.Object( options: Type.Array(NonEmptyString, { minItems: 2, maxItems: 12 }), maxSelections: Type.Optional(Type.Integer({ minimum: 1, maximum: 12 })), /** Poll duration in seconds (channel-specific limits may apply). */ - durationSeconds: Type.Optional(Type.Integer({ minimum: 1, maximum: 600 })), + durationSeconds: Type.Optional(Type.Integer({ minimum: 1, maximum: 604_800 })), durationHours: Type.Optional(Type.Integer({ minimum: 1 })), /** Send silently (no notification) where supported. */ silent: Type.Optional(Type.Boolean()), diff --git a/src/gateway/server-methods/send.ts b/src/gateway/server-methods/send.ts index 674be40c04d..268fd90892f 100644 --- a/src/gateway/server-methods/send.ts +++ b/src/gateway/server-methods/send.ts @@ -303,6 +303,25 @@ export const sendHandlers: GatewayRequestHandlers = { return; } const channel = normalizedChannel ?? DEFAULT_CHAT_CHANNEL; + if (typeof request.durationSeconds === "number" && channel !== "telegram") { + respond( + false, + undefined, + errorShape( + ErrorCodes.INVALID_REQUEST, + "durationSeconds is only supported for Telegram polls", + ), + ); + return; + } + if (typeof request.isAnonymous === "boolean" && channel !== "telegram") { + respond( + false, + undefined, + errorShape(ErrorCodes.INVALID_REQUEST, "isAnonymous is only supported for Telegram polls"), + ); + return; + } const poll = { question: request.question, options: request.options, diff --git a/src/infra/outbound/message-action-runner.ts b/src/infra/outbound/message-action-runner.ts index dfa000bf67b..4d3dbc4bfd4 100644 --- a/src/infra/outbound/message-action-runner.ts +++ b/src/infra/outbound/message-action-runner.ts @@ -59,6 +59,33 @@ export type MessageActionRunnerGateway = { mode: GatewayClientMode; }; +function resolveAndApplyOutboundThreadId( + params: Record, + ctx: { + channel: ChannelId; + to: string; + toolContext?: ChannelThreadingToolContext; + allowSlackAutoThread: boolean; + }, +): string | undefined { + const threadId = readStringParam(params, "threadId"); + const slackAutoThreadId = + ctx.allowSlackAutoThread && ctx.channel === "slack" && !threadId + ? resolveSlackAutoThreadId({ to: ctx.to, toolContext: ctx.toolContext }) + : undefined; + const telegramAutoThreadId = + ctx.channel === "telegram" && !threadId + ? resolveTelegramAutoThreadId({ to: ctx.to, toolContext: ctx.toolContext }) + : undefined; + const resolved = threadId ?? slackAutoThreadId ?? telegramAutoThreadId; + // Write auto-resolved threadId back into params so downstream dispatch + // (plugin `readStringParam(params, "threadId")`) picks it up. + if (resolved && !params.threadId) { + params.threadId = resolved; + } + return resolved ?? undefined; +} + export type RunMessageActionParams = { cfg: OpenClawConfig; action: ChannelMessageActionName; @@ -469,23 +496,12 @@ async function handleSendAction(ctx: ResolvedActionContext): Promise { expect(normalizePollDurationHours(999, { defaultHours: 24, maxHours: 48 })).toBe(48); expect(normalizePollDurationHours(1, { defaultHours: 24, maxHours: 48 })).toBe(1); }); + + it("rejects both durationSeconds and durationHours", () => { + expect(() => + normalizePollInput({ + question: "Q", + options: ["A", "B"], + durationSeconds: 60, + durationHours: 1, + }), + ).toThrow(/mutually exclusive/); + }); }); diff --git a/src/polls.ts b/src/polls.ts index c61d499eaa6..7fe3f800e28 100644 --- a/src/polls.ts +++ b/src/polls.ts @@ -71,6 +71,9 @@ export function normalizePollInput( if (durationHours !== undefined && durationHours < 1) { throw new Error("durationHours must be at least 1"); } + if (durationSeconds !== undefined && durationHours !== undefined) { + throw new Error("durationSeconds and durationHours are mutually exclusive"); + } return { question, options: cleaned,