Skip to content

Fryri documentation

Keep a codebase in sync

Updated 2026-09-26

source_ref names the original of a memory, and an add with a source_ref that the library already holds replaces the earlier memory. GET /v1/memories with source_ref_prefix lists every memory under one prefix, each with the sha256 of its content. Together they keep a library equal to a repository, with one memory per file.

How a sync works#

A sync compares the files with what the library holds:

  • A new file is added with its path under a prefix as source_ref, such as myrepo/src/auth.py.
  • A changed file is added again with the same source_ref, and the add replaces the earlier memory.
  • The memory of a deleted file is deleted with DELETE /v1/memories/{memory_id}.
  • An unchanged file is skipped, because its sha256 in the list equals the SHA-256 of the file.
  • A file whose memory is failed is sent again. Sending the same bytes again starts a new attempt.

Each file sent bills like any other add, as the price list shows. A sync of an unchanged repository only lists, and listing is free.

Replace a changed file#

The add returns the new memory, and replaced lists the ids of the earlier memories with the same source_ref. They stop appearing in search and answers at once. A failed add leaves the earlier memory in place.

Request

curl -X POST https://api.fryri.com/v1/memories \
  -H "Authorization: Bearer $FRYRI_API_KEY" \
  -F "file=@src/auth.py" \
  -F "source_ref=myrepo/src/auth.py" \
  -F "end_user_id=user_42"

Response

{
  "id": "file_hqjP2q_DlrHZT-4U",
  "type": "document",
  "title": "auth.py",
  "status": "processing",
  "status_reason": null,
  "created_at": "2026-09-25T09:07:09.917301Z",
  "source_ref": "myrepo/src/auth.py",
  "sha256": "785108e6758610a47355924cf936ce293ac1c6e5e6c7cd9b8752c19c78c00e26",
  "duplicate": false,
  "replaced": [
    "file_kR1jHO7CDRwaoCEw"
  ]
}

List one codebase#

source_ref_prefix lists only the memories whose source_ref starts with it. Characters like % and _ match themselves. sha256 is the SHA-256 of the file as it was added, in hex.

Request

curl "https://api.fryri.com/v1/memories?end_user_id=user_42&source_ref_prefix=myrepo%2F" \
  -H "Authorization: Bearer $FRYRI_API_KEY"

Response

{
  "memories": [
    {
      "id": "file_hqjP2q_DlrHZT-4U",
      "type": "document",
      "title": "auth.py",
      "status": "processing",
      "status_reason": null,
      "created_at": "2026-09-25T09:07:09.917301Z",
      "source_ref": "myrepo/src/auth.py",
      "sha256": "785108e6758610a47355924cf936ce293ac1c6e5e6c7cd9b8752c19c78c00e26"
    }
  ],
  "next_cursor": null
}

Sync a repository#

The script syncs every text file that git tracks, and skips empty files, binary files and files over 1 MB. Text files such as TypeScript sources and SVG images are added as documents. Run it from the root of the repository, by hand or after every push. FRYRI_PREFIX names the codebase in the library, and FRYRI_END_USER_ID keeps it in the library of one end user.

Complete example

"""Keep a codebase in Fryri.

Every text file git tracks becomes one memory whose source_ref is the file's
path under PREFIX. A changed file replaces its memory, a deleted file's memory
is deleted, a file whose memory failed is sent again, and an unchanged file is
skipped without being sent. A file the API refuses as a photo, video or audio
file is skipped. Run it from the root of the repository, by hand or on every
push.
"""
import hashlib
import os
import subprocess
import time

import requests

API = os.environ.get("FRYRI_API_URL", "https://api.fryri.com") + "/v1/memories"
HEADERS = {"Authorization": f"Bearer {os.environ['FRYRI_API_KEY']}"}
PREFIX = os.environ.get("FRYRI_PREFIX", "myrepo/")  # names this codebase in your library
SCOPE = {"end_user_id": os.environ["FRYRI_END_USER_ID"]} if os.environ.get("FRYRI_END_USER_ID") else {}
MAX_BYTES = 1_000_000  # larger files are skipped


