RememberStackremember.dev/docs

Ingest files

An agent can only remember what you give it. This page shows how to send a file, a string or raw bytes to RememberStack, how to name it so an edited file becomes a new version of the same document instead of a stranger, and how to load a folder of notes in one go.

You need a configured client. If you have not set REMEMBER_API_URL and a token yet, follow the Quickstart first.

Send one file

from datetime import UTC, datetime
 
import remember
 
client = remember.Client.from_env()
 
version = client.ingest(
    "notes/2026-09-17-standup.md",
    title="Stand-up, 17 September 2026",
    source_kind="file",
    source_ref="notes/2026-09-17-standup.md",
    source_modified_at=datetime(2026, 9, 17, 9, 30, tzinfo=UTC),
)
print(version.doc_id, version.version_id, version.created)

The call returns as soon as the bytes are stored. The result (IngestedVersion) carries:

FieldMeaning
deployment_idThe deployment that stored the bytes.
doc_idThe document. Stable for one source_kind + source_ref pair.
version_idThis exact snapshot of the bytes. You wait on it and cite it.
content_hashSHA-256 of the bytes, in hex.
createdTrue when this call stored a new version; False when the bytes were already the document's latest version.
mimeThe MIME type conversion uses for these bytes.
titleThe document's title.
versioning_modesnapshot or living.
parked"no_route" when the file's conversion is parked waiting for a conversion route for its MIME type: the bytes are stored but not read until an operator adds a route if needed and runs remember ops resume-no-route, or you send the same bytes again with a type that has a route. None means only that it is not parked for no_route; readiness tells you the processing state.

title and versioning_mode are set by a document's first ingest, and the MIME type by the first upload of those bytes. Later values you send are not applied, so compare the result with what you sent. The one exception is a type with no converter route: send the bytes again with a routable type and the new type applies, releasing the parked conversion. See What the first ingest fixes.

Processing (reading, structuring, extracting claims, adjudicating facts) happens afterwards and takes minutes. The document is not queryable until it finishes: see Wait until a document is queryable.

The same with the CLI:

remember ingest notes/2026-09-17-standup.md \
  --title "Stand-up, 17 September 2026" \
  --source-kind file \
  --source-ref notes/2026-09-17-standup.md \
  --source-modified-at 2026-09-17T09:30:00+00:00

The CLI prints the same fields as one JSON line. It has no --filename flag: the filename is always the file's own name.

Three ways to pass the body

Client.ingest takes the body in one of three forms.

A path, as a string or a pathlib.Path. The client reads the file, uses its name as the filename and takes the MIME type from the file's extension.

client.ingest("specs/billing-migration.md")

A string that is not an existing file raises ValueError("file not found: …"). The client never treats a string as document text.

Bytes as the first argument. You must name the file.

text = "Dana: the finance sign-off moves to 3 October."
client.ingest(text.encode("utf-8"), filename="dana-update.md")

Bytes as content=. Same rules as bytes; use it when the first argument reads better as nothing at all.

client.ingest(content=b"...", filename="ravi-notes.txt")

With bytes, the MIME type comes from the filename you pass.

The MIME type

The MIME type decides which converter reads the file. The client picks it from the extension: of the file's real name for a path, of filename for bytes. The formats a deployment can convert map the same way on every Python installation:

ExtensionMIME type
.md, .markdowntext/markdown
.txttext/plain
.html, .htmtext/html
.pdfapplication/pdf
.pngimage/png
.jpg, .jpegimage/jpeg
.docx, .pptx, .xlsxThe Office Open XML types (application/vnd.openxmlformats-officedocument.…)

Any other extension is looked up in Python's mimetypes database, and a name it does not know is sent as application/octet-stream. Pass mime= (or --mime on the CLI) to send a different type; an explicit value always wins.

The deployment keeps the first MIME type it saw for a given set of bytes. If you send the same bytes again with a corrected mime, the stored type does not change. Get the type right on the first send.

Filenames and titles

  • filename is required and must not be empty. A path supplies it for you.
  • The extension of the filename is kept with the stored original. The file is read according to mime; for bytes sent without mime, that type comes from the filename's extension.
  • title is optional. Without it, the document's title is the filename without its extension (2026-09-17-standup).
  • The title is set when the document is first created. Sending a new version with a different title does not rename the document.

Name the source: source_kind and source_ref

A document's identity is the pair source_kind + source_ref:

  • source_kind is the kind of place the file comes from, such as file, drive, meeting.
  • source_ref is the file's stable identifier within that kind, such as a relative path or an upstream file ID.

Send the same pair again with changed bytes and you get a new version of the same document. Send it with identical bytes and nothing is stored (created=False). That is what makes re-running an import safe.

