{"record":{"id":"b0dc0cb8b2b01dfc","repo":"antiwork/gumroad","slug":"message-is-required","errorCode":null,"errorMessage":"Message is required","messagePattern":"Message is required","errorType":"exception","errorClass":"Ai::StoreAgentService::Error","httpStatus":422,"severity":"error","filePath":"app/services/ai/store_agent_service.rb","lineNumber":1201,"sourceCode":"        next unless %w[user assistant].include?(role)\n\n        proposal_state = msg[:proposal_state] || msg[\"proposal_state\"]\n        state_suffix =\n          if role == \"assistant\" && proposal_state.present?\n            \"\\n\\n[Server proposal state: #{proposal_state}]\"\n          else\n            \"\"\n          end\n        limit = index == current_message_index ? MAX_CURRENT_MESSAGE_LENGTH : MAX_MESSAGE_LENGTH\n        content = content.truncate(limit - state_suffix.length, omission: \"...\")\n        { role:, content: \"#{content}#{state_suffix}\" }\n      end\n\n      # Anthropic's Messages API requires the conversation to START with a user message. The web chat\n      # always opens with a canned assistant greeting (and a turn could begin with other leading\n      # assistant turns), so drop any leading assistant messages before the first user message.\n      history = history.drop_while { |m| m[:role] != \"user\" }\n      raise Error, \"Message is required\" if history.empty? || history.last[:role] != \"user\"\n\n      history\n    end\n\n    # Assemble the system prompt with the live read/write endpoint manifests embedded, so the model\n    # is told exactly which endpoint ids exist and what each does.\n    def system_prompt\n      format(\n        SYSTEM_PROMPT_HEADER,\n        reads: Ai::StoreAgentApiCatalog.manifest(:read),\n        writes: Ai::StoreAgentApiCatalog.manifest(:write),\n      )\n    end\n\n    # Two generic API tools drive the whole catalog. `complete_turn` is a terminal marker: unlike\n    # the API tools it never runs an operation, and the service validates it before accepting the\n    # model's text as the final seller-facing reply.\n    def run_tool(name:, arguments:)","sourceCodeStart":1183,"sourceCodeEnd":1219,"githubUrl":"https://github.com/antiwork/gumroad/blob/afeacbd394069a1cbf0c6c50ee8e900925050370/app/services/ai/store_agent_service.rb#L1183-L1219","documentation":"Raised as Ai::StoreAgentService::Error from #build_conversation (app/services/ai/store_agent_service.rb:1201). The service assembles an Anthropic Messages API conversation from client-supplied chat history: it keeps only non-blank messages with role 'user' or 'assistant', then drops leading assistant messages (the web chat opens with a canned assistant greeting) because Anthropic requires the conversation to start with a user message. If the filtered history is empty or the last remaining message is not a 'user' turn, there is no valid user turn to send and the service raises 'Message is required'.","triggerScenarios":"Calling the store agent with messages: [] or messages whose :content is blank/whitespace-only (blank content is filtered out by `next if content.blank?`); sending only assistant-role turns (all dropped by drop_while); sending roles like 'system' or 'tool' (rejected by the %w[user assistant] allowlist); or sending history whose last non-blank message is an assistant turn (`history.last[:role] != \"user\"`).","commonSituations":"Frontend sends the greeting-only initial state of the chat; a client sends messages as JSON strings or with symbol/string key mismatch handled but content empty; the current-message input box is submitted empty; roles are typo'd ('User', 'bot'); or a conversation consisting solely of the canned assistant greeting plus a system-style message. Also hits when the MAX_HISTORY_MESSAGES window happens to contain only assistant turns.","solutions":["Ensure the request includes at least one message with role exactly 'user' and non-blank content — typically the buyer's latest chat input.","Make the last element of the messages array the user's new message; append it before calling the service rather than relying on the service to inject it.","Filter client-side before calling: drop blank-content messages and validate every role is 'user' or 'assistant'.","If you pass the chat window, verify the windowed slice still contains a user turn (window = Array(messages).last(MAX_HISTORY_MESSAGES)).","In the caller, rescue Ai::StoreAgentService::Error and map it to a 400 'message is required' response instead of a 500."],"exampleFix":"// before\nresult = Ai::StoreAgentService.new(seller:, messages: params[:messages]).call\n\n// after\nmessages = Array(params[:messages]).select { |m| %w[user assistant].include?(m[:role].to_s) && m[:content].to_s.strip.present? }\nraise ArgumentError, \"Message is required\" unless messages.any? { |m| m[:role] == \"user\" }\nresult = Ai::StoreAgentService.new(seller:, messages:).call","handlingStrategy":"validation","validationCode":"def valid_agent_history?(messages)\n  window = Array(messages).last(MAX_HISTORY_MESSAGES)\n  kept = window.select do |m|\n    %w[user assistant].include?((m[:role] || m[\"role\"]).to_s) &&\n      (m[:content] || m[\"content\"]).to_s.strip.present?\n  end\n  kept.any? { |m| (m[:role] || m[\"role\"]) == \"user\" } && (m = kept.last) && (m[:role] || m[\"role\"]) == \"user\"\nend\n\nraise ArgumentError, \"Message is required\" unless valid_agent_history?(messages)","typeGuard":"# Ruby guard mirroring Ai::StoreAgentService#build_conversation filtering\ndef agent_history_has_user_turn?(messages)\n  Array(messages).any? { |m| m[:role].to_s == \"user\" && m[:content].to_s.strip.present? }\nend","tryCatchPattern":"begin\n  Ai::StoreAgentService.new(seller:, messages:).call\nrescue Ai::StoreAgentService::Error => e\n  render json: { error: e.message }, status: :bad_request # 'Message is required' is caller input, not a 500\nend","preventionTips":["Append the user's new message as the last element of the history array before calling the service.","Reject empty/whitespace submissions at the form layer.","Restrict roles client-side to 'user' and 'assistant' only."],"tags":["ai","anthropic","chat","request-validation","ruby","rails"],"backgroundTag":"llm-message-history-invalid","analyzedSha":"afeacbd394069a1cbf0c6c50ee8e900925050370","analyzedAt":"2026-08-21T17:58:52.159Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}