chatboxai/chatbox · error · Error

Failed to mark attachment ${attachment.id} ready

Error message

Failed to mark attachment ${attachment.id} ready

What it means

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.

Source

Thrown at src/main/session-attachment-rag/file-loaders.ts:262

        `${SESSION_ATTACHMENT_RAG_LOG_PREFIX} [FILE] Transition pending -> indexing: attachmentId=${attachment.id}, file="${attachment.filename}"`
      )
      const markedIndexing = await markSessionAttachmentIndexing(attachment.id)
      if (!markedIndexing) {
        log.debug(
          `${SESSION_ATTACHMENT_RAG_LOG_PREFIX} [FILE] Skip attachment that is no longer pending: attachmentId=${attachment.id}, file="${attachment.filename}"`
        )
        continue
      }
      await deleteAttachmentIndex(attachment.id)
      await processAttachment(attachment.id)
      await ensureAttachmentNotCanceled(attachment.id)
      log.debug(
        `${SESSION_ATTACHMENT_RAG_LOG_PREFIX} [FILE] Transition indexing -> ready: attachmentId=${attachment.id}, file="${attachment.filename}"`
      )
      const markedReady = await markSessionAttachmentReady(attachment.id)
      if (!markedReady) {
        await ensureAttachmentNotCanceled(attachment.id)
        throw new Error(`Failed to mark attachment ${attachment.id} ready`)
      }
    } catch (error) {
      if (error instanceof SessionAttachmentCanceledError) {
        log.debug(
          `${SESSION_ATTACHMENT_RAG_LOG_PREFIX} [FILE] Attachment canceled during processing: attachmentId=${attachment.id}, file="${attachment.filename}"`
        )
        await deleteAttachmentGraph(attachment.id)
        continue
      }
      const message = error instanceof Error ? error.message : String(error)
      log.error(
        `${SESSION_ATTACHMENT_RAG_LOG_PREFIX} [FILE] Failed to process attachment ${attachment.id} (${attachment.filename}):`,
        error
      )
      log.debug(
        `${SESSION_ATTACHMENT_RAG_LOG_PREFIX} [FILE] Transition indexing -> failed: attachmentId=${attachment.id}, error=${message}`
      )
      await markSessionAttachmentFailed(attachment.id, message)

View on GitHub (pinned to 81571269ad)

Solutions

  1. Serialize per-attachment processing (lock or lease) so only one worker owns the indexing->ready transition.
  2. Make markSessionAttachmentReady's return value non-fatal: if 0 rows updated, re-read status and treat 'ready'/'canceled' as success/abandon rather than throwing.
  3. Investigate concurrent status mutations in logs (the canceled branch already covers cancellation; check retry/error paths).
  4. Add the current status to the thrown message to speed up diagnosis.

Example fix

// before
if (!markedReady) { await ensureAttachmentNotCanceled(attachment.id); throw new Error(`Failed to mark attachment ${attachment.id} ready`) }

// after: tolerate benign races
if (!markedReady) {
  await ensureAttachmentNotCanceled(attachment.id)
  const fresh = await getSessionAttachment(attachment.id)
  if (fresh?.status === 'ready') return // another path already completed it
  throw new Error(`Failed to mark attachment ${attachment.id} ready (status=${fresh?.status})`)
}
Defensive patterns

Strategy: try-catch

Validate before calling

const cur = await getSessionAttachment(attachment.id)
if (cur?.status !== 'indexing') { /* another path owns it; skip */ return }

Type guard

function isMarkReadyFailed(e: unknown): e is Error { return e instanceof Error && /^Failed to mark attachment \d+ ready/.test(e.message) }

Try / catch

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 }

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of chatboxai/chatbox@81571269ad (2026-08-12). Data as JSON: /api/errors/67732d83ba2d4c29. Report an issue: GitHub.