Memory

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.

Updated today

Overview

At a glance
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.

supabase/migrations/20260414_brain_files_table.sqlsql
1create table brain_files (
2 id uuid primary key default gen_random_uuid(),
3 user_id uuid not null references auth.users(id) on delete cascade,
4 file_name text not null,
5 file_size bigint,
6 content_hash text, -- sha256 of the binary
7 storage_path text not null, -- path inside brain-files bucket
8 status text not null, -- 'processing' | 'ready' | 'failed'
9 chunks_count int, -- filled after processing
10 created_at timestamptz not null default now()
11);
12
13create table file_embeddings (
14 id uuid primary key default gen_random_uuid(),
15 user_id uuid not null references auth.users(id) on delete cascade,
16 file_name text not null,
17 chunk_index int not null, -- order within the file
18 content text not null, -- the passage
19 embedding vector(1536),
20 metadata jsonb,
21 created_at timestamptz not null default now()
22);
23
24create index file_embeddings_embedding_hnsw_idx
25 on file_embeddings using hnsw (embedding vector_cosine_ops);

Upload flow

Five steps from drop zone to searchable.

  1. 01
    POST /api/brain/files
    The 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.
  2. 02
    Persist binary
    The bytes are written to Supabase Storage under brain-files/<user_id>/<file_id>/<name>. The path is stored in brain_files.storage_path.
  3. 03
    Insert row
    A row is inserted with status processing. The HTTP response returns immediately so the UI can show the file as uploading without blocking on chunking.
  4. 04
    Async parse and chunk
    A 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.
  5. 05
    Mark ready
    When 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.
src/app/api/brain/files/route.tsts
51// Body validation, dedupe, then storage + row insert
52const buf = Buffer.from(await file.arrayBuffer())
53if (buf.byteLength > 50 * 1024 * 1024) {
54 return Response.json({ error: "file_too_large" }, { status: 413 })
55}
56const hash = sha256(buf)
57const existing = await supabase.from("brain_files")
58 .select("*")
59 .eq("user_id", userId)
60 .eq("content_hash", hash)
61 .maybeSingle()
62if (existing.data) return Response.json({ file: existing.data, deduped: true })
63
64await supabase.storage.from("brain-files").upload(path, buf, { contentType: file.type })
65const { data: row } = await supabase.from("brain_files").insert({
66 user_id: userId, file_name: file.name, file_size: buf.byteLength,
67 content_hash: hash, storage_path: path, status: "processing",
68}).select("*").single()
69// fire-and-forget chunk + embed worker
70void enqueueProcessing(row.id)
71return Response.json({ file: row })

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.

ParameterDefault
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 file1024

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.

Note
The embedding model is shared with 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.

Ranking and limits

The defaults are tuned for typical skill usage.

ParameterDefaultBounds
match_limit101 to 30
similarity threshold0.5 cosineset per call by the caller
file_name filternull (search across all files)exact file name to restrict the scan
chunk text length returnedFull chunk contentCaller 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.

FormatParser
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
OtherStored, marked as failed if no parser exists

File map

src/app/api/brain
files/route.tslist, upload, dedupe, status, delete
src/lib
brain-tools.tsbrain_search_files, brain_list_files, brain_reprocess_file
supabase/migrations
20260414_brain_files_table.sql
20260414_file_embeddings.sql
search_file_embeddings.sqlvector RPC

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.