# Memory (AgentMemory)

Source: https://opvs.ai/docs/guides/memory

> These endpoints are the same six methods the @opvs-ai/agentmemory skill gives an agent, plus the one that registers a conversation source.

**AgentMemory** is an agent's long-term memory. It turns an agent's conversations into short structured memories (a decision, a learning, a pattern, a preference or a fact), lets the agent search and correct them, and writes the most useful ones into the `MEMORY.md` file the agent reads at the start of every session.

These endpoints are the same six methods the `@opvs-ai/agentmemory` skill gives an agent, plus the one that registers a conversation source. Base path: `/api/v1/memory`.

The four reads that find the **lessons near a piece of code** are further down, under [Lessons near the code](#lessons-near-the-code). A lesson can also name the test that checks it; see [A lesson checked by a test](#a-lesson-checked-by-a-test).

Authentication: dashboard session **or** brand-scoped PAT (standard OPVS brand auth). See [Authentication](/developers/authentication). Reads need the `memory:read` scope and writes need `memory:write`. Everything you read or write is scoped to the brand your token is pinned to: another brand's memories are invisible to you, and a memory id from another brand answers **404**, the same as one that does not exist.

The search and recall methods answer **YAML by default**, which costs an agent far fewer tokens. Pass `format=json` for JSON, or `format=md` for Markdown. See [Format negotiation](/developers/guides/format-negotiation).

## How a memory reaches an agent

- **From conversations.** Every 10 minutes the service picks up conversations that

have not been turned into memories yet. A conversation whose model call fails is retried with growing waits (10 minutes, 30 minutes, 2 hours, 8 hours) and is marked failed after the fifth attempt, so a failure is never recorded as "nothing to remember".

- **From your own calls.** `POST /memories` stores a memory you write yourself.
- **Into the agent's workspace.** Once an hour the highest-confidence active memories

are written into a marked section of the `MEMORY.md` in the workspace the agent actually reads. Only that section is replaced: the agent's own notes in the file are never overwritten. A correction you make with `PATCH` shows up in search and recall at once, and in `MEMORY.md` at the next hourly export.

## `POST /api/v1/memory/agents/{agent_id}/context`

Search an agent's active memories by text. The skill method is `searchMemories`. Returns active memories with confidence 0.3 or higher, ranked by how well they match `query` when you pass one. A memory not verified for more than 30 days carries `stale: true` and a `stale_caveat`.

Parameter

In

Type

Default

Notes

`agent_id`

path

string

none

The agent's workspace id, such as `acme-sdr-001`.

`query`

query

string

none

Words to match in the title and body. Omit it to list the agent's top memories.

`kinds`

query

string

all

Comma-separated: `decision`, `learning`, `pattern`, `preference`, `fact`, `summary`.

`limit`

query

integer

`10`

1 to 50.

`format`

query

string

`yaml`

`json`, `yaml` or `md`.

```bash
curl -s -X POST "https://api.opvs.ai/api/v1/memory/agents/acme-sdr-001/context?query=pricing+page&kinds=decision,fact&limit=5&format=json" \
  -H "Authorization: Bearer $OPVS_PAT"
# → 200 {"memories": […], "count": 1, "agent_id": "acme-sdr-001"}
# → 422 if limit is above 50
```

```json
{
  "memories": [
    {
      "kind": "decision",
      "title": "Quote annual prices first on the pricing page",
      "body": "Prospects compared our monthly price with competitors' annual price. Show annual first.",
      "description": "Annual price first on the pricing page",
      "confidence": 0.82,
      "tags": ["pricing", "website"]
    }
  ],
  "count": 1,
  "agent_id": "acme-sdr-001"
}
```

**Errors**

Error

When

Resolution

`401`

The PAT is malformed, expired or revoked.

Request a new token.

`403`

The token lacks `memory:read`.

Request a token with the scope.

`422`

`limit` is outside 1 to 50.

Send a value in range.

`502` / `504`

The memory service is unreachable, or did not answer within 30 seconds.

Retry with backoff.

## `POST /api/v1/memory/agents/{agent_id}/context/relevant`

Recall the memories most relevant to a topic or question. The skill method is `recallRelevant`. A text search first collects up to 100 candidates, then a language model reranks them and keeps the best `limit`. If the reranker fails, you get the text search order instead of an error.

Parameter

In

Type

Default

Notes

`agent_id`

path

string

none

The agent's workspace id.

`query`

query

string

required

The topic, question or conversation context, in plain language.

`kinds`

query

string

all

Comma-separated kinds, as above.

`limit`

query

integer

`20`

1 to 50.

`format`

query

string

`yaml`

`json`, `yaml` or `md`.

```bash
curl -s -X POST "https://api.opvs.ai/api/v1/memory/agents/acme-sdr-001/context/relevant?query=how+do+we+price+for+agencies&limit=3&format=json" \
  -H "Authorization: Bearer $OPVS_PAT"
# → 200 {"memories": […], "count": 3, "agent_id": "acme-sdr-001"}
# → 422 if query is missing
```

The response has the same shape as the search above. No match is not an error: it answers `{"memories": [], "count": 0, "agent_id": "…"}`.

**Errors**

Error

When

Resolution

`401`

The PAT is malformed, expired or revoked.

Request a new token.

`403`

The token lacks `memory:read`.

Request a token with the scope.

`422`

`query` is missing, or `limit` is outside 1 to 50.

Send a query and a limit in range.

`502` / `504`

The memory service is unreachable, or did not answer within 30 seconds.

Retry with backoff.

## `POST /api/v1/memory/memories`

Store a memory you write yourself. The skill method is `createMemory`. It always creates a new memory: sending the same memory twice stores it twice. To change one, use `PATCH`.

Field

Type

Required

Notes

`agent_id`

string

yes

The agent's workspace id: 1 to 255 letters, digits, `_` or `-`.

`kind`

string

yes

`decision`, `learning`, `pattern`, `preference` or `fact`.

`title`

string

yes

Up to 500 characters. Keep it short: it is what search shows first.

`body`

string

yes

The memory itself.

`description`

string

no

A one-line summary, up to 200 characters, shown beside the title in recall.

`tags`

string[]

no

Tags for grouping. Three learnings sharing a tag are consolidated into a pattern.

`confidence`

number

no

0.0 to 1.0, default `0.8`. It decays about 2% a week.

`importance`

string

no

`low`, `normal` (default), `high` or `critical`. A `critical` memory does not decay.

```bash
curl -s -X POST "https://api.opvs.ai/api/v1/memory/memories" \
  -H "Authorization: Bearer $OPVS_PAT" \
  -H "Content-Type: application/json" \
  -d '{
        "agent_id": "acme-sdr-001",
        "kind": "decision",
        "title": "Quote annual prices first on the pricing page",
        "body": "Prospects compared our monthly price with competitors annual price. Show annual first.",
        "description": "Annual price first on the pricing page",
        "tags": ["pricing", "website"],
        "importance": "high"
      }'
# → 201 {"id": "9b2f6a0e-4c1d-4e8a-9d3b-2f7c1a5e8b40", "status": "active", …}
# → 403 if agent_id belongs to another brand
```

The response is the full memory, with its `id`. Keep the `id` to correct or delete it later.

**Errors**

Error

When

Resolution

`401`

The PAT is malformed, expired or revoked.

Request a new token.

`403`

The token lacks `memory:write`, or `agent_id` is an agent another brand owns.

Name one of your own agents. The answer is the same whichever brand owns it.

`422`

A required field is missing, `confidence` is outside 0 to 1, `description` is over 200 characters, or `agent_id` is not a plain workspace id (it has a `.`, `/`, a space or another character outside the set).

Fix the field the error names.

`502` / `504`

The memory service is unreachable, or did not answer within 30 seconds.

Retry with backoff.

## `GET /api/v1/memory/memories/stats`

Counts for your brand's memories. The skill method is `getStats`.

Parameter

In

Type

Default

Notes

`agent_id`

query

string

all agents

Limit the counts to one agent.

`format`

query

string

`json`

`json`, `yaml` or `md`.

```bash
curl -s "https://api.opvs.ai/api/v1/memory/memories/stats?agent_id=acme-sdr-001" \
  -H "Authorization: Bearer $OPVS_PAT"
# → 200 {"total_active": 31, "total_archived": 4, "total_merged": 2, "by_kind": {…}, "avg_confidence": 0.71}
# → 401 if the token is revoked
```

```json
{
  "total_active": 31,
  "total_archived": 4,
  "total_merged": 2,
  "by_kind": { "decision": 6, "learning": 14, "fact": 11 },
  "avg_confidence": 0.71
}
```

**Errors**

Error

When

Resolution

`401`

The PAT is malformed, expired or revoked.

Request a new token.

`403`

The token lacks `memory:read`.

Request a token with the scope.

`502` / `504`

The memory service is unreachable, or did not answer within 30 seconds.

Retry with backoff.

## `PATCH /api/v1/memory/memories/{memory_id}`

Correct a memory in place. The skill method is `updateMemory`. Only the fields you send change. Correcting beats writing a second memory that contradicts the first.

Field

Type

Notes

`title`

string

Replacement title.

`body`

string

Replacement body.

`description`

string

Replacement one-line summary, up to 200 characters. Recall shows it beside the title, so when you correct the body, correct this too.

`kind`

string

Re-classify the memory.

`tags`

string[]

Replaces the whole tag list.

`confidence`

number

0.0 to 1.0.

`importance`

string

`low`, `normal`, `high` or `critical`.

`status`

string

`archived` retires a memory from search and recall without deleting it; `active` restores it.

```bash
curl -s -X PATCH "https://api.opvs.ai/api/v1/memory/memories/9b2f6a0e-4c1d-4e8a-9d3b-2f7c1a5e8b40" \
  -H "Authorization: Bearer $OPVS_PAT" \
  -H "Content-Type: application/json" \
  -d '{
        "body": "Agencies compare annual prices. Show annual first for every plan.",
        "description": "Annual price first for every plan"
      }'
# → 200 {"id": "9b2f6a0e-4c1d-4e8a-9d3b-2f7c1a5e8b40", "description": "Annual price first for every plan", …}
# → 404 if the memory is not in your brand
```

The response is the full memory after the change.

**Errors**

Error

When

Resolution

`401`

The PAT is malformed, expired or revoked.

Request a new token.

`403`

The token lacks `memory:write`.

Request a token with the scope.

`404`

No memory with that id in your brand. "Not yours" and "not there" answer the same.

Check the id with a search.

`422`

`memory_id` is not a UUID, `confidence` is outside 0 to 1, or `description` is over 200 characters.

Fix the value the error names.

`502` / `504`

The memory service is unreachable, or did not answer within 30 seconds.

Retry with backoff.

## `DELETE /api/v1/memory/memories/{memory_id}`

Delete a memory that is wrong. The skill method is `deleteMemory`. It stops appearing in search and recall at once, and leaves `MEMORY.md` at the next hourly export. For a memory that is outdated rather than wrong, `PATCH` it to `status: archived` instead.

```bash
curl -s -o /dev/null -w "%{http_code}\n" -X DELETE \
  "https://api.opvs.ai/api/v1/memory/memories/9b2f6a0e-4c1d-4e8a-9d3b-2f7c1a5e8b40" \
  -H "Authorization: Bearer $OPVS_PAT"
# → 204 (no body)
# → 404 if the memory is not in your brand, or was already deleted
```

**Errors**

Error

When

Resolution

`401`

The PAT is malformed, expired or revoked.

Request a new token.

`403`

The token lacks `memory:write`.

Request a token with the scope.

`404`

No memory with that id in your brand, or it is already deleted.

Nothing to do if you deleted it before.

`422`

`memory_id` is not a UUID.

Send the memory's `id`.

`502` / `504`

The memory service is unreachable, or did not answer within 30 seconds.

Retry with backoff.

## `POST /api/v1/memory/sources`

Register a conversation source for one of your agents, so the service turns that agent's conversations into memories. A source can only name an agent your brand owns.

Field

Type

Required

Notes

`agent_id`

string

yes

Your agent's workspace id: 1 to 255 letters, digits, `_` or `-`.

`source_type`

string

yes

`openclaw` (agent chat sessions), `agentboard` (completed tasks) or `claude_code`.

`config`

object

no

Source-specific settings.

`enabled`

boolean

no

Default `true`.

```bash
curl -s -X POST "https://api.opvs.ai/api/v1/memory/sources" \
  -H "Authorization: Bearer $OPVS_PAT" \
  -H "Content-Type: application/json" \
  -d '{"agent_id": "acme-sdr-001", "source_type": "openclaw"}'
# → 201 {"id": "3c5e9d12-7a4b-4f60-8e21-6b9d0c4f2a17", "agent_id": "acme-sdr-001", …}
# → 403 if the agent is not your brand's
```

**Errors**

Error

When

Resolution

`401`

The PAT is malformed, expired or revoked.

Request a new token.

`403`

The token lacks `memory:write`; or `agent_id` is not an agent your brand owns (another brand's, or one of the platform's own agents); or your plan's source limit is reached (the body then says `quota_exceeded`).

Name one of your own agents, or upgrade the plan. The ownership answer is the same for every agent that is not yours, so it does not reveal whether that agent exists.

`422`

`agent_id` is not a plain workspace id, or a required field is missing.

Fix the field the error names.

`503`

Ownership could not be checked just now. Nothing was created.

Retry shortly.

`502` / `504`

The memory service is unreachable, or did not answer within 30 seconds.

Retry with backoff.

## Lessons near the code

A **lesson** is a short typed entry (a rule, a decision, a gotcha) that is anchored to code: a file, a folder, or one function or class inside a file. When an agent is about to change some code, it should read the lessons on that code and on the code around it, not the whole rulebook. These four reads do that. They need a project with a code repository attached and indexed; see [Code index](/developers/guides/code-index).

Read

Answers

`POST /entries:near`

"Which lessons apply to these files, and to the code they import, call or sit next to?"

`GET /entries/search?anchor=`

"Which lessons are on this file, on its functions, or on its folders?"

`POST /anchors:lookup`

"How many lessons sit on each of these code nodes, and which are the top few?"

`POST /entries:similar`

"Is there already a lesson that says this?"

These routes answer **JSON only**; they do not take the `format` parameter. Every read is scoped to your brand: another brand's project answers **404** on `entries:near`, the same as one that does not exist, and another brand's lessons never appear in any answer.

**A lesson follows its code when the file is renamed or moved.** The code index notices the move on the next index of the repository, and the lesson's anchor is moved to the new path. From then on the lesson answers at the new path in all four reads, with no change on your side.

**A lesson goes stale when its code is deleted.** When the next index finds that the file, function or class a lesson is anchored on was removed, the lesson is marked `stale: true`, with `stale_reason` set to `code removed in <commit>`. It is not deleted: it still answers in `entries/search?anchor=`, and `GET /entries/search?stale=true` lists every stale lesson, so you can review it and retract or re-anchor it. A folder anchor does not keep a lesson alive: a lesson anchored on `src/billing/invoice.py` and on `src/billing/` goes stale when the file is deleted, even though the folder is still there. A lesson anchored only on folders goes stale when all of those folders are removed.

**A lesson can name the test that checks it.** A `Checked by:` line makes a test a guard of the lesson. All four reads then say whether that test still exists (`unguarded`), and `entries:near` puts the rules with no nearby test first. See [A lesson checked by a test](#a-lesson-checked-by-a-test).

### `POST /api/v1/memory/entries:near`

Walk the code graph outward from the files or nodes you name, then return the live lessons found on every node the walk reached. The skill method is `entriesNear`. Needs the `memory:read` **and** `code:read` scopes.

Field

Type

Default

Notes

`project_id`

UUID

required

A project of your brand with an attached repository.

`paths`

string[]

`[]`

Files to start from, written as repository paths (`src/billing/invoice.py`) or anchors (`src/billing/invoice.py#Invoice.total`). The service resolves them. A path that does not resolve to live code is skipped.

`node_ids`

UUID[]

`[]`

Stable code-node ids to start from, when you already hold them (for example from `nodes/resolve`). `paths` plus `node_ids` must total 1 to 50.

`depth`

integer

`2`

Hops to walk, 0 to 3. `0` reads only the start nodes.

`kinds`

string[]

`["import", "contains"]`

Edges to follow. `import`: file imports and imports of a named function or class. `call`: a function calling a function. `contains`: a file's folder and a folder's files, and a symbol's file. With `contains`, each hop can climb one folder level.

`direction`

string

`both`

`out` follows what a node imports, calls or contains. `in` follows what imports or calls it, and its parent. `both` follows both.

`hub_cap`

integer

`25`

1 to 200. A node reached during the walk that has more edges than this **of one kind** is a hub: it is listed in `damped` and the walk does not continue past it. Counting per kind means a file with many imports but few calls is damped only for imports. A start node is never a hub: the walk always takes every neighbour of the files you name, however many edges they have.

`limit`

integer

`20`

1 to 100. Most lessons returned.

```python
import os
import httpx

resp = httpx.post(
    "https://api.opvs.ai/api/v1/memory/entries:near",
    headers={"Authorization": f"Bearer {os.environ['OPVS_PAT']}"},
    json={
        "project_id": "2f6b1c9e-8d4a-4e2b-9c71-5a0d3e8f6b12",
        "paths": ["src/billing/invoice.py"],
        "kinds": ["import", "call", "contains"],
        "limit": 10,
    },
    timeout=60,
)
resp.raise_for_status()  # → 404 if the project is not your brand's, 422 for 0 or more than 50 start points
for lesson in resp.json()["entries"]:
    print(lesson["hops"], lesson["path"])  # → 1 lessons/billing/round-half-even.md
```

```bash
curl -s -X POST "https://api.opvs.ai/api/v1/memory/entries:near" \
  -H "Authorization: Bearer $OPVS_PAT" \
  -H "Content-Type: application/json" \
  -d '{"project_id": "2f6b1c9e-8d4a-4e2b-9c71-5a0d3e8f6b12", "paths": ["src/billing/invoice.py"], "limit": 10}'
# → 200 {"entries": […], "reached_nodes": 14, "damped": […], "truncated": false, "degraded": null}
```

```json
{
  "entries": [
    {
      "id": "billing/round-half-even",
      "path": "lessons/billing/round-half-even.md",
      "type": "rule",
      "body": "Round invoice totals half-even, never half-up: the tax authority audits the rounding mode.",
      "pinned": false,
      "stale": false,
      "stale_reason": null,
      "unguarded": false,
      "derived_from": ["INC-42"],
      "guard": {
        "anchors": ["tests/billing/test_rounding.py#test_half_even"],
        "node_ids": ["5e2a9c41-0d7b-4f83-a6e1-9b3c7d2f0a58"],
        "paths": ["tests/billing/test_rounding.py#test_half_even"],
        "in_walk": false,
        "unguarded": false
      },
      "hops": 1,
      "via_node_id": "7a1d0c3e-5b9f-4d26-8e40-2c6f1b8a9d73",
      "path_to": [
        {"node_id": "c4e8a2f1-9b3d-4a57-b6e0-1f2d3c4b5a69", "path": "src/billing/invoice.py", "edge": null},
        {"node_id": "7a1d0c3e-5b9f-4d26-8e40-2c6f1b8a9d73", "path": "src/billing/money.py", "edge": "import"}
      ]
    }
  ],
  "reached_nodes": 14,
  "damped": [
    {"node_id": "0b9e4d2c-6a1f-4c38-9e57-3d8a2b1c0f46", "path": "src/core/settings.py", "degree": 212}
  ],
  "truncated": false,
  "degraded": null
}
```

Response field

Meaning

`entries[]`

Live lessons, each listed once at the nearest node that carries it. Pinned lessons first, then fewer hops; inside one hop count, a lesson whose guard is in the walk comes after one whose guard is not, or that has none. A small `limit` keeps the top of this full ranking.

`entries[].hops`

`0`: the lesson is on a start node or anywhere under it. `1` or more: on a node the walk reached.

`entries[].via_node_id`, `path_to`

The node the lesson was found on, and the chain of nodes from a start node to it. Each step's `edge` is the edge kind that reached it (`import`, `symbol_import`, `call` or `contains`); the first step's is `null`.

`entries[].guard`

`null` when the lesson declares no guard. Otherwise `anchors` (the declared strings), `node_ids`, `paths` (each guard's current path, which differs from the declared one after a rename), `in_walk` (`true` when a guard node, or the file it is in, was reached by this walk) and `unguarded`. See [A lesson checked by a test](#a-lesson-checked-by-a-test).

`reached_nodes`

How many code nodes the walk reached, start nodes included. `0` means none of your start points is live code.

`damped[]`

Hubs the walk stopped at, with their edge count of the kind that made them a hub.

`truncated`

`true` when lessons were left out: more matched than `limit`, a reached node carries more than 100 lessons, or the walk reached its cap of 1,000 nodes.

`guard_lookup_truncated`

Present, and `true`, only when the lessons found have more than 5,000 distinct guards that the walk did not reach directly. Past 5,000, a guard counts as in the walk only when its own node was reached, not merely its file, so it may read `in_walk: false`. Absent otherwise.

`degraded`

`null`, or why the answer is empty or partial: `no_ready_index`, `code_index_unavailable` or `memory_unavailable`. A degraded answer is still `200`.

A folder that the walk reaches contributes only the lessons anchored on that folder itself, not every lesson under it; a start node contributes everything under it.

**How many nodes "near" is, measured.** On a production repository (2026-10-11), with the defaults (`import` and `contains`, `hub_cap` 25):

Start file

Depth 1: nodes / lessons

Depth 2: nodes / lessons

A hub, imported by 139 files

149 / 7

937 / 7

A mid-sized file

76 / 3

109 / 3

A leaf file

40 / 3

45 / 3

So the default answer is a handful of nearby lessons, not the whole repository. The node count grows fast once a walk passes a hub: with `hub_cap` 200, the mid-sized file's depth-2 walk reached 719 nodes (7 lessons) instead of 109, and the hub's reached the 1,000-node cap (`truncated: true`). Raise `hub_cap` only when you want the walk to continue through the hubs it meets along the way.

**Errors**

Error

When

Resolution

`401`

The PAT is malformed, expired or revoked.

Request a new token.

`403`

The token lacks `memory:read` or `code:read`.

Request a token with both scopes.

`404`

The project does not exist, belongs to another brand, or has no repository attached.

Check `project_id`; attach a repository to the project first.

`422`

`paths` plus `node_ids` is 0 or more than 50; a field is out of range or unknown; or a path is not a valid anchor.

Fix the field the error names.

`200` with `degraded`

The project has no ready index yet (`no_ready_index`), or the code index or the memory service could not be read.

Retry later; for `no_ready_index`, wait for the first index of the repository.

### `GET /api/v1/memory/entries/search?anchor=`

Return the lessons on one file and around it: anchored on the file itself, on a function or class inside it, or on one of its folders up to the repository root. The skill method is `entryContext`. Needs `memory:read`.

Parameter

Type

Default

Notes

`anchor`

string

required here

A repository path such as `src/billing/invoice.py`, or one symbol in it (`src/billing/invoice.py#Invoice.total`).

`project_id`

UUID

none

Limit folder and function matches to one project. Recommended.

`q`

string

none

Words to match within those lessons.

`type`

string

all

One lesson type, such as `rule`.

`scope`

string

all

One scope tag.

`unguarded`

boolean

none

`true`: only lessons whose every guard is gone. `false`: only the others. See [A lesson checked by a test](#a-lesson-checked-by-a-test).

`derived_from`

string

none

Only lessons that name exactly this reference on a `Derived from:` line, such as `INC-42`.

`limit`

integer

`20`

1 to 500.

```python
import os
import httpx

resp = httpx.get(
    "https://api.opvs.ai/api/v1/memory/entries/search",
    headers={"Authorization": f"Bearer {os.environ['OPVS_PAT']}"},
    params={
        "anchor": "src/billing/invoice.py",
        "project_id": "2f6b1c9e-8d4a-4e2b-9c71-5a0d3e8f6b12",
        "limit": 10,
    },
)
resp.raise_for_status()  # → 403 if the token lacks memory:read
for item in resp.json()["items"]:
    print(item["matched_by"], item["path"])  # → anchor lessons/billing/invoice-immutable.md
```

```json
{
  "query": "",
  "total": 3,
  "items": [
    {"id": "billing/invoice-immutable", "path": "lessons/billing/invoice-immutable.md", "type": "rule", "matched_by": "anchor", "pinned": true, "anchors": ["src/billing/invoice.py"]},
    {"id": "billing/total-includes-tax", "path": "lessons/billing/total-includes-tax.md", "type": "gotcha", "matched_by": "symbol", "pinned": false, "anchors": ["src/billing/invoice.py#Invoice.total"]},
    {"id": "billing/no-floats", "path": "lessons/billing/no-floats.md", "type": "rule", "matched_by": "folder", "pinned": false, "anchors": ["src/billing/"]}
  ],
  "warnings": []
}
```

Each item carries the full lesson (body, author, card, anchors and so on; abridged above) and `matched_by`: `anchor` (on the file itself), `symbol` (on a function or class in the file), `folder` (on one of its folders) or `guard` (the file is a test that checks the lesson). Pinned lessons come first, then `anchor`, then `symbol`, then `folder` with the nearest folder first, then `guard`. A lesson that matches more than one way is listed once, under its first match in that order.

**Errors**

Error

When

Resolution

`401`

The PAT is malformed, expired or revoked.

Request a new token.

`403`

The token lacks `memory:read`.

Request a token with the scope.

`422`

`limit` is outside 1 to 500, or `project_id` is not a UUID.

Send a value in range.

`502` / `504`

The memory service is unreachable, or did not answer in time.

Retry with backoff.

### `POST /api/v1/memory/anchors:lookup`

Count the live lessons on each of up to 1,000 code nodes, and return the top few (five by default) for each node. The dashboard's Code view uses it to show how many lessons sit on each folder and file. The skill method is `anchorsLookup`. Needs `memory:read`.

Field

Type

Default

Notes

`project_id`

UUID

required

The project the node ids belong to.

`node_ids`

UUID[]

required

Up to 1,000 stable code-node ids.

`match`

string

`subtree`

`subtree` counts lessons on the node or anywhere under it. `exact` counts only lessons on the node itself.

`per_node_limit`

integer

`5`

1 to 100. Most lessons returned in `entries` for each node. `counts` is not affected.

```python
import os
import httpx

resp = httpx.post(
    "https://api.opvs.ai/api/v1/memory/anchors:lookup",
    headers={"Authorization": f"Bearer {os.environ['OPVS_PAT']}"},
    json={
        "project_id": "2f6b1c9e-8d4a-4e2b-9c71-5a0d3e8f6b12",
        "node_ids": ["c4e8a2f1-9b3d-4a57-b6e0-1f2d3c4b5a69"],
        "match": "exact",
    },
)
resp.raise_for_status()  # → 422 if node_ids holds more than 1,000 ids
print(resp.json()["counts"])  # → {"c4e8a2f1-9b3d-4a57-b6e0-1f2d3c4b5a69": 2}
```

```json
{
  "counts": {"c4e8a2f1-9b3d-4a57-b6e0-1f2d3c4b5a69": 2},
  "entries": {
    "c4e8a2f1-9b3d-4a57-b6e0-1f2d3c4b5a69": [
      {"id": "billing/invoice-immutable", "path": "lessons/billing/invoice-immutable.md", "type": "rule", "body": "Never edit an issued invoice; issue a credit note.", "pinned": true, "stale": false, "stale_reason": null}
    ]
  }
}
```

`counts` lists only nodes with at least one lesson; a node with none is absent, not `0`. `entries` holds at most `per_node_limit` lessons per node, pinned first, then newest. Retired and superseded lessons are never counted. Each lesson also carries `unguarded`, `guard_anchors` (the declared guard strings), `guard_paths` (each guard's current path, empty once the test is deleted) and `guard_node_ids`. See [A lesson checked by a test](#a-lesson-checked-by-a-test).

**Errors**

Error

When

Resolution

`401`

The PAT is malformed, expired or revoked.

Request a new token.

`403`

The token lacks `memory:read`.

Request a token with the scope.

`422`

More than 1,000 `node_ids`, an id that is not a UUID, `match` is not `subtree` or `exact`, or `per_node_limit` is outside 1 to 100.

Split the request into pages of 1,000, or send a value in range.

`502` / `504`

The memory service is unreachable, or did not answer in time.

Retry with backoff.

### `POST /api/v1/memory/entries:similar`

Compare a piece of text with your brand's live lessons by meaning, and return the closest ones. Use it before writing a lesson, to avoid a second copy of one that already exists. It writes nothing. The skill method is `entriesSimilar`. Needs `memory:read`.

Field

Type

Default

Notes

`text`

string

required

The lesson you are about to write, or its gist.

`type`

string

all

Compare only with lessons of this type.

`k`

integer

`3`

1 to 10. Most matches returned.

`threshold`

number

`0.90`

-1 to 1. Lowest cosine similarity that counts as a match. Lower it to see looser matches.

```python
import os
import httpx

resp = httpx.post(
    "https://api.opvs.ai/api/v1/memory/entries:similar",
    headers={"Authorization": f"Bearer {os.environ['OPVS_PAT']}"},
    json={"text": "Round invoice totals half-even, not half-up", "k": 3, "threshold": 0.8},
)
resp.raise_for_status()  # → 422 if text is empty or k is above 10
for match in resp.json()["matches"]:
    print(match["similarity"], match["path"])  # → 0.946213 lessons/billing/round-half-even.md
```

```json
{
  "matches": [
    {"id": "billing/round-half-even", "path": "lessons/billing/round-half-even.md", "similarity": 0.946213, "type": "rule", "pinned": false}
  ],
  "compared": 412,
  "without_embedding": 3
}
```

`compared` is how many lessons were compared. A new lesson is compared once its embedding has been computed in the background; until then it is counted in `without_embedding`. If the embedding service is down, the answer is `200` with no matches and a `degraded` reason, never an error.

**Errors**

Error

When

Resolution

`401`

The PAT is malformed, expired or revoked.

Request a new token.

`403`

The token lacks `memory:read`.

Request a token with the scope.

`422`

`text` is empty, `k` is outside 1 to 10, or `threshold` is outside -1 to 1.

Fix the field the error names.

`502` / `504`

The memory service is unreachable, or did not answer in time.

Retry with backoff.

### Duplicate warnings on writes

Lessons written in a batch are checked the same way. When a repository's knowledge files are synced into lessons, each written lesson in the response can carry `possible_duplicate_of`: up to three existing lessons at 0.90 similarity or above, each with its `id`, `path` and `similarity`. The check never blocks a write. It covers up to 200 lessons per write and fully compares the first 25; when a write holds more, the response says `"duplicate_check": "truncated"` and the lessons past that point carry no `possible_duplicate_of`. Check those with `entries:similar`.

## A lesson checked by a test

A rule is safest when a test checks it. A lesson can name the test or script that checks it, and the incident or card it came from. OPVS then tells you two things. It tells you when that test is gone, so the rule now has no check (the lesson is **unguarded**). And when an agent is about to change some code, it tells the agent which nearby rules have a test close to that code, and which do not.

### Where these lessons come from

Every index of a project's attached branch also reads the repository's knowledge files: `CLAUDE.md`, `AGENTS.md`, `GEMINI.md`, `LEARNINGS.md`, `.cursorrules`, `.windsurfrules`, `.cursor/rules/*.mdc`, `.github/copilot-instructions.md`, `.claude/rules/*.md` and `learnings/*.md`, at any depth (at most 400 files, 512 KB each). Each `##` section of such a file is one lesson, and so is the text before its first `##` heading. A lesson is anchored on its file and on the file's folder. Its id is `knowledge:` + the project id + `:` + the file path, plus `#` and the heading turned into a slug for a section, for example `knowledge:2f6b1c9e-8d4a-4e2b-9c71-5a0d3e8f6b12:src/billing/LEARNINGS.md#round-totals-half-even`. A lesson is re-read on every index, so editing the file is how you change it. [Your repo's knowledge in AgentMemory](/developers/guides/repo-knowledge) covers what happens when a file changes, and `opvs memory sync` for a repository no machine indexes.

### Declare a guard and an origin

Write the two lines inside the section they belong to:

```markdown
## Round totals half-even

Round invoice totals half-even, never half-up: the tax authority audits the rounding mode.

Checked by: tests/billing/test_rounding.py#test_half_even, scripts/check_rounding.sh
Derived from: INC-42, https://status.example.com/incidents/2026-03-02
```

Line

Values

Rules

`Checked by:`

The tests or scripts that check this rule, as anchors: a repository path (`scripts/check_rounding.sh`) or one function or class in it (`tests/billing/test_rounding.py#test_half_even`). See [Anchor grammar](/developers/guides/code-index#anchor-grammar).

Each one becomes a **guard** of the lesson. A value that is not an anchor, such as a URL, is dropped.

`Derived from:`

Where the rule came from: a card or incident reference in capitals (`INC-42`, `PAY-118`), or an `http` or `https` URL.

A lowercase reference (`inc-42`) or any other text is dropped.

Both lines follow the same rules:

- The key must start the line, in any letter case (`checked by:` works). An indented line is

not read.

- A line inside a code fence (three or more backticks or tildes) is never read, so you can

quote an example safely.

- A comma always separates two values. Write a comma inside a URL as `%2C`:

`https://example.com/a,b` is read as two values, and the second is dropped.

- A section keeps at most 10 values of each kind; several lines of the same kind add up.

Values past 10, and values that are not valid, are dropped. A dropped value never fails the sync.

### What the reads show

- `derived_from` is on every lesson read: a list of the references, `[]` when none was

declared. `GET /entries/search?derived_from=INC-42` lists the lessons that name exactly that reference.

- **The guards** are anchors with `role: "guard"` in the lesson's `anchor_states`. The lesson's

own file and folder anchors have `role: "subject"`. Read them with `GET /entries/{entry_id}`, below.

- `unguarded: true` means the lesson declares at least one guard and **every** guard is

gone: the test was deleted, or the anchor never matched any code (a typo, or a test not yet written). It is set by the first index after the deletion. Each guard row then reads `state: "removed"` and `anchor_missing: true`, with `stale_reason` naming the commit. `GET /entries/search?unguarded=true` lists every such lesson, so you can review it: add a test, or remove the `Checked by:` line.

- `unguarded`** is not **`stale`**.** `stale` is about the code the lesson is anchored on (see

above). A lesson whose test was deleted still applies to its code. It reads `stale: false` and `unguarded: true`.

- **A renamed or moved test keeps guarding.** It does not flag the lesson, because the test

still runs. After the index that sees the move, the guard row reads `state: "live"`, its `node_path` is the new path, `relocated_from` is the path you declared, and `relocated_at` is when the move was seen. The declared text in your file does not change, so the reads carry both: the declared string (`anchor`, `guard_anchors`, `guard.anchors`) and the current path (`node_path`, `guard_paths`, `guard.paths`). Measured on production on 15 October 2026: the moved guard read the new path through the rename index and two later indexes, and `relocated_at` kept its value across the next index.

- **Editing a test shows the rules it protects.** `GET /entries/search?anchor=` on a test file

returns the lessons whose guard is in that file, with `matched_by: "guard"`. A lesson that also matches the file another way reads that match instead (`anchor`, `symbol` or `folder`). So a lesson in the repository's root `LEARNINGS.md`, which is anchored on the root folder, reads `matched_by: "folder"` for every file.

- `entries:near`** ranks by guard.** Inside one hop count, a lesson whose guard is in the

walk comes **after** a lesson whose guard is outside it, or that has no guard. A test near the code you are changing is likely to run against your change; a rule with no such test is the one to read first. Each entry carries `guard` (see `entries:near` above). A small `limit` returns the top of the full ranking: measured on production on 15 October 2026, `limit: 1` returned the lesson whose guard was outside the walk, and `truncated: true`. The walk still finds lessons by their subject anchors only; it never walks through a guard.

- `anchors:lookup` items carry `guard_anchors`, `guard_paths`, `guard_node_ids` and

`unguarded`.

- **Brand scope holds.** A caller outside your brand reads none of these lessons: asking as

your brand is refused (`403`), and asking as its own brand finds nothing.

### Find the rules whose tests are gone

```python
import os
import httpx

resp = httpx.get(
    "https://api.opvs.ai/api/v1/memory/entries/search",
    headers={"Authorization": f"Bearer {os.environ['OPVS_PAT']}"},
    params={"unguarded": "true", "type": "knowledge", "limit": 100},
)
resp.raise_for_status()  # → 403 if the token lacks memory:read
for lesson in resp.json()["items"]:
    print(lesson["path"], lesson["derived_from"])  # → src/billing/LEARNINGS.md ['INC-42']
```

```bash
curl -s "https://api.opvs.ai/api/v1/memory/entries/search?unguarded=true&type=knowledge&limit=100" \
  -H "Authorization: Bearer $OPVS_PAT"
# → 200 {"query": "", "total": 1, "items": [{"id": "knowledge:2f6b1c9e-8d4a-4e2b-9c71-5a0d3e8f6b12:src/billing/LEARNINGS.md#round-totals-half-even", "unguarded": true, "stale": false}], "warnings": []}
```

`total` counts the lessons in this answer, not every match. If it equals `limit`, raise `limit` (up to 500) to see the rest.

### `GET /api/v1/memory/entries/{entry_id}`

Read one lesson whole, including the state of each of its anchors. Needs `memory:read`.

Parameter

Type

Notes

`entry_id`

string, in the path

The lesson's id. Encode `#` as `%23`. An unencoded `#` is cut off by the HTTP client, so the request returns the file's first lesson instead of the section you asked for, with a `200`.

```python
import os
import urllib.parse
import httpx

lesson_id = "knowledge:2f6b1c9e-8d4a-4e2b-9c71-5a0d3e8f6b12:src/billing/LEARNINGS.md#round-totals-half-even"
resp = httpx.get(
    "https://api.opvs.ai/api/v1/memory/entries/" + urllib.parse.quote(lesson_id, safe=":/"),
    headers={"Authorization": f"Bearer {os.environ['OPVS_PAT']}"},
)
resp.raise_for_status()  # → 404 {"detail": "Entry not found"} for an unknown id or another brand's lesson
lesson = resp.json()
print(lesson["unguarded"], [a["node_path"] for a in lesson["anchor_states"] if a["role"] == "guard"])
# → False ['tests/billing/test_rounding_moved.py']
```

```json
{
  "id": "knowledge:2f6b1c9e-8d4a-4e2b-9c71-5a0d3e8f6b12:src/billing/LEARNINGS.md#round-totals-half-even",
  "path": "src/billing/LEARNINGS.md",
  "type": "knowledge",
  "derived_from": ["INC-42", "https://status.example.com/incidents/2026-03-02"],
  "anchor_states": [
    {"anchor": "src/billing/", "role": "subject", "node_path": "src/billing", "state": "live", "anchor_missing": false, "stale_since": null, "stale_reason": null, "relocated_from": null, "relocated_at": null},
    {"anchor": "src/billing/LEARNINGS.md", "role": "subject", "node_path": "src/billing/LEARNINGS.md", "state": "live", "anchor_missing": false, "stale_since": null, "stale_reason": null, "relocated_from": null, "relocated_at": null},
    {"anchor": "tests/billing/test_rounding.py#test_half_even", "role": "guard", "node_path": "tests/billing/test_rounding_moved.py", "state": "live", "anchor_missing": false, "stale_since": null, "stale_reason": null, "relocated_from": "tests/billing/test_rounding.py", "relocated_at": "2026-10-15T17:18:38.704182+00:00"}
  ],
  "stale": false,
  "stale_reason": null,
  "unguarded": false,
  "synced_commit": "9eb2e43acd3863f450d40d76292b92a9316aa2ab"
}
```

The answer above is abridged: a lesson also carries its body, author, header and the other fields that `entries/search` returns. Here the guard test was renamed: the declared `anchor` is the old path, and `node_path` is where the test lives now. After the test is deleted, the same row reads `"state": "removed"`, `"anchor_missing": true` and `"stale_reason": "code removed in 49b86b8939895b8bf1513ba9512bff5af2493299"`, and the lesson reads `"unguarded": true` with `"stale": false`.

**Errors**

Error

When

Resolution

`401`

The PAT is malformed, expired or revoked.

Request a new token.

`403`

The token lacks `memory:read`, or the token is not a member of the brand it asks for.

Request a token with the scope, pinned to your brand.

`404`

No lesson has this id in your brand: it does not exist, or it is another brand's.

Check the id, and encode `#` as `%23`.

`502` / `504`

The memory service is unreachable, or did not answer in time.

Retry with backoff.
