Session summarization is quadratic: the Stop hook re-summarizes the entire session every assistant turn
Version: 0.9.27 (npm @agentmemory/agentmemory)
Provider: Ollama (llama3.2), local
Platform: Linux, Claude Code plugin hooks
Summary
plugin/scripts/stop.mjs runs on Claude Code's Stop hook, which fires at the end of every assistant turn — not at session end. It POSTs the session ID to /agentmemory/summarize, and the server re-summarizes the entire session from scratch each time.
Because observation count grows monotonically through a session, and chunk count grows with it, total LLM calls over a session's life are O(N²/chunkSize) rather than O(N). Long sessions saturate the local LLM, trip the circuit breaker, and then every chunk fails instantly.
Why it's quadratic
produceSummaryXml(provider, compressed, sessionId, project) chunks the full compressed observation array on every invocation:
const chunks = [];
for (let i = 0; i < compressed.length; i += chunkSize) chunks.push(compressed.slice(i, i + chunkSize));
There's no memo of previously-summarized observations and no partial reuse — chunk k is recomputed identically on every turn for the rest of the session. Only the final merge differs.
Observed
One session on 2026-07-28, logged over ~40 minutes. Each line is a separate turn triggering a full re-summarization:
chunks=20 totalObservations=229
chunks=21 totalObservations=241
chunks=21 totalObservations=251
chunks=24 totalObservations=279
chunks=28 totalObservations=327
chunks=28 totalObservations=331
...
chunks=40 totalObservations=480
That session eventually saturated Ollama. Once the circuit breaker opened, every chunk failed immediately:
warn Summarize chunk LLM call failed {"chunk":"36/40","attempt":2,"error":"circuit_breaker_open"}
warn Summarize chunk LLM call failed {"chunk":"37/40","attempt":1,"error":"circuit_breaker_open"}
...
error Summarize failed {"error":"too_many_chunks_skipped: 40/40 chunks failed to parse after retry"}
2,976 failure records in one night, versus 0–14/day on normal days.
A smaller, current example — 32 chunk-LLM calls to summarize a session holding 47 observations, where incremental would need ~4:
06:50 chunks=2 obs=16
07:29 chunks=3 obs=31
07:32 chunks=3 obs=35
07:33 chunks=4 obs=38
07:36 chunks=4 obs=47
Interaction with SUMMARIZE_CHUNK_SIZE
Lowering SUMMARIZE_CHUNK_SIZE from the default 400 is currently the recommended fix for XML parse failures on small-context local models (400 observations builds a prompt far past llama3.2's default Ollama context, truncating the format instructions). That fix works — but it amplifies this bug, because smaller chunks mean proportionally more LLM calls per re-summarization. At chunkSize=12, a 480-observation session is 40 chunks per turn.
So the two knobs pull against each other: the setting that makes each call succeed makes the redundant work worse.
Secondary: each summarize appears to run twice
Every Summarize chunking session line is logged exactly twice with an identical timestamp:
07:29:12 Summarize chunking session {"chunks":3,...,"totalObservations":31}
07:29:12 Summarize chunking session {"chunks":3,...,"totalObservations":31}
stop.mjs is the only script that POSTs to /summarize, so I couldn't account for the 2×. It may be duplicate logging rather than duplicate work, but if it's the latter it doubles the cost above. Happy to dig further if useful.
Suggested fixes
In rough order of preference:
- Incremental summarization — persist per-chunk partials keyed by observation range/hash and only summarize chunks containing new observations, merging against the stored partials. This is the real fix and turns the cost linear.
- Debounce server-side — skip a summarize request if one completed for that session within the last N seconds (configurable). Cheap to implement, doesn't fix the asymptotics but caps the damage.
- Decouple from
Stop — Stop firing per-turn seems like an unintended reading of the hook's semantics. If per-turn freshness isn't required, SessionEnd alone would be dramatically cheaper. Note session-end.mjs currently does not call /summarize, so moving it there would need that added.
Workaround in use
A wrapper on the Stop hook that lets the full path through at most once per 10 minutes per session, falls back to the cheap /session/end ping otherwise, and forces a final summarize on SessionEnd so the session tail isn't lost. Happy to share if it's useful to others hitting this, though it's obviously a band-aid over (1).
Environment notes
SUMMARIZE_CHUNK_SIZE=12, SUMMARIZE_CHUNK_CONCURRENCY=2, OPENAI_TIMEOUT_MS=120000
- These are read from
process.env only — worth documenting, since setting them in ~/.agentmemory/.env has no effect (that file is loaded for provider config and never merged into process.env). On systemd they must go in a unit drop-in.
Session summarization is quadratic: the
Stophook re-summarizes the entire session every assistant turnVersion: 0.9.27 (npm
@agentmemory/agentmemory)Provider: Ollama (llama3.2), local
Platform: Linux, Claude Code plugin hooks
Summary
plugin/scripts/stop.mjsruns on Claude Code'sStophook, which fires at the end of every assistant turn — not at session end. It POSTs the session ID to/agentmemory/summarize, and the server re-summarizes the entire session from scratch each time.Because observation count grows monotonically through a session, and chunk count grows with it, total LLM calls over a session's life are O(N²/chunkSize) rather than O(N). Long sessions saturate the local LLM, trip the circuit breaker, and then every chunk fails instantly.
Why it's quadratic
produceSummaryXml(provider, compressed, sessionId, project)chunks the fullcompressedobservation array on every invocation:There's no memo of previously-summarized observations and no partial reuse — chunk k is recomputed identically on every turn for the rest of the session. Only the final merge differs.
Observed
One session on 2026-07-28, logged over ~40 minutes. Each line is a separate turn triggering a full re-summarization:
That session eventually saturated Ollama. Once the circuit breaker opened, every chunk failed immediately:
2,976 failure records in one night, versus 0–14/day on normal days.
A smaller, current example — 32 chunk-LLM calls to summarize a session holding 47 observations, where incremental would need ~4:
Interaction with
SUMMARIZE_CHUNK_SIZELowering
SUMMARIZE_CHUNK_SIZEfrom the default 400 is currently the recommended fix for XML parse failures on small-context local models (400 observations builds a prompt far past llama3.2's default Ollama context, truncating the format instructions). That fix works — but it amplifies this bug, because smaller chunks mean proportionally more LLM calls per re-summarization. AtchunkSize=12, a 480-observation session is 40 chunks per turn.So the two knobs pull against each other: the setting that makes each call succeed makes the redundant work worse.
Secondary: each summarize appears to run twice
Every
Summarize chunking sessionline is logged exactly twice with an identical timestamp:stop.mjsis the only script that POSTs to/summarize, so I couldn't account for the 2×. It may be duplicate logging rather than duplicate work, but if it's the latter it doubles the cost above. Happy to dig further if useful.Suggested fixes
In rough order of preference:
Stop—Stopfiring per-turn seems like an unintended reading of the hook's semantics. If per-turn freshness isn't required,SessionEndalone would be dramatically cheaper. Notesession-end.mjscurrently does not call/summarize, so moving it there would need that added.Workaround in use
A wrapper on the
Stophook that lets the full path through at most once per 10 minutes per session, falls back to the cheap/session/endping otherwise, and forces a final summarize onSessionEndso the session tail isn't lost. Happy to share if it's useful to others hitting this, though it's obviously a band-aid over (1).Environment notes
SUMMARIZE_CHUNK_SIZE=12,SUMMARIZE_CHUNK_CONCURRENCY=2,OPENAI_TIMEOUT_MS=120000process.envonly — worth documenting, since setting them in~/.agentmemory/.envhas no effect (that file is loaded for provider config and never merged intoprocess.env). On systemd they must go in a unit drop-in.