Reflections
The three-tier reflection hierarchy - consolidation checkout/commit, reflection debt, lease semantics, lineage inspect, date search, and delete by ID.
Reflections
Every MemoryRouter vault now has a three-tier memory structure. Raw memories capture exact moments. Reflections consolidate them into what happened. High-level reflections distill what it all means. Nothing is ever lost: tiers only layer on top, and every claim can be drilled back down to its sources.
| Tier | Name | Captures | Example |
|---|---|---|---|
| 1 | Raw | Every detail, verbatim | Messages as said, observations, sessions |
| 2 | Reflections | The WHAT | "We built auth with JWT and chose Postgres" |
| 3 | High-level reflections | The WHY | "This project prioritizes correctness over speed" |
All three tiers live in the same vault under the same Memory Key. Retrieval blends them automatically: searches return high-level insight, mid-level reflections, and exact moments together, each result tagged with its tier.
The consolidation API below is live in production today at api.memoryrouter.ai.
How consolidation works
MemoryRouter never consolidates on its own, and it never calls a model. Consolidation is a checkout/commit transaction driven by your side:
- Checkout a batch of unconsolidated memories. The server selects the oldest ones chronologically, puts them under a 15 minute lease, and returns their texts plus instructions for the consolidating model.
- Your model writes the reflections. Whatever model your app, agent, or CLI already uses reads the texts and produces 5 to 10 entries, rating each entry's importance from 1 to 10.
- Commit the entries with the batch id. The server embeds them, stores them at the tier above, links lineage to every source in the batch, and marks the sources consolidated.
The same two calls handle ongoing consolidation and full historical backfill. Tier 2 to tier 3 consolidation is the identical loop with tier: 2.
The model never sees memory IDs. Checkout returns bare texts and instructions; the opaque batch_id is the lease ticket, and the server builds all lineage from its own records at commit time. Your model is a pure text-in, text-out function in the middle.
POST /v1/memory/consolidate/checkout
Lease the oldest unconsolidated memories. Tier 1 batches accumulate up to a token budget (default 50,000 tokens, roughly 150 importance worth of raw material). Tier 2 batches are importance-driven: the server selects the oldest reflections until their summed importance reaches 150, always completing the final row, with a 10,000 token soft cap as a sanity bound.
curl -X POST https://api.memoryrouter.ai/v1/memory/consolidate/checkout \
-H "Authorization: Bearer mk_user-123" \
-H "Content-Type: application/json" \
-d '{ "tier": 1, "max_tokens": 50000 }'| Field | Type | Required | Description |
|---|---|---|---|
tier | integer | Yes | 1 consolidates raw into reflections; 2 consolidates reflections into high-level reflections. |
max_tokens | number | No | Explicit hard token cap, clamped to 1,000 - 200,000. When omitted, tier 1 defaults to 50000 and tier 2 uses importance-driven selection (150 summed importance, 10,000 token soft cap). Smaller batches give tighter lineage. |
session_id | string | No | Recorded on usage events. |
embeddings | string | No | Embedding model override. Also accepted as X-Embedding-Model. |
Response:
{
"batch_id": "cb_9f2c...",
"tier": 1,
"count": 148,
"est_tokens": 49730,
"covers": { "from": "2026-02-01T04:11:09.000Z", "to": "2026-02-09T18:40:22.000Z" },
"lease_expires_at": "2026-08-16T21:25:00.000Z",
"memories": [ { "text": "...", "date": "2026-02-01T04:11:09.000Z" } ],
"source_ids": ["7c9e6679-7425-40de-944b-e07fc1f90ae7", "..."],
"reflection_contract": {
"version": 2,
"target_tier": 2,
"instructions": "You are consolidating raw memories into main-level reflections. ...",
"entry_schema": {
"text": "string, 1 to 8000 chars, clean standalone prose, no IDs or scores inline",
"importance": "integer 1 to 10"
},
"min_entries": 1,
"max_entries": 100,
"recommended_entries": "5-10"
},
"tier_definitions": {
"target_tier": 2,
"instruction": "Tier 2 (main memories) capture the WHAT..."
}
}memoriesis what your model consumes: texts plus dates, nothing else.reflection_contractis the versioned, server-authored consolidation contract. Passinstructionsto your model verbatim; do not write your own reflection prompt.entry_schemadescribes exactly what each committed entry must look like.versionbumps only when instruction wording or entry meaning changes, so integrations can pin behavior against it.tier_definitionsis the legacy pre-contract field. It keeps emitting unchanged for older clients; new integrations should readreflection_contract.source_idsexists for client-side transparency and audit only. Never hand it to the model.- When nothing is unconsolidated, checkout returns
200with{ "batch_id": null, "count": 0, "message": "nothing unconsolidated" }. That is the loop's termination signal, not an error. - Read-only keys receive
403: checkout leads to writes.
POST /v1/memory/consolidate/commit
Complete the transaction: submit the entries your model wrote.
curl -X POST https://api.memoryrouter.ai/v1/memory/consolidate/commit \
-H "Authorization: Bearer mk_user-123" \
-H "Content-Type: application/json" \
-d '{
"batch_id": "cb_9f2c...",
"entries": [
{ "text": "John locked the reflection hierarchy design and shipped the consolidation API.", "importance": 8 },
{ "text": "The InSyte connector v6 shipped and was verified live.", "importance": 6 }
]
}'| Field | Type | Required | Description |
|---|---|---|---|
batch_id | string | Yes | The lease ticket from checkout. |
entries | array | Yes | 1 to 100 items. Each entry: text (clean standalone prose, up to 8,000 chars) and importance (integer 1 to 10). Never put IDs or importance numbers inside the text itself. |
Response:
{
"batch_id": "cb_9f2c...",
"committed": true,
"already_committed": false,
"tier": 2,
"created": [
{ "id": "9b2f1c34-6a7e-4d21-8f0a-3b5c9d7e1a22", "importance": 8 }
],
"sources_consolidated": 148,
"reflection": { "unconsolidated": { "...": "..." }, "threshold_tokens": 50000, "threshold_importance": 150, "suggestion": null }
}Created id values are memory UUIDs in the same public namespace as every other memory: searchable, inspectable, deletable.
Leases, retries, and idempotency
Checkout takes a lease, not a lock:
- 15 minute expiry. If commit never arrives (model died, session crashed), the lease expires and the memories return to the unconsolidated pool. No stuck state, nothing to clean up.
- Idempotent replay. Committing an already-committed batch returns
200withalready_committed: trueand the originally created reflection ids. Nothing is written twice, and replays are not billed. - Expired lease. Committing after expiry returns
409. Re-checkout and try again; the server prevents double-consolidation by construction. - Concurrency safe. Checked-out memories are excluded from new checkouts, so parallel consolidation loops (for example a backfill worker plus a live session) can never collide.
A historical backfill is just this loop run repeatedly until checkout returns empty. The loop is resumable from any crash with zero client-side state.
Reflection debt
The server tracks how much unconsolidated material each vault carries and reports it on responses you are already receiving. There is nothing to poll.
The debt block rides on /v1/memory/search, /v1/memory/prepare, /v1/memory/ingest, commit, and date-search responses as a top-level reflection field:
{
"reflection": {
"unconsolidated": {
"raw": { "units": 40000, "est_tokens": 13200000, "importance_equiv": 39600 },
"tier2": { "count": 120, "importance": 640, "est_tokens": 41000 }
},
"threshold_tokens": 50000,
"threshold_importance": 150,
"suggestion": "raw consolidation suggested: 39600 unconsolidated importance"
}
}Debt is measured in importance. suggestion becomes non-null when unconsolidated importance reaches 150 at either tier boundary ("raw consolidation suggested: N unconsolidated importance", "tier 2 consolidation suggested: N unconsolidated importance", or both). Tier 2 rows carry real importance ratings (1 to 10), which the server sums. Raw memories have no ratings, so their debt is expressed as an importance equivalent derived from tokens: 150 importance is about 50,000 raw tokens (importance_equiv = est_tokens * 150 / 50000). Token aggregates stay in the block for information. The threshold logic is entirely server-side; integrations just render the hint and consolidate when they choose: at the suggestion, at a session boundary, or on demand.
Proxy chat responses (OpenAI, Anthropic, Google formats) cannot carry extra JSON fields, so they expose the same data in a compact X-Memory-Reflection-Debt response header:
{"raw_units":40000,"raw_tokens":13200000,"raw_importance_equiv":39600,"t2_count":120,"t2_importance":640,"t2_tokens":41000,"threshold":50000,"threshold_importance":150,"suggestion":"raw consolidation suggested: 39600 unconsolidated importance"}POST /v1/memory/inspect
Every reflection has receipts. Inspect takes any memory id and returns the tier below it: the actual memories that reflection was built from.
curl -X POST https://api.memoryrouter.ai/v1/memory/inspect \
-H "Authorization: Bearer mk_user-123" \
-H "Content-Type: application/json" \
-d '{ "memory_id": "9b2f1c34-6a7e-4d21-8f0a-3b5c9d7e1a22" }'Response:
{
"memory_id": "9b2f1c34-6a7e-4d21-8f0a-3b5c9d7e1a22",
"tier": 2,
"batch_id": "cb_9f2c...",
"covers": { "from": "2026-02-01T04:11:09.000Z", "to": "2026-02-09T18:40:22.000Z" },
"sources": [
{ "id": "7c9e6679-...", "tier": 1, "content": "...", "role": "user", "importance": null, "date": "..." }
],
"sources_total": 148,
"sources_deleted": 0,
"truncated": false
}- Chainable all the way down: inspect a tier 3 reflection to find its tier 2 sources, inspect one of those to see the exact raw details. An audit log for AI memory.
- Inspecting a raw memory returns upward navigation instead:
consolidated_intolists the reflections built from it. - Orphan-tolerant: deleted sources are counted in
sources_deleted, never errored. - Read-only keys are allowed (pure read). Returns up to 500 sources per call with
truncated: truebeyond that.
POST /v1/memory/date-search
Relevance search and time search are different query types. The existing /v1/memory/search stays relevance-first; date-search is time-first.
curl -X POST https://api.memoryrouter.ai/v1/memory/date-search \
-H "Authorization: Bearer mk_user-123" \
-H "Content-Type: application/json" \
-d '{
"from": "2026-08-08",
"to": "2026-08-15",
"query": "deploy",
"tiers": [1, 2, 3],
"max_tokens": 50000,
"order": "newest_first"
}'| Field | Type | Required | Description |
|---|---|---|---|
from | string | Yes | ISO date or datetime. Date-only means start of day UTC. |
to | string | No | Defaults to now. Date-only means end of day UTC. |
query | string | No | Without it: a pure chronological dump of the window. With it: relevance ranking within the window. |
tiers | array | No | Subset of [1, 2, 3]. Default all. |
max_tokens | number | No | Default 50,000; clamped to 1,000 - 200,000. |
order | string | No | newest_first (default) or oldest_first. |
Date-search also accepts importance (integer 1 to 10, minimum threshold), described in the next section.
The response includes the actually returned window (returned: { from, to }) and truncated. Truncation is pagination: continue with to: returned.from (newest first) or from: returned.to (oldest first). No cursors.
Reflections match on their coverage window (the period they summarize), so asking "what happened in February" surfaces February's reflections even though they were written later.
Filtering retrieval by tier and importance
Once a vault has reflections, you can slice retrieval by tier and by how important the reflections are. Two optional parameters are live on POST /v1/memory/search and POST /v1/memory/prepare, and importance is also live on POST /v1/memory/date-search:
| Param | Type | Meaning |
|---|---|---|
tiers | array | Non-empty subset of [1, 2, 3]. Only return memories from these tiers. Omit for the default blend across all tiers. |
importance | integer | Minimum importance threshold, 1 to 10. Only reflections carry importance ratings, so setting this filters to tier 2 and 3 content. Raw memories are excluded whenever it is set. |
Tier guide: tier 3 holds the highest-level consolidated reflections (identity, principles, what matters most); tier 2 holds reflections of specific meaningful events; tier 1 holds raw verbatim memories. Match filters to the question rather than applying one rigid recipe:
- Broad "most important things we've done" questions: tiers
[3], importance 8 or higher, a large limit such as 250. Time-bounded variants ("most important things last month") use date search with the same filters. - Big-picture overviews ("what do you know about this area or period"): tiers
[3]alone, with no importance filter. Importance filtering is optional and can exclude useful context. - Specific-topic lookups ("find the decision about X"): omit tiers entirely so raw, tier 2, and tier 3 are searched together, and leave importance unset. A high importance filter hides the specific detail being sought.
- Specific meaningful events where raw noise is unwanted: tiers
[2](optionally[2, 3]). - Exact verbatim or forensic detail: if the detail is not already in context, drill down by
lineage: search tiers
[3]to find the relevant thread, then useinspect_memoryon the result to see the tier 2 reflections it consolidated, then inspect those to reach the raw memories underneath. Direct tiers[1]search works when you know exact wording to match.
Mixtures of tiers are legitimate; adjust the importance threshold to the question instead of always setting it high. Only reflections (tiers 2 and 3) carry importance ratings.
Ask for only the big picture:
curl -X POST https://api.memoryrouter.ai/v1/memory/search \
-H "Authorization: Bearer mk_user-123" \
-H "Content-Type: application/json" \
-d '{ "query": "project direction", "limit": 10, "tiers": [3] }'Ask for anything rated 7 or higher, whatever tier it lives in:
curl -X POST https://api.memoryrouter.ai/v1/memory/search \
-H "Authorization: Bearer mk_user-123" \
-H "Content-Type: application/json" \
-d '{ "query": "decisions about the auth system", "limit": 10, "importance": 7 }'Behavior notes:
- Omitting both params leaves the response byte-identical to before these params existed. They are purely additive.
- Search and date-search results tag every row with
tier, and includeimportanceon reflection rows, so you can see what you got. - Invalid values return
400:tiersmust be a non-empty subset of[1, 2, 3],importancemust be an integer 1 to 10. - On
/v1/memory/prepare, the filters shape the injected context block the same way:tiers: [2, 3]builds a context window of pure insight with no raw noise.
POST /v1/memory/delete-ids
Delete is by memory id, universal across all tiers. Search, inspect, and date-search return ids; delete takes them.
curl -X POST https://api.memoryrouter.ai/v1/memory/delete-ids \
-H "Authorization: Bearer mk_user-123" \
-H "Content-Type: application/json" \
-d '{ "ids": ["9b2f1c34-6a7e-4d21-8f0a-3b5c9d7e1a22"] }'Response:
{
"requested": 1,
"deleted": 1,
"do_deleted": 1,
"not_found": 0,
"legacy_fallback_used": 0,
"failed": []
}- Up to 500 ids per request. Deletion is permanent and removes the memory from both storage layers.
- Deleting a raw memory does not delete reflections built from it: reflections are independent rows, and inspect reports the gap honestly via
sources_deleted. - Deleting a reflection is just deleting its id. Identical mechanics at every tier.
- Ids listed in
failedwere kept intact and are safe to retry. - Whole-vault deletion remains
DELETE /v1/memory.
Billing
Consolidation endpoints meter at the same rate as standard retrieval (1x the raw-memory rate):
| Endpoint | Metered as |
|---|---|
| checkout | Retrieval of the checked-out batch tokens |
| commit | Storage of the committed entry tokens |
| inspect | Retrieval of the returned source tokens |
| date-search | Retrieval of the returned result tokens |
| delete-ids | The deleted rows' tokens |
Idempotent commit replays write nothing and are not billed. An empty checkout bills nothing. See the cost model for how retrieval metering works overall.
How integrations must behave
Two invariants apply to every connector, plugin, and SDK that wraps this API:
- The server is the single source of reflection prompts. Integrations pass the checkout
reflection_contract.instructionsto the model verbatim and never author their own consolidation guidance. This keeps reflection quality consistent and centrally improvable across every surface. - No memory ids ever reach the model. The model sees texts, dates, and instructions. Commit entries are text plus importance only; coverage is always the whole batch, and the server owns all id association.
Reflections over MCP
The MCP server at mcp.memoryrouter.ai wraps this API with four live tools, so ChatGPT, Claude, and Claude Cowork can consolidate and inspect memory without any key handling:
| Tool | Scope | Wraps |
|---|---|---|
consolidate_memories | memories:reflect | checkout |
commit_reflections | memories:reflect | commit |
inspect_memory | memories:read | inspect |
delete_memories | memories:delete | delete-ids |
The consolidation round trip inside a chat is exactly the API loop: the model calls consolidate_memories, receives texts plus the reflection contract, writes the entries itself, and calls commit_reflections with the batch id. search_memories on the same server accepts the tiers and importance filters described above. See the MCP server page for details.
Automatic consolidation in the connectors
The connector packages (Claude Code, Codex, OpenClaw plugin) do not wait for anyone to notice the debt: when a retrieval response reports debt above the server's threshold, they consolidate automatically in the background, using the host's own model for the reflecting (your inference, your account). Shared behavior across all three:
- The trigger is always the server's own threshold decision carried on the response; connectors never hardcode a threshold.
- At most 3 batches run per trigger, with a fresh debt check between batches; the run stops as soon as debt drops below threshold.
- A machine-wide lockfile (
~/.memoryrouter/consolidate.lock, stale after 20 minutes) prevents concurrent sessions or multiple connectors from double-firing. Overlap would be harmless anyway (leases plus idempotent commits), but the lock avoids wasted inference. - A 30 minute cooldown follows every completed run.
- Failures are invisible to your session and logged to
~/.memoryrouter/consolidate.log. - Opt out per connector with the
autoConsolidate: falseconfig flag (each integration page shows where); the in-context suggestion and manual reflect commands keep working.
Billing is unchanged: checkout and commit tokens at the 1x raw rate, exactly as in the table above. MemoryRouter never calls a model provider on your behalf.