# Memory Schema v1 > Machine-readable contract for what goes into `.cursor/memory/*.md`. > Without this schema, memory files are just freeform markdown that the > agent may or may not update honestly. With it, every entry is > structured, correlation-linked, and validated. --- ## File roles | File | Required `kind` | Purpose | |------|-----------------|---------| | `session-handoff.md` | `state` | Rolling snapshot of project state. New entry appended at the end of every task. Most recent = authoritative. | | `decisions.md` | `decision` | Append-only architectural / engineering decisions. Never edit past entries. | | `patterns.md` | `pattern` | Lessons learned — what worked, what didn't. Append-only. | Files may contain free markdown (headers, intro text) outside of structured blocks — the validator ignores it. Only the blocks are contract. --- ## Entry block Every structured entry is delimited by HTML comments so prose in the file stays human-readable: ```markdown --- id: 1745333456-0-state correlation_id: 1745333456-0 at: 2026-04-22T19:30:56Z kind: state status: done author: orchestrator summary: Integrated Stripe webhook handler for checkout flow tags: [payments, backend] --- Any markdown body the author wants. Sections, lists, code, links. Bodies are part of the record — they can be arbitrary, but they must exist (≥ 20 non-whitespace characters). ``` The YAML between the two `---` delimiters is the **frontmatter**. The text after it (up to ``) is the **body**. ### Required frontmatter fields | Field | Type | Constraint | |-------|------|------------| | `id` | string | Unique across all memory files. Lowercase `[a-z0-9-]+`. Convention: `-[-]`. | | `correlation_id` | string | Shape `-` where both are integers. Shared across entries that belong to the same task. Generated by the hooks, NOT by the LLM. | | `at` | string | ISO 8601 UTC timestamp (`YYYY-MM-DDTHH:MM:SSZ`). Monotonically increasing within each file. | | `kind` | enum | One of `state`, `decision`, `pattern`. MUST match the file's required kind (see table above). | | `status` | enum | One of `in_progress`, `done`, `blocked`, `rejected`. | | `author` | enum | One of `orchestrator`, `subagent`, `human`. | | `summary` | string | Single line, ≤ 160 chars, no linebreaks. The human-readable title of the entry. | ### Optional frontmatter fields | Field | Type | When to use | |-------|------|-------------| | `tags` | list[string] | Routing/search labels. Lowercase kebab. | | `links` | list[string] | Entry ids this record references (causation notes, "supersedes", …). | | `author_detail` | string | Specific agent slug or human name when `author` alone is insufficient. | | `scope` | string | Bounded context / module the entry affects. | ### Body requirements - ≥ 20 non-whitespace characters (guard against empty entries). - No secrets. Use placeholders like `` if referencing them. - Freeform markdown is allowed; the validator does not police its shape. --- ## Correlation IDs Correlation IDs are **generated by the enforcement hooks**, not by the LLM. The current active correlation ID lives in `.cursor/hooks/.state/session.json` under `active_correlation_id`. The CLI (`memory.py`) reads it automatically. - Format: `-` - `session_id` is the epoch seconds when the Cursor session started. - `task_seq` starts at `0` and increments each time the `stop` gate allows completion after a task that contained writes. - Purpose: link a `decision` and a `pattern` back to the `state` snapshot from the same task. - Rule: an entry whose `correlation_id` does not match `active_correlation_id` at write time is rejected by the validator **unless** the entry is manually authored (status: `human`) and the validator is run with `--allow-manual`. --- ## Validation `.cursor/memory/validate.py` enforces every rule above. It runs: - Automatically via the `stop` hook (any memory file edited → validate). - On demand: `python3 .cursor/memory/validate.py [--path ]`. - In CI (`.gitlab-ci.yml`, `.github/workflows/`). Exit `0` = clean, `1` = errors printed to stderr. --- ## Writing an entry — always through the CLI The CLI is the only supported path for adding entries. It generates timestamps, reads the active correlation ID, and validates the block before writing. Direct hand-edits are allowed but must pass the validator. ``` # From the project root python3 .cursor/memory/memory.py append \ --file session-handoff \ --kind state \ --status done \ --summary "Integrated Stripe webhook handler for checkout flow" \ --tags payments,backend \ --body-file /tmp/handoff-body.md ``` Output: the generated `id` on stdout. Exit `0` on success. Other verbs: - `show ` — print an entry by id - `list --file [--kind ] [--correlation ]` — list entry headers - `latest --file --kind [--n ]` — print the N latest entries - `validate [--path ]` — delegates to `validate.py` --- ## Privacy Memory files WILL accumulate project-sensitive detail (service names, deploy hosts, internal module layout, named customers, etc.). Default guidance: - `.cursor/memory/*.md` SHOULD be listed in the project's `.gitignore` when the project is public or shared with untrusted parties. - Files ending in `.shared.md` are **explicitly** team-shareable and should be reviewed before commit; the validator treats them identically to the base files. - NEVER write raw secrets. Use placeholders. --- ## Schema versioning Memory entries carry an optional `schema_version` field in their frontmatter. When absent, the entry is assumed to be the current version (backwards-compatible default). When present, it must be a member of `KNOWN_SCHEMA_VERSIONS` in `validate.py`. | Constant | Defined in | Purpose | |----------|-----------|---------| | `SCHEMA_VERSION` | `memory/validate.py` | The version `memory.py append` writes into every new entry. Current: `"1"`. | | `KNOWN_SCHEMA_VERSIONS` | `memory/validate.py` | Every version the validator accepts. Unknown → hard error. | ### Bumping the version 1. Implement the new field / constraint in `validate.py` and `memory.py`. 2. Add a one-shot migrator in `memory/migrate.py` (create it when the first breaking change arrives). 3. Bump `SCHEMA_VERSION` and add the old version to `KNOWN_SCHEMA_VERSIONS` so old entries still validate while the migration lands. Adding an optional field (e.g. `author_detail`) does NOT require a version bump. Bump only when an existing entry would fail validation under the new rules.