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
- 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.
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
- 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.
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
- Only failed session attachments can be retried
- Session attachment ${id} not found
- Attachment content not found or empty
- Attachment did not produce any retrievable chunks
- session attachment embedding model not set
AI-assisted analysis of chatboxai/chatbox@81571269ad (2026-08-12).
Data as JSON: /api/errors/67732d83ba2d4c29.
Report an issue: GitHub.