The gap is already documented, in three places
compact reduces what the model sees. The docs are explicit that it stops there:
docs/COMPACT_DISCOVERY.md — "These options reduce model-visible output, not database search or traversal work."
src/basic_memory/man/man3/build-context(3).md — "This reduces MCP output, not API traversal work, and is not a fixed token limit."
src/basic_memory/mcp/tools/build_context.py — "This reduces response size, not traversal work or a guaranteed token budget."
That is a deliberate and well-drawn boundary, and #686's closing note says why: the smaller delivered design was preferred over another loading abstraction. I think that was the right call. This issue is about the half that was consciously left outside it, not a re-proposal of tiered loading.
Why the server-side half matters separately
Running this as a shared HTTP service rather than on a laptop changes which half is expensive.
When a caller asks for graph shape — what is connected to what, so it can decide which one note to actually read — build_context(compact=True) still assembles every observation row and note body, serializes the full GraphContext, and discards the bodies at the formatting layer. The caller's context window is protected. The database work, the object construction and the serialization all happened anyway.
On a single-user local install that cost is invisible. On a small shared instance it is the difference between a discovery call being cheap and a discovery call being the most expensive thing an agent does — and discovery calls are the frequent ones, precisely because compact made them the recommended first step. The guidance in COMPACT_DISCOVERY.md ("use compact results to choose notes before reading their bodies") is good advice that increases how often the un-optimized path runs.
The shape I would suggest
Let the request say it does not want bodies, so the traversal can skip assembling them:
- a parameter on the
/memory API endpoints — include_content: bool = True — threaded into the context builder
- when false, observation rows are not fetched or attached, and note content is left
None
compact=True then becomes the presentation-layer expression of the same intent and can simply pass it through, so nothing changes for MCP callers and the existing flag keeps its meaning
That keeps one user-facing concept. compact already means "I want shape, not bodies" — this lets the server act on that rather than only the formatter.
What I have
I run a fork with roughly this implemented at the API layer. I would rather not carry it indefinitely: it is a parameter name on a public surface, which means reconciling it at every sync, and it is the kind of thing that would best live upstream.
Happy to open a PR against whatever shape you prefer — including a different parameter name, or folding it entirely into compact with no new public parameter, which may well be cleaner than what I have. Equally happy to be told the boundary is where you want it; in that case I will stop re-deciding it every sync, which is worth something on its own.
One note in case it affects the answer: _compact_observation and _format_entity_block currently do this work after GraphContext is built, so the change is additive at the API layer and would not disturb the existing formatting path.
The gap is already documented, in three places
compactreduces what the model sees. The docs are explicit that it stops there:docs/COMPACT_DISCOVERY.md— "These options reduce model-visible output, not database search or traversal work."src/basic_memory/man/man3/build-context(3).md— "This reduces MCP output, not API traversal work, and is not a fixed token limit."src/basic_memory/mcp/tools/build_context.py— "This reduces response size, not traversal work or a guaranteed token budget."That is a deliberate and well-drawn boundary, and #686's closing note says why: the smaller delivered design was preferred over another loading abstraction. I think that was the right call. This issue is about the half that was consciously left outside it, not a re-proposal of tiered loading.
Why the server-side half matters separately
Running this as a shared HTTP service rather than on a laptop changes which half is expensive.
When a caller asks for graph shape — what is connected to what, so it can decide which one note to actually read —
build_context(compact=True)still assembles every observation row and note body, serializes the fullGraphContext, and discards the bodies at the formatting layer. The caller's context window is protected. The database work, the object construction and the serialization all happened anyway.On a single-user local install that cost is invisible. On a small shared instance it is the difference between a discovery call being cheap and a discovery call being the most expensive thing an agent does — and discovery calls are the frequent ones, precisely because
compactmade them the recommended first step. The guidance inCOMPACT_DISCOVERY.md("use compact results to choose notes before reading their bodies") is good advice that increases how often the un-optimized path runs.The shape I would suggest
Let the request say it does not want bodies, so the traversal can skip assembling them:
/memoryAPI endpoints —include_content: bool = True— threaded into the context builderNonecompact=Truethen becomes the presentation-layer expression of the same intent and can simply pass it through, so nothing changes for MCP callers and the existing flag keeps its meaningThat keeps one user-facing concept.
compactalready means "I want shape, not bodies" — this lets the server act on that rather than only the formatter.What I have
I run a fork with roughly this implemented at the API layer. I would rather not carry it indefinitely: it is a parameter name on a public surface, which means reconciling it at every sync, and it is the kind of thing that would best live upstream.
Happy to open a PR against whatever shape you prefer — including a different parameter name, or folding it entirely into
compactwith no new public parameter, which may well be cleaner than what I have. Equally happy to be told the boundary is where you want it; in that case I will stop re-deciding it every sync, which is worth something on its own.One note in case it affects the answer:
_compact_observationand_format_entity_blockcurrently do this work afterGraphContextis built, so the change is additive at the API layer and would not disturb the existing formatting path.