Skip to content

feat(store,mcp): add partial reads to mem_get_observation (offset/limit + find/context)Β #812

Description

@dpaul20

✨ Feature Request

πŸ“‹ Pre-flight Checks

  • I have searched existing issues and this is not a duplicate
  • I understand this issue needs status:approved before a PR can be opened

Searched: mem_get_observation offset range, search within observation, partial update, large observation, observation length limit. The closest matches are #602 / PR #774 (find/replace on mem_update) and #752 (opt-in search env vars). #602 addresses the write side of the same cost; this issue addresses the read side, and the two compose. Filing separately rather than as a comment on #602 so it can be scoped, approved and reverted independently.

πŸ” Problem Description

mem_get_observation returns the whole observation or nothing β€” the only parameter is id (internal/mcp/mcp.go:613-623). For small memories that is exactly right. For a large one, an agent that needs a single section still pays for the entire body.

This bites hardest in the workflow the tool is built for: an agent that edits a stored document repeatedly.

Real case, from an SDD design cycle where four design artifacts were kept in engram:

artifact chars
design (Part A) 47,051
design (Part B) 45,688
design (Part C) 23,446
dispositions 16,833
total 133,018

One of those observations carries revision_count = 7 β€” it was rewritten seven times across review rounds. Every round, an agent had to pull all four artifacts in full (~33,000 tokens of input) in order to correct a few dozen paragraphs. On disk the same job is rg for the section, then read ~2,000 chars around it.

The cost is not the editing. It is that locating the passage requires downloading everything around it.

Two agent runs were lost to session limits mid-task before the artifacts were moved out of engram to plain files β€” which also gave up the recall, topic_key upsert and memory_relations conflict detection that were the reason to use engram in the first place. That trade should not be necessary.

#602 fixes half of this. With find/replace, writing a correction becomes cheap. Reading stays at ~33,000 tokens, because the agent still has to see the text before it can know what to replace. Ranged reads are the other half; together they make engram viable for documents that are revised, not just facts that are recalled.

πŸ’‘ Proposed Solution

Add optional parameters to mem_get_observation. Two independent mechanisms β€” either can ship alone:

1. Mechanical paging β€” offset + limit

mem_get_observation(id: 475, offset: 12000, limit: 2000)

Returns runes [offset, offset+limit) of content. Mirrors how every file-read tool works.

2. Match-scoped read β€” find + context

mem_get_observation(id: 475, find: "test 7", context: 600)

Returns each window of context runes around every occurrence of find, with its rune offset, so the agent can locate and read in one round-trip instead of guessing an offset. This is the mode agents actually need: you rarely know an offset, you know the text you are looking for.

The returned offsets compose directly with #602 β€” locate with find, patch with mem_update(find:…, replace:…).

Response shape (mode 2), each window prefixed so the agent knows what it did not get:

#475 "design Part A" β€” 47,051 runes total, 3 matches for "test 7"

[offset 21,340]
…text…

[offset 28,902]
…text…

Validation rules:

Defaults: limit and context default to a bounded value rather than unlimited, so a partial read cannot accidentally cost more than the full read.

⚠ Offsets must be counted in runes, not bytes. #697, #751 and #809 are all live reports of byte/rune confusion in the truncation and preview paths. A byte-indexed offset would split multi-byte characters and reproduce that same class of bug in a new surface. []rune conversion, or utf8.RuneCountInString plus a rune-aware slice, keeps this consistent with how those issues are being resolved.

πŸ“¦ Affected Area

  • MCP Server (tools, transport)
  • Store (database, queries)

πŸ”„ Alternatives Considered

Comment on #602 instead of filing separately. Rejected: #602 is already approved with an open PR (#774). Adding read-side scope now would widen an approved issue after the fact and complicate a PR that is otherwise passing its checks.

Use mem_search to locate, then mem_get_observation. Does not work for this. mem_search ranks across observations and returns a 300-char preview (internal/mcp/mcp.go:1063); it cannot enumerate matches within one body or report where they are. There is no "grep inside this observation" today.

Split large documents into many small observations. This is what the truncation warning already advises, and it is right for facts. It fails for documents: a design document split across four observations still has to be read in full to be revised, and the split itself caused a real defect in our case β€” the same content ended up duplicated across two artifacts instead of moved, so a single edit then needed two updates that could silently drift apart.

Raise MaxObservationLength. Orthogonal. A bigger cap makes the read cost worse, not better.

Keep documents on disk, keep facts in engram. The current workaround, and what we did. It works, but it forfeits recall, topic_key upsert and memory_relations for exactly the artifacts that most need conflict detection.

SQL-level substr(). Viable for mode 1 and avoids loading the full body into Go, but SQLite's substr() on TEXT counts characters, not runes, and mode 2 needs match positions anyway. Worth considering as an optimisation once behaviour is settled, not as the initial shape.

πŸ“Ž Additional Context

Token math at ~4 chars/token, for the real case above β€” one review round that corrected passages across all four artifacts:

read cost write cost (with #602)
today ~33,000 tok ~cheap
with match-scoped reads ~1,500 tok ~cheap

The read saving is roughly 95%, and it scales with document size while the write saving does not β€” the larger the observation, the more of the remaining cost is reading.

Implementation reference files:

Relationship to #602: complementary, not overlapping. #602 makes an edit cheap once you know what to change. This makes finding out what to change cheap. Neither depends on the other, and shipping both is what removes the reason to keep large documents outside engram.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions