Files
efenex e68d4ebe9b fix(summarize): chunk large sessions to fit LLM context window (#472)
* fix(summarize): chunk large sessions to fit LLM context window

JSONL-imported sessions can have far more observations than the
500-cap MAX_OBS_PER_SESSION that constrains native sessions.
mem::summarize previously built one prompt containing every
observation and shipped it as a single LLM call, which exceeded the
provider's context window for sessions >~7,000 observations and
returned an unhelpful 400 from upstream — silently leaving large
bulk-imported sessions out of the semantic tier.

Approach: map-reduce inside mem::summarize.
- Sessions ≤ SUMMARIZE_CHUNK_SIZE (default 400) take the legacy
  single-call path with no overhead
- Larger sessions are split into chunks, each summarized with the
  existing per-session prompt in parallel batches of
  SUMMARIZE_CHUNK_CONCURRENCY (default 6), and partial summaries
  merged via a new REDUCE_SYSTEM prompt
- Per-chunk retry-once on transient parse / provider errors
- Persistently-failing chunks are skipped (not propagated) so a
  flaky chunk doesn't waste 30+ already-completed LLM calls on
  the same session
- Bail with too_many_chunks_skipped only if >50% of chunks fail

Companion operator tool: scripts/backfill-imported-sessions.sh
walks jsonl-imported sessions and POSTs mem::summarize per session,
with project / agent / obs-count filters, cost estimation, and
per-failure payload dumping for debugging provider rejections.

Validated locally against a real corpus:
- 5,392-obs session (14 chunks, c=6): 39s
- 10,704-obs session (27 chunks, c=6): 34s
- 105,966-obs session (265 chunks, c=50): handler completes
  server-side and persists
- 52-session bulk backfill → 25 new semantic facts + 6 new reflect
  insights produced by consolidate-pipeline

Known limit: iii-engine has a hardcoded 180s function-invocation
timeout. Sessions large enough that chunked summarize wallclock
exceeds that will return a timeout/500 to the HTTP client even
though the handler completes and persists server-side. High-RPM
providers (Novita / DeepInfra / DeepSeek typically allow 100+
concurrent) can raise SUMMARIZE_CHUNK_CONCURRENCY to push the
cliff well past any realistic session size. True fix is an
async-job pattern; left as follow-up.

- src/prompts/summary.ts: add REDUCE_SYSTEM + buildReducePrompt
- src/functions/summarize.ts: chunking, retry, skip, parallelism
- test/summarize.test.ts: 9 cases covering single-call path,
  chunking, env-override, retry-then-success, persistent skip,
  too-many-skipped bail, provider error after retry, concurrency
- .env.example: document SUMMARIZE_CHUNK_SIZE / _CONCURRENCY
- .gitignore: agentmemory-debug/, data-*/  (operator artefacts)
- scripts/backfill-imported-sessions.sh: bulk-import backfill tool

9/9 new tests pass; existing tests untouched.

* fix(summarize): address CodeRabbit review on #472

Four nits flagged by the automated reviewer, all worth fixing:

- scripts/backfill: add curl --connect-timeout + --max-time profiles
  (META_CURL_OPTS vs WORK_CURL_OPTS). Metadata reads fail fast and
  retry on transient blips; LLM-backed work calls get a wide 30-min
  cap and no retry (retrying a half-finished LLM job double-spends).
- scripts/backfill: sanitize sessionId before joining with DEBUG_DIR
  in dump_failure() (otherwise a session id containing `/` or `..`
  could escape the debug dir). UUIDs in practice, but the server
  doesn't enforce that.
- scripts/backfill: switch the observations query to
  `--get --data-urlencode "sessionId=$id"` so special characters
  can't corrupt the query string.
- scripts/backfill: guard `jq` on summarize + consolidate responses
  with `jq -e . </dev/null 2>&1` first. iii's HTTP layer occasionally
  returns non-JSON (HTML 5xx, empty body on timeout). Without the
  guard, `set -e` aborts the whole backfill loop on a single bad
  response — now it logs `invalid_json_response` and moves on.
- test/summarize.test.ts: fix `vi.mock("./audit.js", ...)` path to
  `"../src/functions/audit.js"`. The old path resolved to
  `test/audit.js` (nonexistent), so the mock was a silent no-op.
  Tests passed anyway because `safeAudit` writes to a mocked KV.

9/9 tests still pass; backfill dry-run still resolves the corpus
cleanly.
2026-05-19 19:29:06 +01:00
..