Skip to content

Session summarization is quadratic: the Stop hook re-summarizes the entire session every assistant turn #1131

Description

@FancyPantsJim

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:

  1. 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.
  2. 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.
  3. Decouple from StopStop 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions