●MCP — Support for the 2026-07-28 spec is rolling out across Claude. The protocol moves from bidirectional and stateful to request/response, so MCP servers can now live on serverless and edge infrastructure●EXTENSIONS — Three official extensions have landed: MCP Apps for server-rendered UI, Tasks for async and long-running work, and Enterprise Managed Auth for IdP-based org-wide provisioning●ADOPTION — MCP passed 400 million monthly SDK downloads, roughly 4x growth this year, settling into its role as the standard way to connect agents to applications●QUOTA — Today, August 19, is the last day of the 50 percent weekly usage boost for Claude Code subscribers. If you have long agent runs queued, this is the window●PRICING — Claude Sonnet 5's introductory rate of $2 per million input tokens and $10 output ends August 31; standard pricing of $3 and $15 takes over on September 1, twelve days out●FIX — A bug where MCP v2 connections endlessly reopened subscriptions against servers with fixed timeouts is resolved, and a forward_user_identity setting was added for user attribution●MCP — Support for the 2026-07-28 spec is rolling out across Claude. The protocol moves from bidirectional and stateful to request/response, so MCP servers can now live on serverless and edge infrastructure●EXTENSIONS — Three official extensions have landed: MCP Apps for server-rendered UI, Tasks for async and long-running work, and Enterprise Managed Auth for IdP-based org-wide provisioning●ADOPTION — MCP passed 400 million monthly SDK downloads, roughly 4x growth this year, settling into its role as the standard way to connect agents to applications●QUOTA — Today, August 19, is the last day of the 50 percent weekly usage boost for Claude Code subscribers. If you have long agent runs queued, this is the window●PRICING — Claude Sonnet 5's introductory rate of $2 per million input tokens and $10 output ends August 31; standard pricing of $3 and $15 takes over on September 1, twelve days out●FIX — A bug where MCP v2 connections endlessly reopened subscriptions against servers with fixed timeouts is resolved, and a forward_user_identity setting was added for user attribution
As an indie developer, I hand the day-to-day operations of a handful of apps to a single Managed Agents session. Overnight it looks at crawl status, billing, and inbound questions, then leaves a summary for me to read at breakfast. To support that, I keep a memory store per project, writing down "the decision we made last time" and "the setting nobody should touch."
Every morning I list those memories and look at the diff. It had become a habit. Then one morning the listing returned fewer rows. The day before it had returned 68 across all projects; that morning, 39. Nearly half. No error. No exception, no warning, nothing. A few projects' memories had simply disappeared from the results.
The cause was a single beta header I had bumped the night before: agent-memory-2026-07-22.
Why call the memory listing API yourself
A Managed Agents memory store is mounted into the session container at /mnt/memory/, and the agent reads and writes it with the ordinary file tools. For that part, you can leave everything to the agent.
Calling the listing API from the human side serves a different purpose. Reviewing what the agent wrote. Correcting a bad memory. Exporting the store periodically for an audit. In my case, it was the daily diff: I would walk each project by narrowing memories.list with path_prefix.
Each memory is capped at 100 kB (roughly 25,000 tokens), and the docs explicitly advise structuring memory as "many small focused files, not a few large ones." I followed that, running a tree where the project name sits in the first path segment, like /projects/alpha/decisions/2026-07.md. That design turned out to be exactly where this change landed.
The three things that changed in agent-memory-2026-07-22
Once you bump the beta header to agent-memory-2026-07-22, memories.list behaves differently in three ways.
Aspect
Before
After (agent-memory-2026-07-22)
List ordering
Controlled by order_by / order
Fixed to a stable, server-defined order. order_by / order are ignored
You can brace for the first two just by reading them. The third is the troubling one, because its failure mode is "not an error, just a quietly different result."
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦The three changes in agent-memory-2026-07-22: fixed list ordering, restricted depth values, and segment-based path_prefix matching
✦The trap where a path_prefix without a trailing slash silently returns zero rows, with steps to reproduce it
✦An audit layer that depends on neither ordering nor substring behavior, plus measured depth=1 traversal cost (698.6ms sequential vs 147.8ms level-parallel, and why recursing inside a shared pool deadlocks)
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
The counterintuitive part: realizing I had leaned on substring matching
To sweep across a project and its archive in one call, my audit script did this:
# Before. This picked up both /projects/alpha/... and /projects/alpha_archive/...memories = client.beta.memory_stores.memories.list( memory_store_id=store_id, path_prefix="/projects/alpha", # no trailing slash view="basic", betas=["managed-agents-2026-04-01"],)
The old path_prefix was a substring match. "/projects/alpha" is a prefix of "/projects/alpha/decisions/2026-07.md", but it is equally a prefix of "/projects/alpha_archive/old.md". Half-consciously, I had come to rely on that as a convenient way to grab a project and its archive together.
Under agent-memory-2026-07-22, path_prefix now requires a trailing slash and matches by whole segments. Two things follow.
First, "/projects/alpha" without a trailing slash no longer picks up the range I intended, because it does not land on a segment boundary. Second, the correct form "/projects/alpha/" does not include /projects/alpha_archive/, because alpha and alpha_archive are different segments.
So what used to be "one call returns both the live tree and the archive" became "only the live tree comes back." That was the real reason the count looked halved. No error fired, because the path_prefix itself was still syntactically valid; only the matched range had quietly narrowed.
This was the biggest lesson for me. Of all breaking changes, the dangerous ones are not those that throw, but those that silently return something different. The former you always notice. The latter slips past you unless you are watching.
Rewriting the access layer to survive the change
The fix rests on three principles. Normalize the prefix. Do not depend on ordering. Walk the tree explicitly. With those in mind, I rewrote the audit layer as follows. This is lifted from the code that actually runs every morning, with secrets stripped out.
from anthropic import Anthropicclient = Anthropic() # ANTHROPIC_API_KEY comes from the environmentBETAS = ["managed-agents-2026-04-01", "agent-memory-2026-07-22"]def normalize_prefix(prefix: str) -> str: """A path_prefix must end with a slash. Normalize root to "/".""" if not prefix.startswith("/"): prefix = "/" + prefix if not prefix.endswith("/"): prefix = prefix + "/" return prefixdef list_all_memories(store_id: str, prefix: str = "/") -> list[dict]: """ Return every memory under prefix. - Do not depend on return order (the caller always sorts) - depth accepts only 0/1/omitted, so walk the tree recursively ourselves """ prefix = normalize_prefix(prefix) collected: list[dict] = [] cursor: str | None = None while True: page = client.beta.memory_stores.memories.list( memory_store_id=store_id, path_prefix=prefix, depth=1, # take only direct children; recurse into subtrees view="basic", # metadata only; retrieve content on demand after_id=cursor, betas=BETAS, ) for item in page.data: if item.type == "memory_prefix": # a directory-like node; descend one level collected.extend(list_all_memories(store_id, item.path)) else: collected.append({"id": item.id, "path": item.path}) if not getattr(page, "has_more", False): break cursor = page.last_id # Server order is not guaranteed, so sort ourselves for stable diffs collected.sort(key=lambda m: m["path"]) return collected
A few notes that live outside the code.
I pinned depth=1 and made the function recurse whenever it hits a memory_prefix (a directory-like node). I used to pass a large depth to grab everything at once, but that value is now a 400. Walking the tree myself frees me from the depth limit and, as a bonus, lets me see the count at each level.
I lean on ordering nowhere. After fetching, I sort by path. A day-over-day diff should be compared as a set, not as a sequence, and written that way it does not break when the server order shifts.
And I do not pull content: view="basic". An audit needs only the path and ID. When I actually need the body, I memories.retrieve a narrowed set. Downloading 100 kB times many files every morning was a waste, both in time and in cost.
Rewriting "the live tree plus the archive" as an explicit intent
The old, substring-driven behavior of grabbing both the live tree and the archive was convenient, but the intent was implicit. I took the migration as a chance to rewrite it as an explicit enumeration.
def list_project_including_archive(store_id: str, project: str) -> list[dict]: """Deliberately combine the live tree and archive as two separate prefixes.""" live = list_all_memories(store_id, f"/projects/{project}/") archive = list_all_memories(store_id, f"/projects/{project}_archive/") return live + archive
Writing it out showed me just how fragile the old code had been. Once I had more projects named around "alpha," the substring match would have swept in alpha2 and alpha_experimental too. Segment matching did not make things less convenient; it forced my vague intent to become explicit. That is how I have come to see it.
Safe corrections with a content_sha256 precondition
Half of what an audit is for is correcting bad memories, and there is a hazard here that is independent of the version change. Because the agent and I touch the same memories, overwrite conflicts are possible.
memories.update supports optimistic concurrency. Pass the content_sha256 you read, and the update applies only if the stored hash still matches.
def safe_correct(store_id: str, memory_id: str, new_content: str) -> bool: current = client.beta.memory_stores.memories.retrieve( memory_store_id=store_id, memory_id=memory_id, betas=BETAS, ) try: client.beta.memory_stores.memories.update( memory_store_id=store_id, memory_id=memory_id, content=new_content, precondition={ "type": "content_sha256", "content_sha256": current.content_sha256, }, betas=BETAS, ) return True except Exception: # hash mismatch = someone (usually the agent itself) wrote first. # do not clobber silently; re-read and decide again return False
There really were nights when the agent was appending to the same file. Back when I corrected memories without a precondition, I must have occasionally stomped those appends. Every change is retained as an immutable memory version (memver_...), so you can notice after the fact, but not stomping it is better still.
Counting the blast radius before you migrate
I caught the halving only because I happened to have the previous day's count on hand. Put the other way around: without it, I would not have caught it at all.
Guarding against a repeat turned out to be simpler than expected. As long as you have the list of memory path values, you can measure the blast radius without touching the API. You just apply the old substring rule and the new segment rule to the same set of paths, side by side.
"""prefix_audit.py — count the gap between old substring and new segment matching, offline."""import json, sysdef substring_match(paths, prefix): # the old behavior return [p for p in paths if p.startswith(prefix)]def segment_match(paths, prefix): # the new behavior if not prefix.endswith("/"): return [] # no trailing slash means no match return [p for p in paths if p.startswith(prefix)]def prefix_nodes(paths): """How many directory-like nodes a depth=1 recursion has to step through.""" nodes = set() for p in paths: parts = p.strip("/").split("/")[:-1] for i in range(len(parts)): nodes.add("/" + "/".join(parts[: i + 1]) + "/") return nodesif __name__ == "__main__": paths = json.load(open(sys.argv[1])) # paths collected from memories.list print(f"total memories: {len(paths)}") n = len(prefix_nodes(paths)) print(f"prefix nodes : {n} -> depth=1 recursion needs {n + 1} list calls\n") print(f"{'path_prefix':<28}{'substring':>10}{'as-written':>12}{'normalized':>12}{'lost':>7}") for pf in sys.argv[2:]: old = len(substring_match(paths, pf)) raw = len(segment_match(paths, pf)) new = len(segment_match(paths, pf if pf.endswith("/") else pf + "/")) print(f"{pf:<28}{old:>10}{raw:>12}{new:>12}{old - new:>7}")
Run it against a tree shaped like mine — six projects, 68 memories — and it prints this.
Three columns carry the meaning. substring is the old match count, as-written is what you get if you send the prefix exactly as it appears in your code, and normalized is what you get after adding the trailing slash.
Lined up like that, two distinct failure modes come into view.
/projects/alpha drops from 34 to 20 — a 41% shortfall. Those 14 are alpha_archive and alpha2 — the places where my intent had been implicit. /projects/beta, by contrast, reads 16 under both substring and normalized. By count alone it looks untouched. Yet as-written is 0. Ship it with the trailing slash still missing and it does not come back partially; it comes back empty.
The rows where lost is 0 are the dangerous ones. They show no difference, so they get filed as "unaffected" and nobody touches them.
Then there is the second line of the output. Fifteen nodes in the tree means a depth=1 recursion turns one list call into sixteen, a 16x increase. For a once-a-morning audit that is noise. For a loop that runs every few minutes it is not. I walk the shallow levels every time and descend into the deeper ones only when the count above them has moved. That estimate of sixteen turns out to be wrong, incidentally; the next section runs it and corrects the number.
Measured: what fifteen recursive calls actually cost, and where naive parallelism jams
Saying "sixteen times as many calls" in the previous section was not enough to act on. If a single call takes 5 ms, sixteen of them still add up to 80 ms. So I measured it.
I stood up a minimal local server that returns the same shape of response as memories.list, with 45 ms of latency injected per call. The tree is exactly the one from the audit output above — 68 memories, 15 prefix nodes. Python 3.10.12, median of three runs.
The first thing the run surfaced was a mistake in my own audit script.
prefix nodes : 15 -> depth=1 recursion needs 16 list calls
actual walk : list calls = 15
prefix_nodes() already includes the root /projects/ in its set. Adding n + 1 on top of that counted the root twice. It is a difference of one call, but the reasoning behind the estimate was still wrong. Where the previous section says sixteen, fifteen is the correct number.
With that settled, here is what the sequential depth-first recursion cost.
sequential depth-first items=68 698.6 ms
Fifteen calls at 45 ms each is 675 ms. Almost exactly the theoretical figure, which means nearly all of that time is spent waiting on round trips. That suggested parallelism would help, so I passed a ThreadPoolExecutor down and called ex.map inside the recursion. That is where it jammed.
recursion inside pool workers=2 no return after 5 seconds (deadlock)
recursion inside pool workers=4 no return after 5 seconds (deadlock)
recursion inside pool workers=8 completed items=68 286.0 ms
Each parent task holds a worker while it waits for its children. With four workers, the root plus three projects fill the pool, and the fourth project queues forever. Nothing raises. It simply never comes back. The run that succeeded at workers=8 only did so because this particular tree happened to fit; add one more project and it stalls the same way.
The cause is recursion sharing a bounded pool, so I dropped the recursion and widened the walk one level at a time instead.
from concurrent.futures import ThreadPoolExecutordef fetch_level(store_id: str, prefix: str) -> list[dict]: """Fetch exactly one level below prefix, draining pagination.""" items, cursor = [], None while True: page = client.beta.memory_stores.memories.list( memory_store_id=store_id, path_prefix=prefix, depth=1, view="basic", after_id=cursor, betas=BETAS, ) items += [{"path": i.path, "type": i.type} for i in page.data] if not getattr(page, "has_more", False): return items cursor = page.last_iddef list_level_parallel(store_id: str, prefix: str = "/", workers: int = 8) -> list[str]: """Descend the tree level by level. No recursion inside the pool, so it cannot jam.""" out: list[str] = [] frontier = [normalize_prefix(prefix)] with ThreadPoolExecutor(max_workers=workers) as ex: while frontier: next_level: list[str] = [] for page in ex.map(lambda p: fetch_level(store_id, p), frontier): for item in page: if item["type"] == "memory_prefix": next_level.append(item["path"]) else: out.append(item["path"]) frontier = next_level return sorted(out)
Measured again against the same tree:
Traversal
Elapsed (median)
vs. sequential
Sequential depth-first
698.6 ms
1.00x
Level-parallel, workers=2
376.2 ms
1.86x
Level-parallel, workers=4
238.9 ms
2.92x
Level-parallel, workers=8
147.8 ms
4.73x
Level-parallel, workers=16
147.9 ms
4.72x
Doubling the workers from 8 to 16 does not move the number off 147 ms. This tree is three levels deep — /projects/ to project to subdirectory — and since each level waits on the one above it, nothing gets faster than 3 × 45 ms = 135 ms. The measured 147.8 ms sits just above that floor.
Parallelism buys you the width of the tree, not its depth. The time an audit takes is governed roughly by how many levels it has, not by how many memories live in it. Putting the project name in the first segment and keeping two levels beneath it had, without my intending it, been holding that floor down. It is a useful shape to remember the next time a deeper hierarchy looks tempting.
The 45 ms is a value I injected, not the real latency of the API. What I wanted from this was not the absolute numbers but the shape of the speedup and the position of the ceiling. If you are making the same call for your own setup, measure one round trip first, then map your figure onto this table.
Situational guidance
Here are the judgment calls the migration tends to raise, laid out by situation.
Situation
Recommendation
Narrowing by prefix
Normalize every path_prefix to end with a slash. Hunt down places that relied on substring matching
Controlling order with order_by
Sort yourself after fetching. Move paging to after_id and drop any order-dependent logic
Fetching a deep tree at once
Switch to depth=1 plus recursion, following memory_prefix nodes
Human and agent write the same store
Always use a content_sha256 precondition for corrections. Attach reference-only stores as read_only
One last thing. Right after you bump the beta header, snapshot the total count once. The only reason I caught the halving was that I happened to have the previous day's count on hand. Things that change quietly can only be caught by watching quietly.
Memory changes bite hardest in long-running operations. This path_prefix change cut deepest precisely where the tree had been designed most straightforwardly. If you want to dig into the background of that design thinking, I would also point you to Pitfalls and patterns from running Claude agent memory in production.
I am still feeling my way through much of this area. If this gives even one person who was puzzled by a quiet change in row counts a place to start checking, I would be glad. Thank you for reading.
Share
Thank You for Reading
Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.