{"record":{"id":"bdab075f5a1bb915","repo":"moeru-ai/airi","slug":"retry-target-has-no-retriable-source-message","errorCode":null,"errorMessage":"Retry target has no retriable source message","messagePattern":"Retry target has no retriable source message","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"warning","filePath":"packages/stage-ui/src/stores/chat.ts","lineNumber":415,"sourceCode":"  async function send(payload: ChatSendPayload): Promise<ChatSendResult> {\n    try {\n      return await executeSend(payload)\n    }\n    catch (error) {\n      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","sourceCodeStart":397,"sourceCodeEnd":433,"githubUrl":"https://github.com/moeru-ai/airi/blob/677329427f32468c74b17f3ec47eeca4e05bec65/packages/stage-ui/src/stores/chat.ts#L397-L433","documentation":"retry() maps the target message to the user message that produced the turn using retrySourceIndexFrom; a negative result means no retriable source message exists for the requested index. Concretely, there is no user-role message at or before that position (or the index is out of bounds), so there is nothing to re-execute — e.g. retrying the opening greeting.","triggerScenarios":"Retrying the first assistant greeting (no preceding user message); retrying an 'error' role message appended by appendSendError; index-based retry after messages were deleted so indices shifted; passing an index past the end of the message list.","commonSituations":"Retry UI rendered on every message including greetings and error stubs; long histories where the user retries after manually deleting earlier turns; component state holding an old index after the list re-rendered.","solutions":["Only render retry affordances on assistant turns that have a preceding user message.","Prefer addressing the retry target by stable message id rather than array index.","Refresh messages and recompute indices immediately before retrying.","If it still fires, treat it as a no-op in the UI instead of an error toast."],"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 hasUserSource = messages.slice(0, index + 1).some(m => m.role === 'user')\n  if (!hasUserSource)\n    return // nothing retriable (e.g. greeting or error stub)\n  await chatStore.retry({ sessionId, index })\n}","handlingStrategy":"validation","validationCode":"const messages = chatSession.getSessionMessages(sessionId)\nconst hasRetriableSource = messages\n  .slice(0, payload.index + 1)\n  .some(message => message.role === 'user' && message.content?.trim())\nif (!hasRetriableSource)\n  return // nothing to retry (greeting, error stub, or shifted index)\nawait chatStore.retry(payload)","typeGuard":"function isRetriableMessageIndex(messages: ChatHistoryItem[], index: number): boolean {\n  if (index < 0 || index >= messages.length) return false\n  return messages.slice(0, index + 1).some(m => m.role === 'user' && Boolean(m.content?.trim()))\n}","tryCatchPattern":"try {\n  await chatStore.retry({ sessionId, index })\n}\ncatch (error) {\n  if (errorMessageFrom(error) === 'Retry target has no retriable source message')\n    return // benign UI misuse: ignore\n  throw error\n}","preventionTips":["Do not attach retry buttons to greetings or 'error' role stubs.","Address retry targets by stable message id when the API allows it.","Refresh the message list before computing retry indices.","Treat this message as a no-op signal rather than an error toast."],"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"}