def call(method, url, **kwargs):
    """One API call. Waits out rate limits; a 404 on delete is already done,
    and a 422 unsupported_format is returned for the caller to skip."""
    while True:
        response = requests.request(method, url, headers=HEADERS, timeout=120, **kwargs)
        if response.status_code == 429 and response.json()["error"]["retryable"]:
            time.sleep(int(response.headers.get("Retry-After", "5")))
            continue
        if method == "DELETE" and response.status_code == 404:
            return response
        if response.status_code == 422 and response.json()["error"]["code"] == "unsupported_format":
            return response
        response.raise_for_status()
        return response


# 1. The text files git tracks.
listed = subprocess.run(["git", "ls-files", "-z"], capture_output=True, check=True).stdout
files = {}
for path in listed.decode("utf-8").split("\0"):
    if not path or not os.path.isfile(path):
        continue
    with open(path, "rb") as handle:
        data = handle.read(MAX_BYTES + 1)
    if data and len(data) <= MAX_BYTES and b"\0" not in data:  # skip empty, large and binary files
        files[PREFIX + path] = data

# 2. What Fryri holds for this codebase.
held, cursor = {}, None
while True:
    params = {**SCOPE, "source_ref_prefix": PREFIX, "limit": 100}
    if cursor:
        params["cursor"] = cursor
    page = call("GET", API, params=params).json()
    for memory in page["memories"]:
        if memory["source_ref"] in held:  # two copies of one path: resending the file keeps one
            held[memory["source_ref"]]["sha256"] = None
        else:
            held[memory["source_ref"]] = memory
    cursor = page["next_cursor"]
    if not cursor:
        break

# 3. Delete the memories of deleted files first.
removed = [ref for ref in held if ref not in files]
for ref in removed:
    call("DELETE", f"{API}/{held.pop(ref)['id']}", params=SCOPE)

# 4. Send new and changed files, and files whose memory failed (sending the same
#    bytes again starts a new attempt). Skip a file whose memory is unchanged, and
#    a new file whose exact content Fryri already holds under another path.
usable = {ref: memory for ref, memory in held.items() if memory["status"] != "failed"}
stored = {memory["sha256"] for memory in usable.values()}
sent = refused = 0
for ref, data in sorted(files.items()):
    sha256 = hashlib.sha256(data).hexdigest()
    mine = usable.get(ref)
    if (mine and mine["sha256"] == sha256) or (ref not in held and sha256 in stored):
        continue
    response = call("POST", API, files={"file": (os.path.basename(ref), data)}, data={**SCOPE, "source_ref": ref})
    if response.ok:
        sent += 1
    else:
        refused += 1

unchanged = len(files) - sent - refused
print(f"{len(files)} files: {sent} sent, {len(removed)} removed, {refused} refused, {unchanged} unchanged")

Identical files#

Identical content shares one memory. Two files with the same bytes, such as two copies of a license, become one memory that carries the source_ref of the first. The script skips the second copy while the first is held, and sends it once the first is deleted.

Best Practices#

Syncing

  • End each prefix with /, so the prefix of one repository never matches another, such as myrepo/ and myrepo-old/.
  • Delete the memories of deleted files before adding new files. A moved file then gets a memory under its new path.
  • Compare sha256 before sending, so a sync uploads only what changed.

Searching

  • Search with the same end_user_id that the sync used.
  • Read source_ref on each search result to link back to the file.
  • For a folder without git, list its files with Path.rglob in place of git ls-files.

Tip: Run the sync after every push, so search and answers use the current code.

Warning: A changed file is missing from search and answers until its new memory is ready, because the add removes the earlier memory at once. A small file is usually ready within seconds, and a large sync can take a few minutes.