Once a store grows past a certain size, GET /agentmemory/export stops returning and takes
the whole daemon down with it for about a second. The failure is a hard size limit on the
response, not a timeout, and it is fully deterministic.
What I see
$ curl -s -o /dev/null -w '%{http_code}\n' localhost:3111/agentmemory/export
500
$ curl -s localhost:3111/agentmemory/export
{"error":"Invocation stopped","error_id":"…"}
$ curl -s localhost:3111/agentmemory/export
{"error":"Function api::export not found","error_id":"…"}
The daemon log shows the export finishing before the connection dies:
[agentmemory] info Export complete {"sessions":15,"observations":12750,"memories":3000,…}
[iii] Reconnecting in 955ms (attempt 1)...
[iii] Worker registered with ID: 43829794-b900-4040-8351-58cf42d5ec61
Export complete is logged, so the data was assembled and the function returned. What dies is
the response on its way back. While the worker re-registers, every endpoint 404s, so a single
GET makes the entire REST surface unavailable for ~1s.
Expected
Either the export returns, or it fails on its own without disconnecting the worker and taking
128 unrelated endpoints with it.
The limit is 16 MiB, and it is a limit rather than a timeout
Bisected on a local instance by growing the store in ~27 KB steps and re-requesting a fixed
slice:
| serialized response |
result |
| 16 771 046 B (15.9941 MiB) |
200 OK in 365 ms |
| next step, +27 KB |
500 in 103 ms |
16 MiB is 16 777 216 B, so the last success lands 6 170 B short of it — 0.037%. Note the
failing request returns faster than the succeeding one (103 ms vs 365 ms). Nothing is
timing out; the frame is refused.
Where the limit appears to live
I could not find it in this package. iii-sdk opens its WebSocket with no maxPayload, so
the ws client default of 100 MiB applies. The iii engine binary carries tungstenite's
WebSocketConfig field names (max_write_buffer_size, max_fragment_size), and
tungstenite's default max_frame_size is exactly 16 MiB, which matches the measurement.
This is an inference from strings in the binary, not something I read in engine source,
and I could find no config key or CLI flag that exposes it. If the engine can be configured
to raise it, that would be a second, independent fix.
One detail that matters for where a guard can go: api::export reaches mem::export through
sdk.trigger, so the result crosses the worker↔engine boundary twice. It dies on the first
hop, which is why the HTTP layer only ever sees Invocation stopped.
Why pagination does not currently help
?maxSessions=/?offset= slice sessions and the observations hanging off them. Every
other collection — memories, summaries, graphNodes, graphEdges, semantic,
procedural, actions, actionEdges, sentinels, sketches, crystals, facets,
lessons, insights, routines, signals, checkpoints, accessLog — is fetched in full
regardless. On my repro store ?maxSessions=1 still returned 5.50 MB, of which ~4.9 MB was
that unpaginatable floor.
That floor grows with the store. Once it alone crosses 16 MiB, export becomes impossible at
any parameter combination, with no way back — the same dead end #890 describes for
mesh/export.
Collateral: the drop leaves a KV call hanging
Every reproduction also left an index-persistence write in flight that never settled:
[agentmemory] warn audit write failed {"functionId":"mem::index-persistence",
"operation":"index_persist","error":"Invocation timeout after 180000ms: state::set"}
That is the 180 s inherited worker default #1127/#1128 is about, reached because the worker
went away mid-call rather than because the call was slow.
Reproduction
Against a local instance with a scratch data dir (agentmemory --data-dir /tmp/scratch),
seed in rounds and probe after each:
const B = "http://localhost:3111/agentmemory";
const obs = (s, i) => ({
id: `obs_${s}_${i}`, sessionId: s, timestamp: "2026-02-01T10:00:00Z",
type: "file_edit", title: `Edit ${i}`, facts: [`fact ${i} `.repeat(6)],
narrative: `narrative ${i} `.repeat(12), concepts: ["auth"],
files: [`src/f_${i}.ts`], importance: 5,
});
for (let round = 1; round <= 8; round++) {
const sessions = [], observations = {};
for (let s = 0; s < 5; s++) {
const id = `ses_r${round}_${s}`;
sessions.push({ id, project: "p", cwd: "/tmp", startedAt: "2026-02-01T00:00:00Z",
status: "completed", observationCount: 850 });
observations[id] = Array.from({ length: 850 }, (_, i) => obs(id, i));
}
await fetch(`${B}/import`, {
method: "POST", headers: { "content-type": "application/json" },
body: JSON.stringify({ exportData: {
version: "0.9.28", exportedAt: new Date().toISOString(),
sessions, observations, memories: [], summaries: [] }, strategy: "merge" }),
});
const r = await fetch(`${B}/export`);
const body = await r.text();
console.log(round, r.status, (body.length / 1048576).toFixed(2) + " MiB");
}
Reproduced 6/6. On my machine it flips at round 3.
Environment
- agentmemory
0.9.28 (npm view @agentmemory/agentmemory version → 0.9.28)
- Node
v26.5.0
- macOS 26.5.2, arm64
- native
iii engine 0.11.2 (the pinned version), store_method: file_based
Related
#890 (mesh/export unpaginated, same Invocation stopped), #1124 (a single request
unregistering the whole worker), #1100 (a fan-out that wedges an endpoint), #195 (event-loop
freezes), #1127/#1128 (the 180 s KV default the hanging call reaches).
A PR follows this issue: refuse the dump before it reaches the transport and return 413
with the paging parameters, plus extend paging to the other collections so a large store stays
exportable. Happy to reshape it if you'd rather solve this a different way.
Once a store grows past a certain size,
GET /agentmemory/exportstops returning and takesthe whole daemon down with it for about a second. The failure is a hard size limit on the
response, not a timeout, and it is fully deterministic.
What I see
The daemon log shows the export finishing before the connection dies:
Export completeis logged, so the data was assembled and the function returned. What dies isthe response on its way back. While the worker re-registers, every endpoint 404s, so a single
GET makes the entire REST surface unavailable for ~1s.
Expected
Either the export returns, or it fails on its own without disconnecting the worker and taking
128 unrelated endpoints with it.
The limit is 16 MiB, and it is a limit rather than a timeout
Bisected on a local instance by growing the store in ~27 KB steps and re-requesting a fixed
slice:
16 MiB is 16 777 216 B, so the last success lands 6 170 B short of it — 0.037%. Note the
failing request returns faster than the succeeding one (103 ms vs 365 ms). Nothing is
timing out; the frame is refused.
Where the limit appears to live
I could not find it in this package.
iii-sdkopens its WebSocket with nomaxPayload, sothe
wsclient default of 100 MiB applies. Theiiiengine binary carries tungstenite'sWebSocketConfigfield names (max_write_buffer_size,max_fragment_size), andtungstenite's default
max_frame_sizeis exactly 16 MiB, which matches the measurement.This is an inference from strings in the binary, not something I read in engine source,
and I could find no config key or CLI flag that exposes it. If the engine can be configured
to raise it, that would be a second, independent fix.
One detail that matters for where a guard can go:
api::exportreachesmem::exportthroughsdk.trigger, so the result crosses the worker↔engine boundary twice. It dies on the firsthop, which is why the HTTP layer only ever sees
Invocation stopped.Why pagination does not currently help
?maxSessions=/?offset=slicesessionsand theobservationshanging off them. Everyother collection —
memories,summaries,graphNodes,graphEdges,semantic,procedural,actions,actionEdges,sentinels,sketches,crystals,facets,lessons,insights,routines,signals,checkpoints,accessLog— is fetched in fullregardless. On my repro store
?maxSessions=1still returned 5.50 MB, of which ~4.9 MB wasthat unpaginatable floor.
That floor grows with the store. Once it alone crosses 16 MiB, export becomes impossible at
any parameter combination, with no way back — the same dead end #890 describes for
mesh/export.Collateral: the drop leaves a KV call hanging
Every reproduction also left an index-persistence write in flight that never settled:
That is the 180 s inherited worker default #1127/#1128 is about, reached because the worker
went away mid-call rather than because the call was slow.
Reproduction
Against a local instance with a scratch data dir (
agentmemory --data-dir /tmp/scratch),seed in rounds and probe after each:
Reproduced 6/6. On my machine it flips at round 3.
Environment
0.9.28(npm view @agentmemory/agentmemory version→0.9.28)v26.5.0iiiengine0.11.2(the pinned version),store_method: file_basedRelated
#890 (mesh/export unpaginated, same
Invocation stopped), #1124 (a single requestunregistering the whole worker), #1100 (a fan-out that wedges an endpoint), #195 (event-loop
freezes), #1127/#1128 (the 180 s KV default the hanging call reaches).
A PR follows this issue: refuse the dump before it reaches the transport and return
413with the paging parameters, plus extend paging to the other collections so a large store stays
exportable. Happy to reshape it if you'd rather solve this a different way.