{"record":{"id":"b9fa24dabbc6b09c","repo":"moeru-ai/airi","slug":"retry-target-has-no-retriable-user-message","errorCode":null,"errorMessage":"Retry target has no retriable user message","messagePattern":"Retry target has no retriable user message","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"warning","filePath":"packages/stage-ui/src/stores/chat.ts","lineNumber":420,"sourceCode":"      appendSendError(payload.sessionId, error)\n      throw error\n    }\n  }\n\n  /** Replaces one stored turn with a new execution of its user message. */\n  async function retry(payload: ChatRetryPayload): Promise<ChatSendResult> {\n    if (!await chatSession.loadSession(payload.sessionId))\n      throw new Error('Failed to load the target chat session')\n\n    const currentMessages = chatSession.getSessionMessages(payload.sessionId)\n    const sourceIndex = retrySourceIndexFrom(currentMessages, payload.index)\n    if (sourceIndex < 0)\n      throw new Error('Retry target has no retriable source message')\n\n    const sourceMessage = currentMessages[sourceIndex]\n    const text = retryTextFrom(sourceMessage)\n    if (!text)\n      throw new Error('Retry target has no retriable user message')\n\n    chatSession.setSessionMessages(payload.sessionId, currentMessages.slice(0, sourceIndex))\n\n    try {\n      return await executeSend({\n        sessionId: payload.sessionId,\n        text,\n        tools: payload.tools ?? sourceMessage?.tools,\n      })\n    }\n    catch (error) {\n      appendSendError(payload.sessionId, error)\n      throw error\n    }\n  }\n\n  /** Runs one stored tool call again and replaces its stored result. */\n  async function rerunToolCall(payload: ChatToolCallRerunPayload): Promise<void> {","sourceCodeStart":402,"sourceCodeEnd":438,"githubUrl":"https://github.com/moeru-ai/airi/blob/677329427f32468c74b17f3ec47eeca4e05bec65/packages/stage-ui/src/stores/chat.ts#L402-L438","documentation":"retry() found the source message via retrySourceIndexFrom, but retryTextFrom(sourceMessage) could not extract any user text from it, so there is no prompt to re-send. This happens when the located user message has empty or missing text content — for example an attachment-only turn with no caption, or a corrupted/incomplete history record.","triggerScenarios":"Retrying a user turn that consisted only of attachments (images/audio) with no text; a history record whose content field is empty due to an interrupted write or partial restore; message objects from an older schema where text lives under a different key.","commonSituations":"Users sending media-only messages then tapping retry; migrated or hand-edited chat histories with empty user turns; older record formats after a schema change in the session store.","solutions":["Disable retry for user turns whose text is empty (attachment-only messages).","If the turn should have text, inspect the stored session record in IndexedDB and repair or drop the broken turn.","Re-send the intended prompt as a new message instead of retrying.","When migrating history formats, normalize user messages so text is present (or filter them out of retriable targets)."],"exampleFix":"// before\nasync function onRetry(index: number) {\n  await chatStore.retry({ sessionId, index })\n}\n\n// after\nasync function onRetry(index: number) {\n  const messages = chatSession.getSessionMessages(sessionId)\n  const source = messages.slice(0, index + 1).reverse().find(m => m.role === 'user')\n  if (!source?.content?.trim()) {\n    toast.info('This turn has no text to retry')\n    return\n  }\n  await chatStore.retry({ sessionId, index })\n}","handlingStrategy":"validation","validationCode":"const messages = chatSession.getSessionMessages(sessionId)\nconst source = messages.slice(0, payload.index + 1).reverse().find(m => m.role === 'user')\nif (!source?.content?.trim()) {\n  toast.info('This turn has no text to retry')\n  return\n}\nawait chatStore.retry(payload)","typeGuard":"function hasRetriableUserText(messages: ChatHistoryItem[], index: number): boolean {\n  const source = messages.slice(0, index + 1).reverse().find(m => m.role === 'user')\n  return Boolean(source?.content?.trim())\n}","tryCatchPattern":"try {\n  await chatStore.retry({ sessionId, index })\n}\ncatch (error) {\n  const message = errorMessageFrom(error) ?? ''\n  if (message === 'Retry target has no retriable user message') {\n    toast.info('Cannot retry a turn without text (e.g. attachments only)')\n    return\n  }\n  throw error\n}","preventionTips":["Disable retry on user turns that contain only attachments.","Normalize migrated histories so user messages always carry text.","Validate imported session records before offering message actions.","Educate users that media-only turns cannot be retried."],"tags":["chat","retry","validation","message-history"],"backgroundTag":"invalid-retry-target","analyzedSha":"677329427f32468c74b17f3ec47eeca4e05bec65","analyzedAt":"2026-08-18T17:29:58.153Z","schemaVersion":2},"datasetVersion":"2026-08-23T11:17:13.642Z"}