diff --git a/apps/docs/docs.json b/apps/docs/docs.json
index 0d41141c..802a8e4e 100644
--- a/apps/docs/docs.json
+++ b/apps/docs/docs.json
@@ -218,6 +218,23 @@
{
"group": "Migration Guides",
"pages": [
+ {
+ "group": "V3/V4 to V5",
+ "icon": "arrow-up-right",
+ "pages": [
+ "migration/api-v5",
+ "migration/api-v5-document-writes",
+ "migration/api-v5-document-updates",
+ "migration/api-v5-document-reads",
+ "migration/api-v5-recall",
+ "migration/api-v5-profiles",
+ "migration/api-v5-memory-forgetting",
+ "migration/api-v5-settings",
+ "migration/api-v5-organization",
+ "migration/api-v5-filters",
+ "migration/api-v5-rollout"
+ ]
+ },
{
"group": "From another provider",
"icon": "truck",
diff --git a/apps/docs/migration/api-v5-document-reads.mdx b/apps/docs/migration/api-v5-document-reads.mdx
new file mode 100644
index 00000000..90fca05a
--- /dev/null
+++ b/apps/docs/migration/api-v5-document-reads.mdx
@@ -0,0 +1,11 @@
+---
+title: "Reading and deleting content on V5"
+description: "Fetching documents, listing resources, and deleting documents or memories"
+sidebarTitle: "Reading content"
+---
+
+import ContentManagement from "/snippets/api-v5-content-management.mdx";
+
+## Migration details
+
+
diff --git a/apps/docs/migration/api-v5-document-updates.mdx b/apps/docs/migration/api-v5-document-updates.mdx
new file mode 100644
index 00000000..c80bf773
--- /dev/null
+++ b/apps/docs/migration/api-v5-document-updates.mdx
@@ -0,0 +1,11 @@
+---
+title: "Updating documents and files on V5"
+description: "Pick deliberately between appending, replacing, and touching only metadata"
+sidebarTitle: "Updating documents"
+---
+
+import DocumentUpdates from "/snippets/api-v5-document-updates.mdx";
+
+## Migration details
+
+
diff --git a/apps/docs/migration/api-v5-document-writes.mdx b/apps/docs/migration/api-v5-document-writes.mdx
new file mode 100644
index 00000000..f6a59240
--- /dev/null
+++ b/apps/docs/migration/api-v5-document-writes.mdx
@@ -0,0 +1,11 @@
+---
+title: "Writing documents on V5"
+description: "Single, batch, and file ingestion that keeps legacy append semantics"
+sidebarTitle: "Ingesting documents"
+---
+
+import DocumentIngestion from "/snippets/api-v5-document-ingestion.mdx";
+
+## Migration details
+
+
diff --git a/apps/docs/migration/api-v5-filters.mdx b/apps/docs/migration/api-v5-filters.mdx
new file mode 100644
index 00000000..d04858a6
--- /dev/null
+++ b/apps/docs/migration/api-v5-filters.mdx
@@ -0,0 +1,11 @@
+---
+title: "Metadata filters on V5"
+description: "Turning loose legacy filter objects into expressions the server validates"
+sidebarTitle: "Filter expressions"
+---
+
+import Filters from "/snippets/api-v5-filters.mdx";
+
+## Migration details
+
+
diff --git a/apps/docs/migration/api-v5-memory-forgetting.mdx b/apps/docs/migration/api-v5-memory-forgetting.mdx
new file mode 100644
index 00000000..684314b5
--- /dev/null
+++ b/apps/docs/migration/api-v5-memory-forgetting.mdx
@@ -0,0 +1,11 @@
+---
+title: "Forgetting memories on V5"
+description: "Exact and semantic deletion now share a single response contract"
+sidebarTitle: "Forgetting memories"
+---
+
+import MemoryForgetting from "/snippets/api-v5-memory-forgetting.mdx";
+
+## Migration details
+
+
diff --git a/apps/docs/migration/api-v5-organization.mdx b/apps/docs/migration/api-v5-organization.mdx
new file mode 100644
index 00000000..7c2cb9b8
--- /dev/null
+++ b/apps/docs/migration/api-v5-organization.mdx
@@ -0,0 +1,11 @@
+---
+title: "Organization settings on V5"
+description: "What remains is the shared context string and a read-only namespace count"
+sidebarTitle: "Organization context"
+---
+
+import Organization from "/snippets/api-v5-organization.mdx";
+
+## Migration details
+
+
diff --git a/apps/docs/migration/api-v5-profiles.mdx b/apps/docs/migration/api-v5-profiles.mdx
new file mode 100644
index 00000000..dbb27513
--- /dev/null
+++ b/apps/docs/migration/api-v5-profiles.mdx
@@ -0,0 +1,11 @@
+---
+title: "Profiles and buckets on V5"
+description: "Profiles no longer take search parameters; buckets get their own resource"
+sidebarTitle: "Profiles & buckets"
+---
+
+import Profiles from "/snippets/api-v5-profiles.mdx";
+
+## Migration details
+
+
diff --git a/apps/docs/migration/api-v5-recall.mdx b/apps/docs/migration/api-v5-recall.mdx
new file mode 100644
index 00000000..a2d46703
--- /dev/null
+++ b/apps/docs/migration/api-v5-recall.mdx
@@ -0,0 +1,11 @@
+---
+title: "Searching on V5"
+description: "Modes, filters, attachments, changed defaults, and response readers"
+sidebarTitle: "Searching"
+---
+
+import Search from "/snippets/api-v5-search.mdx";
+
+## Migration details
+
+
diff --git a/apps/docs/migration/api-v5-rollout.mdx b/apps/docs/migration/api-v5-rollout.mdx
new file mode 100644
index 00000000..3e5c04f7
--- /dev/null
+++ b/apps/docs/migration/api-v5-rollout.mdx
@@ -0,0 +1,11 @@
+---
+title: "Checking parity, then rolling out V5"
+description: "Establish behavioral parity, account for intended differences, and cut over safely"
+sidebarTitle: "Parity and rollout"
+---
+
+import Rollout from "/snippets/api-v5-rollout.mdx";
+
+## Migration details
+
+
diff --git a/apps/docs/migration/api-v5-settings.mdx b/apps/docs/migration/api-v5-settings.mdx
new file mode 100644
index 00000000..9f88f6d9
--- /dev/null
+++ b/apps/docs/migration/api-v5-settings.mdx
@@ -0,0 +1,11 @@
+---
+title: "Container tags become namespaces"
+description: "Listing namespaces, editing their settings, deleting them, and moving their content"
+sidebarTitle: "Namespace lifecycle"
+---
+
+import Namespaces from "/snippets/api-v5-namespaces.mdx";
+
+## Migration details
+
+
diff --git a/apps/docs/migration/api-v5.mdx b/apps/docs/migration/api-v5.mdx
new file mode 100644
index 00000000..67152a98
--- /dev/null
+++ b/apps/docs/migration/api-v5.mdx
@@ -0,0 +1,63 @@
+---
+title: "Moving a V3/V4 integration onto V5"
+description: "Move existing API integrations onto the namespace-scoped V5 surface"
+sidebarTitle: "V3/V4 to V5"
+icon: "arrow-up-right"
+---
+
+import Overview from "/snippets/api-v5-overview.mdx";
+import DocumentIngestion from "/snippets/api-v5-document-ingestion.mdx";
+import DocumentUpdates from "/snippets/api-v5-document-updates.mdx";
+import ContentManagement from "/snippets/api-v5-content-management.mdx";
+import Search from "/snippets/api-v5-search.mdx";
+import Profiles from "/snippets/api-v5-profiles.mdx";
+import MemoryForgetting from "/snippets/api-v5-memory-forgetting.mdx";
+import Namespaces from "/snippets/api-v5-namespaces.mdx";
+import Organization from "/snippets/api-v5-organization.mdx";
+import Filters from "/snippets/api-v5-filters.mdx";
+import Rollout from "/snippets/api-v5-rollout.mdx";
+import Completion from "/snippets/api-v5-completion.mdx";
+
+
+
+## Document ingestion
+
+
+
+## Document updates
+
+
+
+## Content management
+
+
+
+## Search
+
+
+
+## Profiles and buckets
+
+
+
+## Memory forgetting
+
+
+
+## Namespaces
+
+
+
+## Organization settings
+
+
+
+## Typed filters
+
+
+
+## Verification and rollout
+
+
+
+
diff --git a/apps/docs/snippets/api-v5-completion.mdx b/apps/docs/snippets/api-v5-completion.mdx
new file mode 100644
index 00000000..bfcb2907
--- /dev/null
+++ b/apps/docs/snippets/api-v5-completion.mdx
@@ -0,0 +1,29 @@
+## Agent migration prompt
+
+```text
+Move this repository off the legacy Supermemory V3/V4 API and onto V5. Begin by
+taking inventory: list every call site still targeting a legacy route. Then work
+through the per-domain guides linked from this page. Address exactly one
+namespace in each request. Where a default differs between the two versions,
+state it in the request rather than relying on whatever the server supplies.
+Bring response readers up to date as well. Back the change with contract tests
+that exercise old and new side by side. Leave application routes unprefixed, and
+leave connector routes alone. When an operation has been dropped, name it as
+dropped rather than manufacturing a stand-in.
+```
+
+## Operations without a direct replacement
+
+- **Direct memory creation and version updates** (`POST /v4/memories`,
+ `PATCH /v4/memories`): ingest or replace a source document instead.
+- **Organization bucket suggestion** (`POST /v3/settings/suggest-buckets`) and
+ **organization reset** (`POST /v3/settings/reset`): not part of the V5 public
+ surface.
+- **Connector routes** that still use `/v3`: unchanged; do not rewrite them.
+
+## Where to go next
+
+- [Authentication](/authentication) — unchanged bearer keys
+- [Search guide](/recall/search)
+- [Memory operations](/recall/memory-operations)
+- [API reference](/api-reference/overview) — legacy V3/V4 reference
diff --git a/apps/docs/snippets/api-v5-content-management.mdx b/apps/docs/snippets/api-v5-content-management.mdx
new file mode 100644
index 00000000..d523b31d
--- /dev/null
+++ b/apps/docs/snippets/api-v5-content-management.mdx
@@ -0,0 +1,105 @@
+In V5, retrieving, listing, and deleting all happen inside one namespace you name
+explicitly.
+
+### Retrieve a document and its derived context
+
+
+```bash Legacy
+GET /v3/documents/{id}
+GET /v3/documents/{id}/chunks
+```
+
+```bash V5
+GET /ns/{namespace}/document/{id}?attach=chunks&attach=memories
+```
+
+
+Supply `attach` more than once to pull in chunks, memories, or both. An
+attachment you never asked for is simply **missing** from the payload, whereas one
+you did ask for that found nothing comes back as an **empty array**. The
+difference is real: `result.chunks === undefined` and `result.chunks.length === 0`
+do not mean the same thing.
+
+The lifecycle fields have moved beneath `system`:
+
+```json
+{"system":{"status":"done","createdAt":"...","updatedAt":"..."}}
+```
+
+Legacy `GET /v3/documents/{id}` returned `status`, `createdAt`, and `updatedAt`
+as top-level fields, and needed a separate `GET /v3/documents/{id}/chunks` call
+for chunk data. V5 folds both into one request.
+
+### List documents, chunks, or memories
+
+
+```bash Legacy
+POST /v3/documents/list
+POST /v4/memories/list
+```
+
+```bash V5
+POST /ns/{namespace}/list/{type}?page=1&limit=100&sort=createdAt&order=desc
+{}
+```
+
+
+Give `type` one of `documents`, `chunks`, or `memories`. Paging and ordering move
+into the **query string**; the body carries nothing beyond an optional `filter`.
+
+Whichever type you ask for, the response carries `documents`, `chunks`,
+`memories`, and `pagination` — but only the array you selected has entries in it.
+Code that used to pull `memories` out of a document list has to read `documents`
+now.
+
+
+ This is the one place where the legacy and V5 shapes diverge most sharply.
+ `ListMemoriesQuerySchema` in this repository takes `page`, `limit`, `sort`,
+ `order`, and a **stringified** `filters` value in the query string; V5 keeps
+ the paging controls in the query string but moves filtering into a typed body
+ field.
+
+
+### Delete documents
+
+
+```bash Legacy
+DELETE /v3/documents/{id}
+DELETE /v3/documents/bulk
+```
+
+```bash V5
+DELETE /ns/{namespace}/document
+{"ids":["doc_1","external_id_2"]}
+```
+
+
+The V5 array accepts **1–100** Supermemory or caller-defined IDs. Inspect both
+`deletedCount` **and** per-ID `errors`: HTTP success can still include partial
+failures. The legacy bulk schema capped `ids` at 100 as well, so the limit is
+unchanged — but the legacy single-delete route is gone, and one call now covers
+both cases.
+
+### Forget memories
+
+| Legacy intent | V5 operation |
+| --- | --- |
+| Forget exact IDs | `DELETE /ns/{namespace}/memories` with `{"ids":[...]}` |
+| Find memories by meaning | `DELETE /ns/{namespace}/memories/semantic` with `{"query":"...","dryRun":true}` |
+
+Both answer with `{ count, matches, errors }`. To delete semantically without
+drift, preview under `dryRun: true`, inspect the returned IDs, and send that exact
+list to the exact-ID endpoint.
+
+The full dry-run, approval, response, and audit flow lives in
+[memory forgetting](./api-v5-memory-forgetting).
+
+### Verification
+
+- Assert that requested-but-empty attachments are `[]` while omitted attachments
+ are absent.
+- Paginate each resource type until `currentPage >= totalPages`; confirm
+ unselected arrays stay empty.
+- Verify chunk rows carry their parent `documentId`.
+- Try a document delete that only partly succeeds, and a semantic dry run.
+- Verify an ID cannot read, list, or delete content outside its namespace.
diff --git a/apps/docs/snippets/api-v5-document-ingestion.mdx b/apps/docs/snippets/api-v5-document-ingestion.mdx
new file mode 100644
index 00000000..67fbaf0e
--- /dev/null
+++ b/apps/docs/snippets/api-v5-document-ingestion.mdx
@@ -0,0 +1,91 @@
+V5 moves document scope into the URL and keeps a repeated caller-supplied ID attached to one evolving document instead of creating a new one each time.
+
+### Rename common fields
+
+| Legacy | V5 | Location |
+| --- | --- | --- |
+| `containerTag` | `{namespace}` | Path |
+| `customId` | `id` | JSON body |
+| `entityContext` | `supportingContext` | JSON / form body |
+| `filterByMetadata` | `group` | JSON / form body |
+| `documentDate` | `date` | JSON / form body |
+| `taskType`, `dreaming` | unchanged | Query string |
+
+In the legacy schema these live in `MemoryUpdateSchema` (`customId`,
+`entityContext`, `metadata`, `containerTags`) and are sent in the request body.
+In V5 the scope leaves the body entirely and becomes a path segment, so a single
+request can only ever address one namespace.
+
+### Add or append one document
+
+
+```bash Legacy
+POST /v3/documents
+{"content":"new turn","customId":"conv_1","containerTag":"user_1"}
+```
+
+```bash V5
+POST /ns/user_1/document?dreaming=dynamic
+{"content":"new turn","id":"conv_1"}
+```
+
+
+The response is still an acceptance result carrying `id` and `status`. Repeating
+the V5 request with the same `id: "conv_1"` adds or diffs the new content into
+that document — it does **not** silently replace the canonical source.
+
+### Batch ingestion
+
+
+```json Legacy
+{"documents":["first","second"],"containerTag":"user_1"}
+```
+
+```json V5
+{"documents":[{"content":"first","id":"doc_1"},{"content":"second","id":"doc_2"}]}
+```
+
+
+Post that V5 body to `POST /ns/user_1/document/batch`. The array holds anything
+from **1** to **600** document objects. Namespace, `taskType`, and processing mode
+apply once for the whole request, while content, ID, context, metadata, grouping,
+and date are set per item.
+
+Results come back in submission order. Read `success`, `failed`, and every entry
+of `results` — a single batch may mix accepted and rejected documents, so a `200`
+on its own proves nothing.
+
+### File ingestion
+
+`POST /v3/documents/file` becomes `POST /ns/{namespace}/document/file`. Keep
+sending `multipart/form-data`:
+
+| Part | Encoding |
+| --- | --- |
+| `file` | Binary file |
+| `supportingContext`, `date` | Plain strings |
+| `metadata`, `group` | JSON-encoded strings |
+| `fileType`, `mimeType` | Query parameters, when inference is not enough |
+
+The API acknowledges the file once it is durably accepted. Extraction, indexing,
+and memory formation continue asynchronously — poll the document rather than
+assuming the first response means processing is complete.
+
+### Processing choices
+
+- `taskType=memory` extracts long-term memories; `taskType=superrag` indexes
+ source context without generating memories.
+- `dreaming=dynamic` (the default) groups related documents so memories form
+ from coherent units rather than one isolated entry at a time.
+- `dreaming=instant` handles documents one at a time and adds one billable
+ operation for each of them.
+
+### Verification
+
+- Ingest text, a public URL, and a file, then wait for each document to reach a
+ terminal processing state.
+- Repeat a caller-defined ID and confirm append/diff behavior rather than
+ replacement.
+- Post a batch in which some documents succeed and some fail; check that the
+ result order holds and that per-item errors are reported.
+- Confirm metadata and grouping are still filterable after processing completes.
diff --git a/apps/docs/snippets/api-v5-document-updates.mdx b/apps/docs/snippets/api-v5-document-updates.mdx
new file mode 100644
index 00000000..bfb1818c
--- /dev/null
+++ b/apps/docs/snippets/api-v5-document-updates.mdx
@@ -0,0 +1,95 @@
+V5 makes the difference between *adding* new information and *replacing* the canonical source explicit. Legacy `PATCH /v3/documents/{id}` overloaded both meanings, so pick the write deliberately.
+
+### Choose the correct write
+
+| Intent | Operation | Content behavior |
+| --- | --- | --- |
+| Add information to a stable caller ID | `POST /ns/{namespace}/document` | Append / diff |
+| Replace text or URL content | `PATCH /ns/{namespace}/document/{id}` | Replace and reprocess |
+| Update only supporting fields | Same `PATCH`, without `content` | Canonical content unchanged |
+| Replace a source file and its user metadata | `POST /ns/{namespace}/document/file/{id}` | Full replacement and reprocess |
+| Partially update a file or its supporting fields | `PATCH /ns/{namespace}/document/file/{id}` | Omitted fields unchanged |
+
+### Update text or URL content
+
+
+```bash Legacy
+PATCH /v3/documents/doc_1
+{"content":"corrected source","metadata":{"revision":2}}
+```
+
+```bash V5
+PATCH /ns/user_1/document/doc_1?dreaming=dynamic
+{"content":"corrected source","metadata":{"revision":2}}
+```
+
+
+You may send any non-empty selection from `content`, `supportingContext`,
+`metadata`, `group`, and `date`. Once `content` is present it becomes the new
+canonical source — which means facts that only the previous source supported can
+vanish when the document is reprocessed.
+
+
+ Replacing `content` is not a metadata edit. If you only want to re-label a
+ document, send `metadata` / `group` / `date` and omit `content` entirely.
+
+
+### Replace a file-backed document
+
+```bash
+POST /ns/user_1/document/file/doc_1?dreaming=dynamic
+Content-Type: multipart/form-data
+
+file=@corrected.pdf
+metadata={"revision":2}
+```
+
+`file` is mandatory on `POST`, and the call swaps out the canonical source along
+with the user-controlled metadata, grouping, context, and date. Any supporting
+field you leave out is **cleared**. Reach for this when the request you are
+sending is the whole new state of the file-backed document.
+
+### Partially update a file-backed document
+
+```bash
+PATCH /ns/user_1/document/file/doc_1?dreaming=dynamic
+Content-Type: multipart/form-data
+
+metadata={"reviewed":true}
+```
+
+`PATCH` touches nothing beyond the fields you actually send. Send `file` to swap
+the source while the supporting fields you omit survive, or drop `file` entirely
+for a change confined to metadata, group, context, or date. Note the encoding
+split: `metadata` and `group` travel as JSON-encoded strings, whereas
+`supportingContext` and `date` are plain strings.
+
+
+ V5 exposes no public `PUT /ns/{namespace}/document/file/{id}`. For a full
+ replacement use `POST`; for a partial one use `PATCH`.
+
+
+### IDs and scope
+
+The path `id` may be the Supermemory document ID **or** your caller-defined ID.
+It is resolved only inside `{namespace}`. An ID from another namespace is not a
+cross-namespace update mechanism — the request is rejected or not found rather
+than reaching across the boundary.
+
+### Processing and conflicts
+
+Content or file replacement is accepted before downstream processing finishes. A
+document that is still processing, a namespace conflict, or a conflicting
+internal file path can return `409`. Retry only after the conflicting operation
+reaches a terminal state.
+
+### Verification
+
+- Patch metadata alone and confirm the document content and its derived facts are
+ unchanged.
+- Patch content, then check that the replacement source is canonical once
+ processing finishes.
+- Replace a file with `POST` and confirm omitted user metadata is cleared.
+- Patch a file-backed document and confirm omitted fields survive.
+- Try the same ID against a different namespace; the update should be rejected or
+ come back not found.
diff --git a/apps/docs/snippets/api-v5-filters.mdx b/apps/docs/snippets/api-v5-filters.mdx
new file mode 100644
index 00000000..72dc3d9e
--- /dev/null
+++ b/apps/docs/snippets/api-v5-filters.mdx
@@ -0,0 +1,109 @@
+V5 uses one optional **singular** `filter` field for search, profiles, and list
+operations, replacing the loosely-typed `filters` object.
+
+### Why the legacy shape was hard to migrate
+
+In this repository the legacy filter type is deliberately permissive:
+
+```ts
+export const SearchFiltersSchema = z
+ .object({
+ AND: z.array(z.unknown()).optional(),
+ OR: z.array(z.unknown()).optional(),
+ })
+ .or(z.record(z.unknown()))
+```
+
+`z.array(z.unknown())` means the individual conditions are **unvalidated**. The
+only place their intended shape is written down is an OpenAPI example and a
+`// TODO: Improve filter schema` comment above `ListMemoriesQuerySchema`. Any
+condition that "looked right" was accepted at the edge and interpreted
+downstream. V5 replaces that with a discriminated union the server can reject.
+
+### Shape
+
+```ts
+type Filter =
+ | { field: string; operator: "eq" | "neq"; value: string; caseSensitive?: boolean }
+ | { field: string; operator: "eq" | "neq"; value: number | boolean }
+ | { field: string; operator: "gt" | "gte" | "lt" | "lte"; value: number }
+ | { field: string; operator: "contains" | "notContains"; value: string; caseSensitive?: boolean }
+ | { field: string; operator: "arrayContains" | "arrayNotContains"; value: string }
+ | { operator: "and" | "or"; operands: Filter[] };
+```
+
+A field name may be built from letters, numbers, `_`, `.`, and `-`. An expression
+may nest at most **five levels** deep and carry at most **200 operands** in a
+single logical group.
+
+### Operator mapping
+
+| Legacy condition | V5 predicate |
+| --- | --- |
+| `{ key, value }` | `{ field: key, operator: "eq", value }` |
+| `negate: true` equality | `operator: "neq"` |
+| `filterType: "string_contains"` | `operator: "contains"` |
+| contains + `negate: true` | `operator: "notContains"` |
+| `filterType: "array_contains"` | `operator: "arrayContains"` |
+| array contains + `negate: true` | `operator: "arrayNotContains"` |
+| numeric `=` / numeric `=` + `negate: true` | `eq` / `neq` with a JSON number |
+| numeric `>`, `>=`, `<`, `<=` | `gt`, `gte`, `lt`, `lte` |
+| `AND` / `OR` arrays | lowercase `and` / `or` with `operands` |
+| `ignoreCase: true` | `caseSensitive: false` |
+
+### Before and after
+
+
+```json Legacy
+{
+ "AND": [
+ { "key": "category", "value": "research" },
+ { "key": "score", "value": 0.8, "filterType": "numeric", "numericOperator": ">=" }
+ ]
+}
+```
+
+```json V5
+{
+ "operator": "and",
+ "operands": [
+ { "field": "category", "operator": "eq", "value": "research" },
+ { "field": "score", "operator": "gte", "value": 0.8 }
+ ]
+}
+```
+
+
+### Deterministic conversion
+
+Apply these steps in order. They are mechanical on purpose — a codemod should be
+able to do them without judgement calls.
+
+1. Rename the outer `filters` field to `filter`.
+2. Recursively replace `AND` / `OR` objects with `{ operator, operands }`.
+3. The legacy `key` becomes `field`.
+4. Convert legacy flags (`negate`, `numericOperator`, `filterType`) into one
+ explicit operator.
+5. Keep numeric values as JSON **numbers**, not numeric strings.
+6. Delete the legacy `filterType`, `negate`, `numericOperator`, and `ignoreCase`
+ keys.
+
+
+ Step 5 is the step most likely to be missed. Three schemas in this repository
+ carry the same trap — `ListMemoriesQuerySchema`, `SearchRequestSchema`
+ (`packages/validation/api.ts:414`), and `Searchv4RequestSchema`
+ (`packages/validation/api.ts:508`) each ship an example that passes
+ `"value": "1742745777"`, a numeric comparison written as a **string**. Every
+ `gt`/`gte`/`lt`/`lte` operator in V5's union demands a JSON number, so a
+ straight rename will fail validation.
+
+
+### Verification
+
+- Compare result IDs for equality, inequality, contains, numeric, array, nested
+ `AND`, and nested `OR` fixtures.
+- Add negative tests: legacy shapes, empty `operands`, incompatible value types,
+ and unknown keys must all return `400`.
+- Confirm an omitted `filter` preserves unfiltered behavior.
+- Test one fixture at the nesting limit and one beyond it, to pin the five-level
+ bound.
diff --git a/apps/docs/snippets/api-v5-memory-forgetting.mdx b/apps/docs/snippets/api-v5-memory-forgetting.mdx
new file mode 100644
index 00000000..0e20c365
--- /dev/null
+++ b/apps/docs/snippets/api-v5-memory-forgetting.mdx
@@ -0,0 +1,93 @@
+Forgetting in V5 acts on a single namespace, and an exact-ID request and a
+semantic request come back in the same result envelope.
+
+### Choose exact or semantic forgetting
+
+| Intent | V5 operation |
+| --- | --- |
+| Forget reviewed memory IDs | `DELETE /ns/{namespace}/memories` |
+| Find memories by meaning | `DELETE /ns/{namespace}/memories/semantic` |
+
+The legacy API split these across two differently shaped endpoints:
+`DELETE /v4/memories` (identify by `id` **or** exact `content`, plus a `reason`)
+and `POST /v4/memories/forget-matching` (semantic `query` or explicit `ids`, with
+`dryRun`, `threshold`, and `maxForget`). V5 normalizes both onto one response
+contract.
+
+### Forget exact IDs
+
+```bash
+DELETE /ns/user_1/memories
+Content-Type: application/json
+
+{"ids":["mem_1","mem_2"]}
+```
+
+The count runs from **1** to **500** IDs. Your successes arrive under `matches`;
+anything missing or ineligible lands in `errors`. A 2xx status therefore does not
+prove that every ID you named was actually forgotten.
+
+### Preview a semantic request
+
+```bash
+DELETE /ns/user_1/memories/semantic
+Content-Type: application/json
+
+{"query":"outdated home address","dryRun":true}
+```
+
+With `dryRun: true` the request only selects — memory state is left **untouched**.
+With `dryRun: false` it removes whatever the selection resolves to at the moment
+the request runs.
+
+### Avoid selection drift
+
+```text
+semantic request with dryRun: true
+ |
+ v
+review matches[].id
+ |
+ v
+exact DELETE with reviewed IDs
+```
+
+Use this workflow whenever a human or a policy must approve the exact set.
+Re-running the semantic request with `dryRun: false` can select a **different**
+set if memories changed after the preview.
+
+
+ Do not assume the semantic endpoint is idempotent with respect to your earlier
+ preview. The legacy `forget-matching` route had the same property, but the
+ legacy docs described the two-step preview flow as optional; in V5 it is the
+ recommended path for any deletion a reviewer has to sign off on.
+
+
+### Read the normalized response
+
+```json
+{
+ "count": 1,
+ "matches": [{ "id": "mem_1", "memory": "Old address" }],
+ "errors": [{ "id": "mem_2", "error": "Memory not found" }]
+}
+```
+
+The value of `count` is by definition the length of `matches`. Dry-run and
+applied semantic requests share this shape, so the response body on its own will
+not reveal which mode produced it — record the mode next to your audit entry.
+
+### Removed memory-write routes
+
+V5 offers nothing in place of direct V4 memory creation or version updates. Feed
+source material in through the document routes and rewrite the canonical document
+whenever the underlying facts change.
+
+### Verification
+
+- Try ID sets that are all-success, partially successful, duplicated, unknown,
+ and drawn from across namespaces.
+- A dry run should leave every matched memory recallable.
+- Forget the reviewed IDs exactly, then check that ordinary recall no longer
+ surfaces them.
+- Log the request mode alongside the audit trail for semantic operations.
diff --git a/apps/docs/snippets/api-v5-namespaces.mdx b/apps/docs/snippets/api-v5-namespaces.mdx
new file mode 100644
index 00000000..4df57764
--- /dev/null
+++ b/apps/docs/snippets/api-v5-namespaces.mdx
@@ -0,0 +1,86 @@
+V5 renames the public isolation boundary from *container tag* to *namespace*.
+Existing values remain valid identifiers — no stored data rename is required.
+
+### Endpoint mapping
+
+| Legacy | V5 |
+| --- | --- |
+| `GET /v3/container-tags/list` | `GET /namespaces` |
+| `GET /v3/container-tags/{tag}` | `GET /ns/{namespace}` |
+| `PATCH /v3/container-tags/{tag}` | `PATCH /ns/{namespace}` |
+| `DELETE /v3/container-tags/{tag}` | `DELETE /ns/{namespace}` |
+| Merge container tags | `DELETE /ns/{source}` with `{"moveTo":"target"}` |
+
+`GET /ns` also works and answers the same way as `GET /namespaces`; new code
+should standardize on the longer form.
+
+### List namespaces
+
+Each entry now exposes `id`, `namespace`, `documentCount`, `memoryCount`,
+nullable `description`, and `system.createdAt` / `system.updatedAt`. If a reader
+of yours still looks for `containerTag`, top-level timestamps, or the internal
+settings blobs, it needs to change.
+
+
+ The legacy list response in this repository is a flat array whose entries carry
+ `id`, `name`, `containerTag`, `createdAt`, `updatedAt`, `isExperimental`,
+ `emoji`, `isNova`, and `visibility`. V5 replaces `containerTag` with
+ `namespace`, nests the timestamps under `system`, and drops the
+ project/consumer distinction fields from the public shape.
+
+
+### Read and update settings
+
+
+```bash Legacy
+PATCH /v3/container-tags/project_alpha
+{"entityContext":"Research project for distributed systems"}
+```
+
+```bash V5
+PATCH /ns/project_alpha
+{"supportingContext":"Research project for distributed systems"}
+```
+
+
+The public `GET` and `PATCH` shapes contain only `namespace`, `supportingContext`,
+and lifecycle timestamps. Profile buckets have their own `/profile/buckets`
+resource and are **not** namespace settings.
+
+Passing `supportingContext: null` wipes whatever context is stored. You cannot
+simply leave the field out — `PATCH` demands at least one supported setting.
+
+### Permanently delete a namespace
+
+```bash
+DELETE /ns/project_alpha
+{}
+```
+
+Omit `moveTo` and the delete runs synchronously: you get `200` back along with
+`deletedDocumentsCount` and `deletedMemoriesCount`. Namespace and content are
+both gone.
+
+### Move then remove a namespace
+
+```bash
+DELETE /ns/project_alpha
+{"moveTo":"project_archive"}
+```
+
+You get `202` back with `status: "queued"` and an `operationId`. Source and
+destination must not be the same namespace. A `202` here means the move was
+queued, not that it finished.
+
+Where legacy merge took several sources at once, V5 takes **one source per
+request**. Chain the moves and log each one separately.
+
+### Verification
+
+- Check that existing container-tag values still resolve when used as namespace
+ paths.
+- Compare namespace counts against namespace-scoped document and memory lists.
+- A permanent delete should return final counts; a move should return `202`.
+- Confirm a restricted caller can neither read nor change namespaces beyond its
+ own scope.
+- Confirm only organization-authorized callers can change settings or lifecycle.
diff --git a/apps/docs/snippets/api-v5-organization.mdx b/apps/docs/snippets/api-v5-organization.mdx
new file mode 100644
index 00000000..5038dd85
--- /dev/null
+++ b/apps/docs/snippets/api-v5-organization.mdx
@@ -0,0 +1,76 @@
+What survives into V5 is the single organization-wide context string that steers
+memory formation. The administrative knobs and the profile-bucket mutation
+endpoints are gone from this resource.
+
+### Endpoint mapping
+
+| Legacy | V5 |
+| --- | --- |
+| `GET /v3/settings` | `GET /organization` |
+| `PATCH /v3/settings` | `PATCH /organization` |
+| `filterPrompt` | `organizationalContext` |
+
+### Read organization settings
+
+```bash
+GET /organization
+```
+
+```json
+{
+ "organizationalContext": "Acme builds security tools for enterprises",
+ "namespaceCount": 42
+}
+```
+
+Any reader you have for a legacy setting that is absent from this allowlisted
+payload should be deleted. `namespaceCount` is read-only information; `PATCH`
+will not move it.
+
+### Update organization context
+
+
+```bash Legacy
+PATCH /v3/settings
+{"filterPrompt":"Acme builds security tools for enterprises"}
+```
+
+```bash V5
+PATCH /organization
+{"organizationalContext":"Acme builds security tools for enterprises"}
+```
+
+
+The write is exhaustive — whatever you supply becomes the context, fully
+**replacing** what was there. Passing `null` clears it; an empty string is
+refused.
+
+An organization administrator is required for these writes. A `403` means the
+caller is not one — it is not an invitation to fall back to namespace context
+quietly.
+
+### Removed public operations
+
+- There is no organization-settings route for profile-bucket mutation.
+- Bucket suggestion (`POST /v3/settings/suggest-buckets`) is not part of V5.
+- Organization data reset (`POST /v3/settings/reset`) is not part of V5.
+- Buckets belonging to a namespace live under
+ `/ns/{namespace}/profile/buckets`.
+
+
+ The legacy `OrganizationSettingsSchema` in this repository carries connection
+ credentials (`googleDriveClientId`, `notionClientId`, `onedriveClientId`, and
+ their secrets and enablement flags) alongside the filtering fields. Those are
+ **not** part of the public V5 `/organization` response. If your code read them
+ from `GET /v3/settings`, move that configuration to your own secret store or to
+ the connector setup flow.
+
+
+### Verification
+
+- Diff the V5 context against the legacy `filterPrompt` before you cut over.
+- Write the context, rewrite it, then clear it.
+- Check that `namespaceCount` matches what `GET /namespaces` reports for the same
+ credentials.
+- Verify non-admin callers receive `403` on `PATCH`.
+- Check that nothing downstream still depends on the fields V5 dropped.
diff --git a/apps/docs/snippets/api-v5-overview.mdx b/apps/docs/snippets/api-v5-overview.mdx
new file mode 100644
index 00000000..5e8d3b71
--- /dev/null
+++ b/apps/docs/snippets/api-v5-overview.mdx
@@ -0,0 +1,90 @@
+V5 keeps the same ingestion and recall workflows as V3/V4, but makes scope explicit in the URL, consolidates routes that used to overlap, and tightens request and response types.
+
+
+ This is a **breaking API migration**. Changing only the URL is not enough: fields moved between path, query, and body, search defaults changed, response envelopes changed, and a few legacy operations have no V5 replacement at all.
+
+
+The API base URL and your bearer keys do not change. V5 application routes are **unversioned** — `/v5/reference` identifies the documentation version, not an API path prefix. Do not put `/v5` in application request paths.
+
+## Recommended migration process
+
+
+
+ Grep your integration for `/v3/`, `/v4/`, `containerTag`, `containerTags`,
+ `customId`, `entityContext`, `filterByMetadata`, `filters`, and any legacy
+ SDK method wrappers. Record each one with its caller and its purpose.
+
+
+ Move the legacy `containerTag` value into the `/ns/{namespace}` path
+ segment. Never infer scope from a document or memory ID, and never send
+ more than one namespace in a single V5 request.
+
+
+ Apply [document writes](./api-v5-document-writes),
+ [document updates](./api-v5-document-updates),
+ [content management](./api-v5-document-reads),
+ [recall/search](./api-v5-recall),
+ [profiles](./api-v5-profiles),
+ [forgetting](./api-v5-memory-forgetting),
+ [namespaces](./api-v5-settings),
+ [organization](./api-v5-organization), and
+ [typed filters](./api-v5-filters) independently.
+
+
+ Migrate envelopes, attachments, pagination, profile buckets, `system`
+ lifecycle fields, and partial-error handling. A translated request with an
+ untranslated reader still fails.
+
+
+ Work through the [verification and rollout guide](./api-v5-rollout). Check
+ identity and behavior — never raw JSON ordering — and pass changed defaults
+ explicitly so a default change cannot masquerade as a bug.
+
+
+ Switch traffic, monitor failures and semantic drift, then delete that
+ domain's legacy compatibility code only after it passes verification.
+
+
+
+## Endpoint map
+
+| Legacy | V5 |
+| --- | --- |
+| `POST /v3/documents` | `POST /ns/{namespace}/document` |
+| `POST /v3/documents/batch` | `POST /ns/{namespace}/document/batch` |
+| `POST /v3/documents/file` | `POST /ns/{namespace}/document/file` |
+| `GET/PATCH /v3/documents/{id}` | `GET/PATCH /ns/{namespace}/document/{id}` |
+| `DELETE /v3/documents/{id}`, `DELETE /v3/documents/bulk` | `DELETE /ns/{namespace}/document` |
+| `POST /v3/documents/list`, `POST /v4/memories/list` | `POST /ns/{namespace}/list/{type}` |
+| `POST /v3/search`, `POST /v4/search` | `POST /ns/{namespace}/search` |
+| `POST /v4/profile` | `POST /ns/{namespace}/profile` |
+| `POST /v4/profile/buckets` | `GET/PUT/DELETE /ns/{namespace}/profile/buckets` |
+| `DELETE /v4/memories`, `POST /v4/memories/forget-matching` | `DELETE /ns/{namespace}/memories`, `DELETE /ns/{namespace}/memories/semantic` |
+| `GET/PATCH/DELETE /v3/container-tags/{tag}`, `POST /v3/container-tags/merge` | `GET /namespaces`, `GET/PATCH/DELETE /ns/{namespace}` |
+| `GET/PATCH /v3/settings` | `GET/PATCH /organization` |
+
+## What has no V5 replacement
+
+These legacy operations were deliberately dropped. Do not invent a substitute —
+rework the caller instead.
+
+- **Direct memory creation and version updates.** `POST /v4/memories` and
+ `PATCH /v4/memories` have no V5 equivalent. Ingest or replace a source
+ document and let memories form from it.
+- **Organization bucket suggestion.** `POST /v3/settings/suggest-buckets` is not
+ part of the V5 public surface.
+- **Organization data reset.** `POST /v3/settings/reset` is not part of the V5
+ public surface.
+- **Connector routes.** Connections still use their existing `/v3/connections/*`
+ paths and are **unchanged** by this migration. Do not rewrite them.
+
+## Grounding note
+
+This guide tracks the contract described by the V5 OpenAPI document that
+accompanies the V5 deployment. (For the legacy surfaces, the live
+`/v4/openapi` and `/v3/openapi` documents remain the references.) The legacy
+field names and defaults contrasted here are the ones still present in this
+repository's shared validation package — notably `SearchRequestSchema`,
+`Searchv4RequestSchema`, `ListMemoriesQuerySchema`, `MemoryUpdateSchema`, and the
+`SearchFiltersSchema` union. Where a behavior is not described by either source,
+this guide says so rather than guessing.
diff --git a/apps/docs/snippets/api-v5-profiles.mdx b/apps/docs/snippets/api-v5-profiles.mdx
new file mode 100644
index 00000000..a4660b5b
--- /dev/null
+++ b/apps/docs/snippets/api-v5-profiles.mdx
@@ -0,0 +1,95 @@
+V5 returns a maintained profile directly, and gives profile bucket definitions
+their own namespace-scoped resource.
+
+### Remove search behavior from profile calls
+
+
+```bash Legacy
+POST /v4/profile
+{"containerTag":"user_1","q":"work preferences","threshold":0.6,"include":{}}
+```
+
+```bash V5
+POST /ns/user_1/profile
+{"filter":{"field":"region","operator":"eq","value":"us-west"},"buckets":["work"]}
+```
+
+
+Remove the legacy `q`, `threshold`, and `include`. If the caller needs
+query-ranked results, issue a **separate** V5 search request. Move `containerTag`
+to the path and rename `filters` to the singular `filter`.
+
+
+ The legacy shape is visible in this repository: `POST /v4/profile` is called
+ with a body of `{ q, containerTag, include: ["static","dynamic"] }` and the
+ response is read as `profile.static`, `profile.dynamic`, and
+ `profile.buckets`. V5 keeps the `profile.static` / `profile.dynamic` /
+ `profile.buckets` reader intact — what changes is that query parameters no
+ longer belong on the profile call.
+
+
+### Read the V5 profile shape
+
+```json
+{
+ "profile": {
+ "static": ["The user works in design"],
+ "dynamic": ["The user is preparing a launch"],
+ "buckets": { "work": ["Prefers concise project updates"] }
+ }
+}
+```
+
+There is no way to switch off `static` or `dynamic`; both come back on every
+call. Leave `buckets` out of the request and you receive every effective custom
+bucket — or name up to 50 of them to restrict just that section.
+
+### Read bucket definitions
+
+
+```bash Legacy
+POST /v4/profile/buckets
+{"containerTag":"user_1"}
+```
+
+```bash V5
+GET /ns/user_1/profile/buckets
+```
+
+
+The response changes from key/description objects to a plain map:
+
+```json
+{"buckets":{"work":"Professional preferences and ongoing work"}}
+```
+
+### Add or edit namespace buckets
+
+```bash
+PUT /ns/user_1/profile/buckets
+{"buckets":{"work":"Professional preferences and ongoing work"}}
+```
+
+Supply anywhere between one and 50 name-to-description entries. Names already in
+the namespace get rewritten, new ones get created, and any namespace bucket you
+leave out stays exactly as it was.
+
+### Delete namespace buckets
+
+```bash
+DELETE /ns/user_1/profile/buckets
+{"buckets":["work"]}
+```
+
+Every name has to be distinct. Buckets owned by the organization may show up in
+the effective `GET` response, but a namespace-level `PUT` or `DELETE` will not
+alter or drop them.
+
+### Verification
+
+- Check that `static`, `dynamic`, and `buckets` are present in every profile
+ response.
+- Compare an omitted `buckets` request against one-name and multi-name narrowing.
+- Add, edit, and delete a namespace bucket without disturbing omitted buckets.
+- Attempt to mutate an inherited organization bucket and expect the documented
+ error rather than a silent no-op.
diff --git a/apps/docs/snippets/api-v5-rollout.mdx b/apps/docs/snippets/api-v5-rollout.mdx
new file mode 100644
index 00000000..f5ace46b
--- /dev/null
+++ b/apps/docs/snippets/api-v5-rollout.mdx
@@ -0,0 +1,77 @@
+Treat migration as a **behavioral comparison**, not a raw response-snapshot
+update. A diff of two JSON blobs will be dominated by generated IDs and
+timestamps and will tell you nothing about whether recall still works.
+
+### Build deterministic fixtures
+
+Use an isolated namespace, give every fixture a stable ID, and hold the source
+content fixed. The fixture set should span plaintext, URL, file, and batch
+ingestion, plus metadata, grouping, profile facts, related memories, forgotten
+memories, and the case where nothing matches.
+
+For each case, write down the legacy request, the V5 request, the semantic
+outcome you expect, and any delta you are knowingly accepting. Whatever you do,
+keep generated IDs, signed URLs, timings, and JSON key ordering out of the
+comparison unless the contract actually promises them.
+
+### Compare writes
+
+- Repeated `POST` appends or diffs; `PATCH` replaces canonical content.
+- A batch keeps its results in the order you submitted them, and reports
+ per-item failures rather than hiding them.
+- Touching only metadata leaves the source content as it was.
+- An accepted write is not a finished one: poll until processing settles into a
+ terminal state.
+
+### Compare reads and recall
+
+- Attachments you did not request are missing from the payload; attachments you
+ did request but that matched nothing come back as empty arrays.
+- A list call fills in the array for the resource type you asked for and leaves
+ the others untouched.
+- Before trying V5's own search defaults, establish parity with the V4 mode and
+ threshold stated explicitly.
+- Profiles always contain `static`, `dynamic`, and `buckets`.
+
+### Exercise boundaries
+
+| Boundary | Cases |
+| --- | --- |
+| Namespace | Correct, missing, unauthorized, cross-namespace ID |
+| Pagination | First, middle, final, empty, maximum limit |
+| Filters | Every operator, nested AND/OR, invalid type, excessive depth |
+| Deletion | All success, partial success, unknown IDs, semantic dry run |
+| Settings | Admin, non-admin, null removal, invalid empty value |
+
+### Classify differences
+
+```text
+same request intent
+ |
+ +-- same semantic result ------> parity
+ +-- documented V5 difference --> update assertion
+ +-- undocumented difference ----> block cutover
+```
+
+An undocumented difference is a finding, not noise to be smoothed over. Save the
+request pair, the namespace, the IDs, the response status, and the smallest slice
+of the response that reproduces it.
+
+### Cut over by domain
+
+1. Build V5 requests behind a switch you can flip per domain.
+2. Where side effects permit, read both paths or shadow-call the new one.
+3. Cut over ingestion, content management, search, profiles, and settings one at
+ a time, in that order.
+4. Watch for validation and authorization failures, latency shifts, a changing
+ empty-result rate, and processing errors.
+5. Keep the legacy path in place until the observation window closes.
+
+### Completion checklist
+
+- No application call ends up pointing at a `/v5` prefix by mistake.
+- Connector routes, which this migration leaves alone, still carry their
+ documented `/v3` paths.
+- Nothing still reads a legacy field name or legacy response shape.
+- Every default that changed is either deliberately accepted or sent explicitly.
+- Rolling back returns the previous caller to service without a data repair.
diff --git a/apps/docs/snippets/api-v5-search.mdx b/apps/docs/snippets/api-v5-search.mdx
new file mode 100644
index 00000000..610078e2
--- /dev/null
+++ b/apps/docs/snippets/api-v5-search.mdx
@@ -0,0 +1,98 @@
+A V5 search runs against a single namespace, falls back to hybrid recall when you
+say nothing, and takes its ranking controls from a typed request body.
+
+### Request mapping
+
+| Legacy | V5 |
+| --- | --- |
+| `containerTag` | `/ns/{namespace}` |
+| body `q` | body `query` |
+| body `limit` | query `limit` |
+| `searchMode: "documents"` | `searchMode=chunks` |
+| omitted search mode | `searchMode=hybrid` |
+| `filters` | singular `filter` |
+| `include.documents` or `include.summaries` | `attach.documents` |
+| `include.relatedMemories` | `attach.related` |
+| `include.forgottenMemories` | `attach.forgotten` |
+| `rerank: true` / `aggregate: true` | `rerank: "order"` / `"aggregate"` |
+
+
+```bash Legacy
+POST /v4/search
+{"q":"What did the user decide?","containerTag":"user_1","limit":10,"searchMode":"memories"}
+```
+
+```bash V5
+POST /ns/user_1/search?limit=10&searchMode=memories
+{"query":"What did the user decide?","threshold":0.6,"rewriteQuery":false}
+```
+
+
+### Changed defaults
+
+| Setting | V4 | V5 |
+| --- | --- | --- |
+| Search mode | `memories` | `hybrid` |
+| Similarity threshold | `0.6` | `0.3` |
+| Reranking | Disabled | `rerank: "none"` |
+| Query rewriting | Disabled | `false` |
+
+These are the values actually declared in `Searchv4RequestSchema` in this
+repository: `threshold` defaults to `0.6`, `limit` to `10`, and `rerank` and
+`rewriteQuery` both default to `false`. V5 widens recall by default, so set mode
+and threshold **explicitly** while you compare versions. Once parity testing
+passes, drop them only if you actually want the broader V5 hybrid defaults.
+
+
+ A silently widened threshold is the single most common false "regression"
+ during this migration. It returns *more* results, not wrong ones — pin
+ `threshold` and `searchMode` until your comparison is green.
+
+
+### Search modes
+
+| Mode | Returns |
+| --- | --- |
+| `memories` | Formed memories only |
+| `chunks` | Source chunks only |
+| `hybrid` | Both result types in one ranked list |
+
+Nothing in V5 corresponds to the legacy `include.chunks` flag. Pick `chunks` or
+`hybrid` instead.
+
+### Attachments and ranking
+
+- Asking for `attach.documents` pulls the single most relevant source document
+ into each result.
+- `attach.related` brings in parent, child, and sibling memories. The legacy
+ `Searchv4RequestSchema` exposed this as `include.relatedMemories`, and memory
+ results still carry a `context` object with `parents` and `children` arrays
+ whose entries use the `updates` / `extends` / `derives` relation enum.
+- `attach.forgotten` lets forgotten memories appear in related context; it does
+ not promote them to primary results.
+- The `rerank` setting takes `none`, `order`, or `aggregate`, while
+ `rewriteQuery` governs query rewriting aimed at retrieval.
+
+### Response mapping
+
+| Legacy reader | V5 reader |
+| --- | --- |
+| Result array | `results` |
+| Timing | `searchTime` |
+| Source expansion | `result.included.document` |
+| Related context | `result.included.related.{parents,children,siblings}` |
+| Lifecycle fields | `result.system` |
+
+Each primary result contains either `memory`, `chunk`, or both only where the
+contract allows it. Branch on field presence rather than assuming one result
+shape — in hybrid mode a single result array mixes both kinds.
+
+### Verification
+
+- Pin the V4 defaults explicitly and check result IDs against them first; treat
+ V5 hybrid behavior as a separate test.
+- Cover all three modes, thresholds at `0` and `1`, each rerank option, and query
+ rewriting on and off.
+- Try each attachment on its own and in combination, including the empty case.
+- Verify filters, namespace isolation, result limits, and rejection of body
+ parameters placed in the query string (or vice versa).