{"record":{"id":"c531502c786369d9","repo":"openclaw/openclaw","slug":"title-must-be-80-characters-or-fewer","errorCode":null,"errorMessage":"title must be 80 characters or fewer","messagePattern":"title must be 80 characters or fewer","errorType":"exception","errorClass":"WidgetHtmlInputError","httpStatus":null,"severity":"error","filePath":"extensions/discord/src/activities/tool.ts","lineNumber":127,"sourceCode":"\n  return {\n    label: variant.label,\n    name: variant.name,\n    description: variant.description,\n    parameters: variant.parameters,\n    execute: async (_toolCallId, rawParams) => {\n      const params = rawParams as Record<string, unknown>;\n      const html = readStringParam(params, variant.htmlParam, { required: true, trim: false });\n      const title = readStringParam(params, \"title\", { required: true });\n      const buttonLabel = readStringParam(params, \"button_label\") || \"Open widget\";\n      if (!html.trim()) {\n        throw new WidgetHtmlInputError(`${variant.htmlParam} is required`);\n      }\n      assertWidgetHtmlSize(html, DISCORD_WIDGET_HTML_MAX_BYTES, {\n        inputName: variant.htmlParam,\n      });\n      if (title.length > 80) {\n        throw new WidgetHtmlInputError(\"title must be 80 characters or fewer\");\n      }\n      if (!buttonLabel.trim() || buttonLabel.length > 80) {\n        throw new WidgetHtmlInputError(\"button_label must be 1 to 80 characters\");\n      }\n      const channelId = resolveDiscordChannelId(context);\n      if (!channelId) {\n        throw new WidgetHtmlInputError(\n          `${variant.name} requires a concrete Discord channel in the current session`,\n        );\n      }\n      // Persist before the button can be delivered so a launch never races an absent record;\n      // roll the record back if the post fails so a failed send leaves no unreachable widget.\n      const widgetId = await deps.runtime.store.createWidget({\n        html: buildDiscordWidgetDocument(title, html),\n        title,\n        channelId,\n        accountId: account.accountId,\n        createdAt: (deps.now ?? Date.now)(),","sourceCodeStart":109,"sourceCodeEnd":145,"githubUrl":"https://github.com/openclaw/openclaw/blob/01804a75319da4b69c9ab98ceaa30477e22b8c0b/extensions/discord/src/activities/tool.ts#L109-L145","documentation":"Thrown by the widget tool when the title param exceeds 80 characters. Discord Activity launch buttons and widget titles have a hard length cap; the Typebox schema (maxLength: 80) declares it statically, but this runtime WidgetHtmlInputError enforces it as a defense-in-depth check since raw params may bypass schema validation in some paths.","triggerScenarios":"Passing a title longer than 80 characters; the model generates a verbose title; a dynamic title from user data exceeds the limit.","commonSituations":"Long descriptive titles generated without length awareness; concatenating dynamic segments that overflow.","solutions":["Truncate the title to 80 characters or fewer before calling the tool.","If the title comes from user content, apply a truncation/ellipsis strategy upstream."],"exampleFix":"// before\ndiscord_widget({ html: '...', title: 'A'.repeat(90) }) // throws\n// after\ndiscord_widget({ html: '...', title: longTitle.slice(0, 80) })","handlingStrategy":"validation","validationCode":"function clampTitle(title: string): string {\n  return title.length > 80 ? title.slice(0, 80) : title;\n}","typeGuard":"function isValidWidgetTitle(title: unknown): boolean {\n  return typeof title === 'string' && title.length >= 1 && title.length <= 80;\n}","tryCatchPattern":null,"preventionTips":["Cap title length to 80 characters at the source.","Apply truncation when titles come from dynamic content."],"tags":["discord","widget","title","validation","length-limit"],"backgroundTag":null,"analyzedSha":"01804a75319da4b69c9ab98ceaa30477e22b8c0b","analyzedAt":"2026-08-12T04:37:58.197Z","schemaVersion":2},"datasetVersion":"2026-08-13T14:17:21.547Z"}