struct

Engram::MemoryFile

Inherits Struct < Value < Object

A single parsed memory migration file: frontmatter metadata plus markdown body.

Files live at .agents/memories/<ID>_<slug>.md. The filename is canonical for id and slug; the frontmatter id must match it.

Constants

FILENAME_PATTERN = /\A(\d{14})_(.+)\.md\z/

Matches <14-digit-id>_<slug>.md; captures the id and slug portions.

Constructors

new(id : Int64, slug : String, title : String, topics : Array(String), supersedes : Array(Int64), author : String | Nil, body : String, file_path : String)

Builds a memory file record from already-parsed fields.

Source
parse(content : String, path : String) : MemoryFile

Parses raw migration-file content, using path for filename validation and error context.

Source
parse(path : String) : MemoryFile

Reads and parses the memory file at path from disk.

Source

Class methods

check_duplicates(files : Array(MemoryFile)) : Nil

Raises Engram::DuplicateIdError if any two of files share an id.

Source
claim_and_write(memories_dir : String, slug : String, start_time : Time = Time.utc, & : Int64 -> String) : Tuple(Int64, String)

Atomically claims the next unused migration id for a memory whose filename slug is slug under memories_dir, and publishes the block's result as that file's content in one step. Returns {id, path}.

This is the race-free replacement for the naive "next_id then File.write" pattern: that pattern has a TOCTOU gap where two callers (two processes, or two near-simultaneous remember/new calls) can both land on the same id in the same wall-clock second, and the second File.write truncates the first one's file with no error and no duplicate-id detection, because only one file ever ends up on disk.

The id -- not the <id>_<slug>.md pathname -- is the memory's globally-unique key: two files sharing an id are a duplicate-id conflict even when their slugs (titles) differ, so a candidate id is rejected if ANY <id>_*.md already exists, not just one matching this exact slug. We combine two guards to that end:

  1. A one-time directory snapshot (like next_id): a candidate whose id-prefix already appears is skipped. This is what makes repeated claims within the same second -- even with different slugs -- march forward to distinct ids (the common, single-process case: engram new twice, or an MCP session firing several remembers a second apart).

  2. For each surviving candidate we write the block's result (the block receives the candidate id, since the frontmatter embeds it) to a fresh same-directory temp file, then File.link that temp file onto the final <id>_<slug>.md path. link(2) is atomic with respect to existence: if the target already exists -- because a concurrent caller already claimed that exact id/slug pair after our snapshot -- the link fails with File::AlreadyExistsError and the existing file is left completely untouched (the core defect this replaces: a plain File.write would have silently truncated it). On that failure we discard the temp, bump the id by a second, and retry; on success the temp is unlinked (it was only ever a staging name) and {id, path} is returned.

memories_dir must already exist (same precondition as next_id; callers already Dir.mkdir_p it first). start_time defaults to Time.utc and exists so specs can freeze the clock to deterministically reproduce two claims racing in the same second.

Source
next_id(memories_dir : String) : Int64

The next unused 14-digit migration id for memories_dir: the current UTC time, bumped a second at a time until no existing <id>_*.md file already claims it, per a one-time directory scan.

NOTE: this is a best-effort peek, not a claim -- two calls to next_id in the same wall-clock second (from two processes, or from new/ remember racing each other) can and will return the same id, because nothing stops either caller's subsequent plain File.write from silently truncating whatever the other one just wrote. Anything that's actually about to create the migration file must use claim_and_write instead, which makes the id allocation and the file's creation a single atomic step. next_id remains only for callers that just want to know what id would be used next without writing anything.

Source
repo_relative_path(memories_dir : String, path : String) : String

The repo-relative form of path (a file living under memories_dir): the last two path segments of memories_dir (conventionally ".agents/memories") joined with the file's basename. Keeps the file_path schema column (docs/SPEC.md: "repo-relative source path") stable no matter how absolute memories_dir and path happen to be for the caller.

Source
scaffold(id : Int64, title : String, topics : Array(String) = [] of String, supersedes : Array(Int64) = [] of Int64, author : String | Nil = nil) : String

Builds file content for a brand-new memory (used by engram new): a fresh frontmatter block plus a Decision/Why/Rejected body scaffold.

Source
slugify(title : String) : String

Turns a title into a filesystem-safe slug: lowercase, dashes, no repeats.

Source

Instance methods

author
Source
body
Source
content_hash

SHA256 hex digest over the meaningful fields (not id/slug/path), used by sync to detect whether a memory's content changed since it was applied. Topics are lowercased in the canonical string to match how Store persists them (docs/SPEC.md: "comma-joined, lowercased") — otherwise a memory whose frontmatter uses mixed-case topics would hash differently from the record read back from the store and sync would treat it as perpetually changed.

Source
file_path
Source
filename

The canonical filename for this memory: <id>_<slug>.md.

Source
serialize

Renders this memory back into migration-file text (frontmatter + body).

Source
slug
Source
supersedes
Source
title
Source
topics
Source