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

  1. Check the thread's existing attachments before calling ingestFile and skip files with matching name and path.
  2. If re-ingestion is intentional (file content changed), use a distinct path or remove the existing attachment first.
  3. Catch this error and treat it as a no-op/success if your workflow only needs the file attached once.
  4. 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

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


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)