# Files kept exactly

> Keep text under a path, such as /memories/notes.md, and read it back exactly as it was written. Geniffy also learns from it like a note.

Some text has to come back exactly as it was written, such as the notes an agent keeps for itself. Put it
under a path and Geniffy keeps it character for character, spaces and line endings included. It also learns
from it like a note titled by its path, so `context()` and `ask()` recall what it says.

```python
notes = "# Lumen\n- Priya Nair signs the renewal.\n"
mem.files.put("/memories/notes.md", notes)    # creates or replaces it
mem.files.get("/memories/notes.md").text      # notes, exactly
```

```ts
const notes = "# Lumen\n- Priya Nair signs the renewal.\n";
await mem.files.put("/memories/notes.md", notes);  // creates or replaces it
const { text } = await mem.files.get("/memories/notes.md");  // notes, exactly
```

```bash
curl -X PUT https://api.geniffy.com/v1/files \
  -H "Authorization: Bearer $GENIFFY_API_KEY" \
  -H "X-Geniffy-Space: customer_1042" \
  -H "Content-Type: application/json" \
  -d '{"path": "/memories/notes.md",
       "text": "# Lumen\n- Priya Nair signs the renewal.\n"}'
```

A write answers with the file and the source it is learned from. `size` is in characters:

```json
{
  "path": "/memories/notes.md",
  "size": 40,
  "updated_at": "2026-10-06T11:00:01.204913+00:00",
  "created": true,
  "source": {
    "id": "6f1c0a3e9b2d4c8e8a7f5b3d2e1c0f9a",
    "kind": "note",
    "title": "/memories/notes.md",
    "labels": {}
  }
}
```

This is what [Claude's memory tool](https://docs.geniffy.com/integrations/claude-memory-tool) keeps its notes in. Any agent that writes
its own notes as files can do the same, with any model.

## What a write does

- **A new path** gets a file, and what it says is learned as a note titled by its path. `created` is `true`.
- **A path that has a file** gets the new text whole. Only the paragraphs that changed are learned, and what was
  removed is taken back, as when a record is sent again under [your own id](https://docs.geniffy.com/add-memories/your-own-ids).
- **Empty text** is a file too: it is kept, with nothing learned from it, and emptying a file takes back what
  it taught.
- **Labels** work as on anything you add, such as `labels={"channel": "claude-memory"}`. Written again, new
  labels replace the old ones; left out, the file keeps its own; `{}` clears them. See [Labels](https://docs.geniffy.com/add-memories/labels).
- **A write is whole or not at all.** If the memory service refuses it, a new file leaves nothing behind, and a
  file written before keeps its old text and what that taught. In the rare case the memory service can't be
  reached to take back what it may have kept of a new file, the error names the failed source that holds it:
  the file still reads as missing, and writing it again or deleting it clears that too.

## Read and list

```python
f = mem.files.get("/memories/notes.md")   # .text, .size, .updated_at
page = mem.files.list("/memories/")       # .files, .total and .next
```

```ts
const f = await mem.files.get("/memories/notes.md");   // text, size, updated_at
const { files, total, next } = await mem.files.list({ prefix: "/memories/" });
```

```bash
curl "https://api.geniffy.com/v1/files?prefix=/memories/" \
  -H "Authorization: Bearer $GENIFFY_API_KEY" \
  -H "X-Geniffy-Space: customer_1042"
```

- **A file's `size` is in characters.** A list holds each file's path, size and `updated_at`, without the text,
  in path order.
- **A prefix is a plain prefix of paths,** not a pattern: `/memories/` is every file beneath that directory,
  `/` is every file, and `/memories/pre` finds `/memories/preferences.md`. Paths are compared character by
  character, so `/Memories/` is another directory.
- **A page holds up to 200 files,** 100 unless you ask for more with `limit`, and `next` is the cursor of the
  page after, to pass as `cursor`; it is `null` (`None` in Python) at the end.
- **A path with no file** is a `404` with the code `not_found`; the SDKs raise `NotFoundError`.

## Move

```python
mem.files.move("/memories/notes.md", "/memories/lumen.md")   # a file
mem.files.move("/memories/drafts", "/memories/archive")      # a directory
```

```ts
await mem.files.move("/memories/notes.md", "/memories/lumen.md");  // a file
await mem.files.move("/memories/drafts", "/memories/archive");  // a directory
```

```bash
curl https://api.geniffy.com/v1/files/move \
  -H "Authorization: Bearer $GENIFFY_API_KEY" \
  -H "X-Geniffy-Space: customer_1042" \
  -H "Content-Type: application/json" \
  -d '{"from": "/memories/drafts", "to": "/memories/archive"}'
```

When there is a file at `from`, it moves to `to`. When there isn't, every file beneath `from/` moves to the
same place beneath `to/`. Each keeps its text and what it taught, and nothing is learned again. A move answers
with how many files moved. When a file is already where one of them would go, nothing moves and the call is a
`409` with the code `conflict`; when there is nothing at `from`, a `404`.

## Delete

```python
mem.files.delete("/memories/lumen.md")          # a file
mem.files.delete_prefix("/memories/archive/")   # every file under a prefix
```

```ts
await mem.files.delete("/memories/lumen.md");         // a file
await mem.files.deletePrefix("/memories/archive/");   // every file under a prefix
```

```bash
curl -X DELETE "https://api.geniffy.com/v1/files?prefix=/memories/archive/" \
  -H "Authorization: Bearer $GENIFFY_API_KEY" \
  -H "X-Geniffy-Space: customer_1042"
```

Each file goes with its text and every memory only it taught. A memory that another source also taught stays,
because something still says it. Over HTTP, a prefix deletes up to 100 files a call and `more: true` says to
call again; the SDKs call again for you, and return how many files were deleted.

## The rules for a path

- It starts with `/` and is up to 255 characters.
- None of its parts is empty, `.` or `..`, and it holds no backslash and no control character.
- A file's path doesn't end with `/`; a prefix may.

A path that breaks a rule is refused with `bad_path`, and nothing is stored.

## Files and the rest of a memory

- **Each file is a source,** a note titled by its path under the `external_id` `file:` and its path:
  `sources.list()` shows it, and `DELETE /v1/sources?external_id=file:/memories/lumen.md` deletes it as
  deleting the file does. Only `PUT /v1/files` writes one, so adding a note, a page or a file under an
  `external_id` that starts with `file:` is refused with `bad_external_id`.
- **Recall reads them** with everything else, and each line cites the file's path:
  `- Priya Nair signs the renewal.  [/memories/notes.md, 2026-10-06]`.
- **They go with the user.** [Forgetting a user](https://docs.geniffy.com/correct-and-forget#forget-a-user) takes their files, and
  their copy, `export()`, lists every file by its path, size and when it was written. `files.get(path)` hands
  back each one's text as written, so the copy stays small however much the files hold.
- **A file holds up to 2,000,000 characters,** as a note does. Text with a NUL character (`\u0000`) in it, or
  half of a surrogate pair, can't be kept exactly as sent, so it is refused with `invalid_request`.

Source: https://docs.geniffy.com/add-memories/files-kept-exactly
