janhq/jan · error · Error
File ' ' has already been attached to this thread
Error message
File '${file.name}' has already been attached to this thread What it means
ingestFile guards against attaching the same file twice to a thread: it lists existing attachments in the thread's collection and throws if a file with identical name AND path already exists. This is an idempotency guard to prevent duplicate chunks/embeddings for the same document.
Solutions
- Check the thread's existing attachments before calling ingestFile and skip files with matching name and path.
- If re-ingestion is intentional (file content changed), use a distinct path or remove the existing attachment first.
- Catch this error and treat it as a no-op/success if your workflow only needs the file attached once.
- Use deterministic file identifiers so retries of the same attach job are recognized as duplicates.
Example fix
// before
await ingestFile(threadId, file, opts) // throws on second call
// after
const existing = await vecdb.listAttachments(collection).catch(() => [])
if (!existing.some(f => f.name === file.name && f.path === file.path)) {
await ingestFile(threadId, file, opts)
} Defensive patterns
Strategy: validation
Validate before calling
const existing = await vecdb.listAttachments(collection).catch(() => []) const isDup = existing.some((f: any) => f.name === file.name && f.path === file.path) if (isDup) return // or update instead of ingest await ingestFile(threadId, file, opts)
Try / catch
try {
await ingestFile(threadId, file, opts)
} catch (e) {
if (String(e).includes("has already been attached")) {
return existingAttachmentInfo // treat as idempotent success
}
throw e
} Prevention
- Track attached files per thread in application state and disable duplicate attach in the UI.
- Debounce/lock attach buttons to prevent double-submission races.
- Make ingest jobs idempotent by keying on (threadId, name, path).
When it happens
Trigger: Calling ingestFile(threadId, file, opts) where the thread's attachment list already contains a file whose f.name === file.name and f.path === file.path — i.e. re-attaching the exact same file to the same thread.
Common situations: Re-running an ingest/attach script against the same thread, a UI allowing a second attach click before the first completes and persisted, or replaying a queued attachment job.
Understand the failure class
Background: "already exists" / EEXIST / FileAlreadyExistsException: what the 'file already exists' error means and how to fix it — this error's family across 37 libraries.
Related errors
- Failed to ingest file
- Failed to parse archive name
- File ' ' exceeds size limit ( bytes > MB).
- File ' ' has already been attached to this project
- Invalid argument
AI-assisted analysis of janhq/jan@7205d770c1 (2026-09-17).
Data as JSON: /api/errors/080e142bc2c471a0.
Report an issue: GitHub.
Appendix: source
Thrown at extensions/vector-db-extension/src/index.ts:244
} catch (e) {
throw new Error(
`Failed to count embedding tokens: ${e instanceof Error ? e.message : String(e)}`
)
}
if (count <= budget || text.length <= MIN_CHUNK_SIZE_CHARS) return [text]
const mid = Math.floor(text.length / 2)
return [
...(await this.splitChunkToFit(text.slice(0, mid), budget, llm)),
...(await this.splitChunkToFit(text.slice(mid), budget, llm)),
]
}
async ingestFile(threadId: string, file: VectorDBFileInput, opts: VectorDBIngestOptions): Promise<AttachmentFileInfo> {
// Check for duplicate file (same name + path)
const existingFiles = await vecdb.listAttachments(this.collectionForThread(threadId)).catch(() => [])
const duplicate = existingFiles.find((f: any) => f.name === file.name && f.path === file.path)
if (duplicate) {
throw new Error(`File '${file.name}' has already been attached to this thread`)
}
const text = await ragApi.parseDocument(file.path, file.type || 'application/octet-stream')
const chunks = await this.chunkText(text, opts.chunkSize, opts.chunkOverlap)
if (!chunks.length) {
const fi = await vecdb.createFile(this.collectionForThread(threadId), file)
return fi
}
const embeddings = await embedTexts(chunks)
const dimension = embeddings[0]?.length || 0
if (dimension <= 0) throw new Error('Embedding dimension not available')
await this.createCollection(threadId, dimension)
const fi = await vecdb.createFile(this.collectionForThread(threadId), file)
await vecdb.insertChunks(
this.collectionForThread(threadId),
fi.id,
chunks.map((t, i) => ({ text: t, embedding: embeddings[i] }))
)View on GitHub (pinned to 7205d770c1)