The two must be supplied together. One without the other raises ValueError in the client and returns HTTP 422 from the API.

Without the pair, the document's identity is its content hash. Sending the same bytes twice is still a no-op, but an edited copy of the file becomes a second, unrelated document, and both keep speaking. Use the pair for anything you will send more than once. Documents, versions and sources explains the model.

source_modified_at is when the source last changed. It becomes the time the document's claims were asserted, which is how RememberStack reads "yesterday" or "next week" inside the text. It must be a timezone-aware UTC datetime; a naive or non-UTC value raises ValueError. It requires the source pair.

versioning_mode and source_version_ref also require the pair. They matter when a file changes: see Keep a source up to date.

Load a folder

This loop sends every Markdown and text file under a folder, keyed by its path, and collects the versions that need processing:

from datetime import UTC, datetime
from pathlib import Path
 
import remember
from remember import MemoryApiError
 
SUFFIXES = {".md", ".txt"}
 
client = remember.Client.from_env()
root = Path("billing-migration")
pending = []
 
for path in sorted(root.rglob("*")):
    if path.suffix.lower() not in SUFFIXES or not path.is_file():
        continue
    ref = path.relative_to(root).as_posix()
    modified = datetime.fromtimestamp(path.stat().st_mtime, tz=UTC)
    try:
        version = client.ingest(
            path,
            source_kind="file",
            source_ref=f"billing-migration/{ref}",
            source_modified_at=modified,
        )
    except MemoryApiError as error:
        print(f"skipped {ref}: {error.status_code} {error.detail}")
        continue
    if version.created:
        pending.append(version.version_id)
 
print(f"{len(pending)} new versions")

Run it again after editing one note and only that note produces a new version. Unchanged files return created=False and cost nothing.

Then wait for the new versions. A readiness check takes at most 1,000 versions, so wait in batches. A bulk load takes longer than the default 30-minute timeout, so raise it:

for start in range(0, len(pending), 1000):
    client.wait_for_readiness(pending[start:start + 1000], timeout=3600)

The client does not retry a failed ingest for you. If a call fails with a transport error, sending the same bytes with the same source pair again is safe: at worst it returns created=False.

Send a document through MCP

An agent connected over MCP uses the ingest tool. Exactly one of text, content_base64 or path carries the body:

{
  "name": "ingest",
  "arguments": {
    "text": "# Decision log\n\nDana moved the finance sign-off to 3 October.",
    "filename": "decision-log.md",
    "source_kind": "agent",
    "source_ref": "billing-migration/decision-log",
    "source_modified_at": "2026-09-18T08:00:00+00:00"
  }
}
  • text is UTF-8 and needs filename; mime is the text type of the filename's extension (decision-log.md is text/markdown), else text/plain.
  • content_base64 is standard base64 and needs filename; mime comes from the filename's extension as in the table above, else application/octet-stream.
  • path reads a local file on the machine running remember mcp. It is refused unless the operator lists allowed directories in REMEMBERSTACK_MCP_INGEST_ROOTS.

Limits on the tool arguments: filename up to 512 characters, mime 255, title 512, source_kind 128, source_ref 512, source_version_ref 512. The tool's reply includes the arguments to pass to pipeline_readiness next. When the file was parked ("parked": "no_route"), the reply's pipeline.status is parked_no_route and it tells the agent to report that to the user instead of waiting. See Connect your coding agent.

What gets read

Every ingest is stored. Whether it is read depends on the MIME type.

A fresh self-hosted deployment reads Markdown, plain text, HTML and Word, PowerPoint and Excel files (.docx, .pptx, .xlsx). A file with any other MIME type is stored and its processing is parked, not refused: the ingest succeeds with "parked": "no_route", and the version waits until an operator adds a converter for its type. PDFs and images need converters that you configure with your own provider key. See File formats and converters.

There is no body size limit unless the operator sets REMEMBERSTACK_SELFHOST_INGEST_BODY_MAX_BYTES. Over that limit the API answers HTTP 413 body_too_large. A request without a Content-Length header is refused with HTTP 411 when a limit is set.

A parked version never becomes ready, so a readiness wait on it runs until its timeout. Check parked in the ingest result before you wait; remember ingest prints a warning when it is set.

Errors

StatusWhen
413Body over the deployment limit (body_too_large). Split the file.
422Missing half of the source pair, a non-UTC source_modified_at, or living mode or a revision without a source pair.
401, 403Missing or wrong token, or a token without write access.

The Python client raises remember.MemoryApiError with status_code and detail. Client-side checks (the source pair, UTC, a missing file) raise ValueError before anything is sent.

Next