{"record":{"id":"67732d83ba2d4c29","repo":"chatboxai/chatbox","slug":"failed-to-mark-attachment-attachment-id-ready","errorCode":null,"errorMessage":"Failed to mark attachment ${attachment.id} ready","messagePattern":"Failed to mark attachment (.+?) ready","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/main/session-attachment-rag/file-loaders.ts","lineNumber":262,"sourceCode":"        `${SESSION_ATTACHMENT_RAG_LOG_PREFIX} [FILE] Transition pending -> indexing: attachmentId=${attachment.id}, file=\"${attachment.filename}\"`\n      )\n      const markedIndexing = await markSessionAttachmentIndexing(attachment.id)\n      if (!markedIndexing) {\n        log.debug(\n          `${SESSION_ATTACHMENT_RAG_LOG_PREFIX} [FILE] Skip attachment that is no longer pending: attachmentId=${attachment.id}, file=\"${attachment.filename}\"`\n        )\n        continue\n      }\n      await deleteAttachmentIndex(attachment.id)\n      await processAttachment(attachment.id)\n      await ensureAttachmentNotCanceled(attachment.id)\n      log.debug(\n        `${SESSION_ATTACHMENT_RAG_LOG_PREFIX} [FILE] Transition indexing -> ready: attachmentId=${attachment.id}, file=\"${attachment.filename}\"`\n      )\n      const markedReady = await markSessionAttachmentReady(attachment.id)\n      if (!markedReady) {\n        await ensureAttachmentNotCanceled(attachment.id)\n        throw new Error(`Failed to mark attachment ${attachment.id} ready`)\n      }\n    } catch (error) {\n      if (error instanceof SessionAttachmentCanceledError) {\n        log.debug(\n          `${SESSION_ATTACHMENT_RAG_LOG_PREFIX} [FILE] Attachment canceled during processing: attachmentId=${attachment.id}, file=\"${attachment.filename}\"`\n        )\n        await deleteAttachmentGraph(attachment.id)\n        continue\n      }\n      const message = error instanceof Error ? error.message : String(error)\n      log.error(\n        `${SESSION_ATTACHMENT_RAG_LOG_PREFIX} [FILE] Failed to process attachment ${attachment.id} (${attachment.filename}):`,\n        error\n      )\n      log.debug(\n        `${SESSION_ATTACHMENT_RAG_LOG_PREFIX} [FILE] Transition indexing -> failed: attachmentId=${attachment.id}, error=${message}`\n      )\n      await markSessionAttachmentFailed(attachment.id, message)","sourceCodeStart":244,"sourceCodeEnd":280,"githubUrl":"https://github.com/chatboxai/chatbox/blob/81571269addb6bafb589a920b2883f1e1e084fd1/src/main/session-attachment-rag/file-loaders.ts#L244-L280","documentation":"Thrown during the indexing->ready transition when markSessionAttachmentReady(attachment.id) returns falsy. Before throwing, the code re-checks ensureAttachmentNotCanceled so a cancel between processing and markReady surfaces as SessionAttachmentCanceledError instead. A falsy markReady therefore means the UPDATE matched zero rows (status was mutated by another path) rather than cancellation.","triggerScenarios":"markSessionAttachmentReady's UPDATE ... WHERE status='indexing' (or similar) matched 0 rows because a concurrent retry/cancel/error path already changed status away from 'indexing'. The processing pipeline finished but the DB no longer reflects the state it expected.","commonSituations":"Two workers processing the same attachment; a user-triggered cancel/retry that raced with completion; a DB trigger or external mutation changed status; status field type/payload drift so the WHERE clause never matches.","solutions":["Serialize per-attachment processing (lock or lease) so only one worker owns the indexing->ready transition.","Make markSessionAttachmentReady's return value non-fatal: if 0 rows updated, re-read status and treat 'ready'/'canceled' as success/abandon rather than throwing.","Investigate concurrent status mutations in logs (the canceled branch already covers cancellation; check retry/error paths).","Add the current status to the thrown message to speed up diagnosis."],"exampleFix":"// before\nif (!markedReady) { await ensureAttachmentNotCanceled(attachment.id); throw new Error(`Failed to mark attachment ${attachment.id} ready`) }\n\n// after: tolerate benign races\nif (!markedReady) {\n  await ensureAttachmentNotCanceled(attachment.id)\n  const fresh = await getSessionAttachment(attachment.id)\n  if (fresh?.status === 'ready') return // another path already completed it\n  throw new Error(`Failed to mark attachment ${attachment.id} ready (status=${fresh?.status})`)\n}","handlingStrategy":"try-catch","validationCode":"const cur = await getSessionAttachment(attachment.id)\nif (cur?.status !== 'indexing') { /* another path owns it; skip */ return }","typeGuard":"function isMarkReadyFailed(e: unknown): e is Error { return e instanceof Error && /^Failed to mark attachment \\d+ ready/.test(e.message) }","tryCatchPattern":"try { await processAttachment(id); await markSessionAttachmentReady(id) } catch (e) { if (isMarkReadyFailed(e)) { const fresh = await getSessionAttachment(id); if (fresh?.status === 'ready') return /* benign race */ } throw e }","preventionTips":["Serialize per-attachment processing with a lease/lock.","Make markReady idempotent and treat 'already ready' as success.","Avoid concurrent retry/cancel on the same attachment."],"tags":["session-attachment-rag","database","concurrency","state-machine","typescript"],"backgroundTag":null,"analyzedSha":"81571269addb6bafb589a920b2883f1e1e084fd1","analyzedAt":"2026-08-12T21:51:44.981Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}