diff --git a/src/agents/tools/common.params.test.ts b/src/agents/tools/common.params.test.ts index d93038cd606..32eb63d036e 100644 --- a/src/agents/tools/common.params.test.ts +++ b/src/agents/tools/common.params.test.ts @@ -48,6 +48,16 @@ describe("readNumberParam", () => { expect(readNumberParam(params, "messageId")).toBe(42); }); + it("keeps partial parse behavior by default", () => { + const params = { messageId: "42abc" }; + expect(readNumberParam(params, "messageId")).toBe(42); + }); + + it("rejects partial numeric strings when strict is enabled", () => { + const params = { messageId: "42abc" }; + expect(readNumberParam(params, "messageId", { strict: true })).toBeUndefined(); + }); + it("truncates when integer is true", () => { const params = { messageId: "42.9" }; expect(readNumberParam(params, "messageId", { integer: true })).toBe(42); diff --git a/src/agents/tools/common.ts b/src/agents/tools/common.ts index d4b3bc9fc3b..19cca2d7927 100644 --- a/src/agents/tools/common.ts +++ b/src/agents/tools/common.ts @@ -129,9 +129,9 @@ export function readStringOrNumberParam( export function readNumberParam( params: Record, key: string, - options: { required?: boolean; label?: string; integer?: boolean } = {}, + options: { required?: boolean; label?: string; integer?: boolean; strict?: boolean } = {}, ): number | undefined { - const { required = false, label = key, integer = false } = options; + const { required = false, label = key, integer = false, strict = false } = options; const raw = readParamRaw(params, key); let value: number | undefined; if (typeof raw === "number" && Number.isFinite(raw)) { @@ -139,7 +139,7 @@ export function readNumberParam( } else if (typeof raw === "string") { const trimmed = raw.trim(); if (trimmed) { - const parsed = Number.parseFloat(trimmed); + const parsed = strict ? Number(trimmed) : Number.parseFloat(trimmed); if (Number.isFinite(parsed)) { value = parsed; } diff --git a/src/channels/plugins/actions/actions.test.ts b/src/channels/plugins/actions/actions.test.ts index b369bcde768..a6e1e89fc2e 100644 --- a/src/channels/plugins/actions/actions.test.ts +++ b/src/channels/plugins/actions/actions.test.ts @@ -348,6 +348,25 @@ describe("handleDiscordMessageAction", () => { allowMultiselect: true, }, }, + { + name: "rejects partially numeric poll duration for discord poll adapter params", + input: { + action: "poll" as const, + params: { + to: "channel:123", + pollQuestion: "Ready?", + pollOption: ["Yes", "No"], + pollDurationHours: "24h", + }, + }, + expected: { + action: "poll", + to: "channel:123", + question: "Ready?", + answers: ["Yes", "No"], + durationHours: undefined, + }, + }, { name: "forwards accountId for thread replies", input: { @@ -734,6 +753,30 @@ describe("telegramMessageActions", () => { accountId: undefined, }, }, + { + name: "poll rejects partially numeric duration strings before telegram action handoff", + action: "poll" as const, + params: { + to: "123", + pollQuestion: "Ready?", + pollOption: ["Yes", "No"], + pollDurationSeconds: "60s", + }, + expectedPayload: { + action: "poll", + to: "123", + question: "Ready?", + answers: ["Yes", "No"], + allowMultiselect: undefined, + durationHours: undefined, + durationSeconds: undefined, + replyToMessageId: undefined, + messageThreadId: undefined, + isAnonymous: undefined, + silent: undefined, + accountId: undefined, + }, + }, { name: "topic-create maps to createForumTopic", action: "topic-create" as const, diff --git a/src/channels/plugins/actions/discord/handle-action.ts b/src/channels/plugins/actions/discord/handle-action.ts index 4484d2abd42..5b11246210a 100644 --- a/src/channels/plugins/actions/discord/handle-action.ts +++ b/src/channels/plugins/actions/discord/handle-action.ts @@ -91,6 +91,7 @@ export async function handleDiscordMessageAction( const allowMultiselect = readBooleanParam(params, "pollMulti"); const durationHours = readNumberParam(params, "pollDurationHours", { integer: true, + strict: true, }); return await handleDiscordAction( { diff --git a/src/channels/plugins/actions/telegram.ts b/src/channels/plugins/actions/telegram.ts index eba844fdc4b..6e55349698b 100644 --- a/src/channels/plugins/actions/telegram.ts +++ b/src/channels/plugins/actions/telegram.ts @@ -159,9 +159,11 @@ export const telegramMessageActions: ChannelMessageActionAdapter = { const answers = readStringArrayParam(params, "pollOption", { required: true }); const durationHours = readNumberParam(params, "pollDurationHours", { integer: true, + strict: true, }); const durationSeconds = readNumberParam(params, "pollDurationSeconds", { integer: true, + strict: true, }); const replyToMessageId = readNumberParam(params, "replyTo", { integer: true }); const messageThreadId = readNumberParam(params, "threadId", { integer: true }); diff --git a/src/infra/outbound/message-action-runner.ts b/src/infra/outbound/message-action-runner.ts index 1c397f582e8..c703cd34d24 100644 --- a/src/infra/outbound/message-action-runner.ts +++ b/src/infra/outbound/message-action-runner.ts @@ -584,9 +584,11 @@ async function handlePollAction(ctx: ResolvedActionContext): Promise