Brain files
Brain files are documents the user uploads so skills can ground on their content. The platform parses the file into passages, embeds each passage in the same 1536-dimensional space as the rest of the memory layer, and exposes a vector RPC for skills to search through. The result is that a skill can quote from a contract, a research report, or a brand guide without anything special at the skill level.
Overview
- Storage bucket
brain-files(Supabase Storage)- Metadata table
brain_files- Chunk table
file_embeddings- Embedding model
- OpenAI text-embedding-3-small (1536 dims)
- Vector index
- pgvector HNSW, cosine
- Search RPC
search_file_embeddings- Search tool
brain_search_files- Max file size
- 50 MB enforced at upload
Uploaded files live in two places at the same time. The binary lives in Supabase Storage so the original document can be displayed, downloaded, and shared. The parsed text lives in file_embeddings chunked into passages with embeddings so retrieval is fast and scoped to the user. The two are kept in sync by the worker that processes uploads.
Data model
A file row and its chunks.
Upload flow
Five steps from drop zone to searchable.
- 01POST /api/brain/filesThe client uploads the file as multipart form data. The server validates the size cap (50 MB), computes a SHA-256 of the bytes, and checks for an existing row with the same content_hash for the same user. If one exists, the upload short-circuits and returns the existing row.
- 02Persist binaryThe bytes are written to Supabase Storage under
brain-files/<user_id>/<file_id>/<name>. The path is stored inbrain_files.storage_path. - 03Insert rowA row is inserted with status processing. The HTTP response returns immediately so the UI can show the file as uploading without blocking on chunking.
- 04Async parse and chunkA worker fetches the file from Storage, runs the format-specific parser, splits the result into passages, and embeds each passage. Rows land in file_embeddings as they finish.
- 05Mark readyWhen the chunker finishes, brain_files.status flips to ready and chunks_count is updated. Search only surfaces rows whose parent file is in status ready.
Chunking
The parser is format-aware. The chunker is uniform across formats.
Each format goes through its own parser: PDFs are passed through pdfjs, DOCX files through docx, plain text and Markdown are read as-is, and HTML is stripped to text. The output is a single string that the chunker splits into passages with a sliding window. Passages are sized to fit comfortably inside the embedding model's context, with a small overlap so that a fact that straddles a boundary still lands inside at least one chunk.
| Parameter | Default |
|---|---|
| Target chunk size | ~800 tokens |
| Chunk overlap | ~120 tokens |
| Split points (in order of preference) | Paragraph break, sentence boundary, word boundary, hard cut |
| Hard cap on chunks per file | 1024 |
Embedding
Same model as the rest of the memory layer so retrieval is uniform across surfaces.
Each chunk is embedded with OpenAI text-embedding-3-small. The 1536-dimensional vector is stored in the embedding column. Embedding happens in batches of up to 96 chunks per request to keep latency low and rate-limit headroom comfortable. A failure on one batch retries with exponential backoff before failing the whole file.
agent_memories. A query embedded once can be evaluated against both tables without re-embedding. This is intentional and load-bearing for skills that want to reach both memory and uploaded documents in the same turn.Search
A vector RPC scoped to the current user.
The RPC computes 1 - (embedding <=> query_embedding) for every chunk above the threshold and returns the top matches. Results are ordered by similarity descending. Each result includes the file name, the chunk index, the similarity score, and the chunk content so the model can quote from it directly.
Ranking and limits
The defaults are tuned for typical skill usage.
| Parameter | Default | Bounds |
|---|---|---|
| match_limit | 10 | 1 to 30 |
| similarity threshold | 0.5 cosine | set per call by the caller |
| file_name filter | null (search across all files) | exact file name to restrict the scan |
| chunk text length returned | Full chunk content | Caller can truncate downstream |
Reprocess
The user can trigger a re-chunk and re-embed without re-uploading.
The brain_manage_notes tool exposes a brain_reprocess_file action that flips a file's status back to processing, deletes its existing chunks, and re-runs the chunker against the binary still in Storage. This is useful when the chunker or the embedding model changes, or when the user notices a parse problem.
Deduplication
One copy per user per content hash.
The upload handler computes a SHA-256 of the binary and looks for an existing brain_files row with the same hash for the same user. If one exists, the upload returns that row instead of creating a duplicate. This applies across renames; the same content under a different file name still dedupes. The dedupe is per user; two different users uploading the same file each get their own row and their own chunks.
Supported formats
What the parser knows how to read.
| Format | Parser |
|---|---|
| PDF (.pdf) | pdfjs, multi-column aware |
| Word (.docx) | docx library |
| Markdown (.md) | Read as plain text |
| Plain text (.txt) | Read as is |
| HTML (.html) | Stripped to text, DOMPurify allowlist applied first |
| Other | Stored, marked as failed if no parser exists |
File map
Glossary
- Brain file
- A document the user uploaded. Tracked in brain_files with status processing or ready.
- Chunk
- One passage of a file. Stored as a row in file_embeddings with an embedding vector.
- Content hash
- SHA-256 of the file bytes. Used to dedupe uploads per user.
- Storage path
- The Supabase Storage path the binary lives at. brain-files/<user_id>/<file_id>/<name>.
- Search RPC
- search_file_embeddings, the Postgres function that runs the vector search scoped to a user.
- Reprocess
- Delete all chunks for a file and re-run the chunker plus embedder against the binary still in Storage.