Files
thedotmack__claude-mem/tests/context
Alex Newman 1e284feb05 fix(context): fit the session block to the hook output limit by selection (#3995)
* fix(context): fit the session block to the hook output limit by selection

Claude Code delivers a hook's stdout verbatim up to 10,000 characters, then
writes the whole block to disk and hands the model a ~2KB preview stub
instead -- while the hook still reports success. The context block was sized
by item counts, so on an active project it crossed that line and the model
silently received no context at all.

Counts cannot fix it: an observation title runs ~150-320 characters, a
session-summary line ~250-600, and the last-session summary block 1-5K on its
own, so any count safe on a quiet project wastes the budget and any count
that fills the budget there overflows on a busy one. The shipped default of
50 observations overflows on titles alone.

Measure instead. Render, and while the result is over budget drop the most
expensive thing still in it and render again: full narratives first (their
titles stay), then the last-session summary block, then sessions, then
observations. Whole items go, never half of one, so the block stays
well-formed and the header, footer and at least one observation survive.

--full is an explicit human request and is left unbounded.

Fixes #3802

* fix(context): count the health warning and report what was delivered

Addresses the three P1 findings on this PR. All three were real; the first two
are fixed here with tests, the third by rebuilding the two bundles that
actually carry the change.

**The observer-health warning is now inside the measured block.** It was
appended to the already-fitted text, so its characters were never counted: a
block fitted to 9,998 came back at 10,585 whenever the observer was unhealthy,
and over the limit Claude Code replaces the whole thing with the ~2KB preview
stub — losing the context *and* the outage warning, which is exactly when that
warning matters most.

`readObserverHealth` is now read once rather than per render. `fitContextToBudget`
calls its render repeatedly and that function touches state; re-reading it per
reduction would let the measured length move under the loop.

**Injection stats now describe the delivered block.** A run trimmed from seven
observations to three still reported `observation_count: 7` and 9,646 tokens
injected while the model received three observations and 4,134 tokens. That is
the one direction this telemetry must not be wrong in — it read as healthy
precisely when context was being dropped. `ContextInjectStats` already promised
this ("computed from the same observation set that was rendered"); the code now
matches its own contract. Summaries use `budget.config.sessionCount`, the same
slice `buildContextOutput` takes for `displaySummaries`.

Both live in a new exported `fitContextForDelivery`, split out of
`generateContextWithStats` so the fitting and the reporting can be tested
without a database — the module previously had no in-process test path at all,
which is why neither defect was caught.

**Bundles.** `plugin/scripts/worker-service.cjs` and
`plugin/scripts/context-generator.cjs` are rebuilt: marketplace hooks execute
the checked-in bundle, so the fix does not reach installs without them.

`mcp-server.cjs` and `server-service.cjs` are deliberately NOT included, and
that is measured rather than assumed. Rebuilding them from a clean tree
produces byte-identical output to rebuilding them with this patch applied, and
both differ from what is committed on `main` — so their diff is pre-existing
drift, not this change, and carrying it here would bury the review in noise.

Five cases in `tests/context/context-budget-3802.test.ts`, driven through
`fitContextForDelivery` rather than through `fitContextToBudget`, because both
defects were in what happens *around* the fitter.

The fixture is chosen so it can tell the two apart: 47 observations render to
9,900 characters, which fits, and 9,900 plus a 600-character warning does not.
An earlier version of this test used 50 observations and **passed under the
mutation** — the fitter's reductions are coarse, so the fitted block landed far
enough below the limit that a warning appended afterwards still fit. Measured,
then fixed.

- Moving the warning back outside the fitter fails
  `counts the observer-health warning against the budget`.
- Passing the original arrays to `buildInjectStats` fails two of the stats cases.

`bun test tests/context/` gives 83 pass / 4 fail; the four are
`context database ownership`, which fail identically on `main` (a child-process
stdout JSON parse, unrelated to this change). `bun run typecheck:root`,
`lint:hook-io` and `lint:spawn-env` are clean.

Co-authored-by: Alex Newman <thedotmack@users.noreply.github.com>

---------

Co-authored-by: Nguyen Thanh Dat <ntdat812@gmail.com>
Co-authored-by: Alex Newman <thedotmack@users.noreply.github.com>
2026-09-10 21:57:52 -07:00
..