# Clavue v8.13.7 — Compaction Fixes & PinnedContext on Hot Paths

**Theme:** /compact stops firing too often, and the LLM compactor / SessionMemory maintainer / microcompact cleaner now see the live working set instead of inventing it from transcript turn-counting.

---

## TL;DR

Six changes, one root cause family — context-recovery paths were operating on stale or partial information:

| # | Area | Symptom users saw |
|---|------|-------------------|
| 1 | autocompact | `/compact` triggering at well under heap pressure on 12GB-heap installs |
| 2 | autocompact | `critical` band tripping at 80%, blocking edits before genuine OOM territory |
| 3 | emergency truncate | Single 60s wall-clock gate firing twice in a row when heap was actually flat |
| 4 | LLM full-compact prompt | Summarizer dropped active plan / todos / recent constraints — random-quality outputs |
| 5 | SessionMemory updater | Notes file's "Current State" drifted away from the real plan and todos every extraction |
| 6 | microcompact (this release) | `[Old tool result content cleared]` for tool calls the user just asked about |

Three of the six (4 / 5 / 6) share a single new substrate: the existing `pinnedContext` snapshot is now threaded into every LLM-facing recovery path, with the same sentinel string (`PINNED CONTEXT — MUST PRESERVE VERBATIM`) so the model treats them as one contract.

---

## Fix 1 — autocompact heap-ratio cap

`src/services/compact/autoCompact.ts`

The ratio against `--max-old-space-size` was being silently floored by an absolute MB threshold. On the new default heap (`DEFAULT_CLAVUE_MAX_OLD_SPACE_SIZE_MB = 12288`), that absolute floor was tripping at ~6GB — half the heap — and forcing autocompact when the process had plenty of room.

Now: ratio-based, capped at `0.78` of `--max-old-space-size`. No absolute MB knob. On 12GB heaps, autocompact starts at ~9.6GB instead of ~6GB.

## Fix 2 — critical band 0.8 → 0.9

`src/services/compact/autoCompact.ts`

The "critical" band (where edits hard-block) was `0.8` of heap. On 12GB heap that's 9.6GB — well below genuine pressure. Bumped to `0.9` (10.8GB), with `hard-block = 0.95` (11.4GB). Autocompact's `0.78` cap stays safely below `critical`, so we never enter critical from a clean state.

## Fix 3 — emergency truncate dual gate

`src/services/compact/emergencyTruncate.ts`

Pre-fix: 60s wall-clock gate alone. If heap was flat at 95% for 70s, we'd emergency-truncate, then 60s later truncate again — same RSS, same data, second pass throwing away tool_results we just kept.

Now: 60s wall-clock **AND** 10% measured heap drop since last fire. Both required. Sustained-pressure cycles can't double-fire on flat heap.

## Fix 4 — LLM full-compact + partial-compact prompts see pinnedContext

`src/services/compact/prompt.ts`, `src/services/compact/compact.ts`

Pre-fix, only the emergency (no-LLM) path consumed `pinnedContext` and the micro-reflection JSONL. The two LLM compact paths summarized the conversation with no "must keep verbatim" guidance.

Now: `buildPinnedContextAddendum(pinned)` exists, emits `PINNED CONTEXT — MUST PRESERVE VERBATIM`, and is wired into both `getCompactPrompt` and `getPartialCompactPrompt`. Empty pinned → empty string (cache-stable; no prompt-cache key churn for sessions without a plan).

Tests: `tests/compact-pinned-injection.test.mjs` (8/8).

## Fix 5 — SessionMemory update prompt sees pinnedContext

`src/services/SessionMemory/sessionMemory.ts`, `src/services/SessionMemory/prompts.ts`

The forked agent that maintains the SessionMemory `.md` file was getting the conversation, the current notes, and a strict edit template — but no pinned snapshot. "Current State" drifted; `do not mock the DB`-style constraints were rediscovered (or lost) every extraction.

Now: `buildSessionMemoryUpdatePrompt(currentNotes, notesPath, { pinned })` splices a `PINNED CONTEXT` block above the existing substitution, with explicit guidance about which SM section each pin lands in (Current State / Task specification / Errors & Corrections / Workflow / Learnings / Worklog). Both extraction entrypoints (post-sampling hook + `/summary`) thread pinned through.

Tests: `tests/sm-pinned-injection.test.mjs` (7/7).

## Fix 6 — microcompact respects the pinned working set (this release)

`src/services/pinnedContext/pinnedContextCore.ts`, `src/services/compact/microCompact.ts`, `src/query.ts`

The two microcompact paths — cached (cache-edit deletion) and time-based (content-clear) — used `collectCompactableToolIds(messages)` blindly. A `Read` result the user just asked about (`show me what src/foo.ts had again`) could be cleared mid-turn, leaving the model with `[Old tool result content cleared]` for a tool call the live `pinnedContext` was still pointing at.

**Substrate:** `pinnedContextCore.ts` now exports

```
derivePinnedToolUseIds(messages, pinned): Set<string>
```

Heuristic — and intentionally so: parsing plan markdown / todo bodies for `tool_use_id` substrings is brittle. The reliable signal is the working set — every `tool_use` issued in assistant turns answering the recent user requests window. Empty `recentUserRequests` → empty Set (cache-stable no-op).

**Wiring:** `microcompactMessages` accepts a fourth optional `skipToolUseIds: ReadonlySet<string>`. Both internal paths filter `collectCompactableToolIds` by it (single source of truth). `query.ts` derives the snapshot synchronously (`buildPinnedContextSync` — no disk I/O on the hot per-turn path) and passes it through.

Tests: `tests/microcompact-pinned-skip.test.mjs` (9/9, includes one real unit test for the pure helper).

---

## Verification

- 9/9 new tests pass
- Full suite: 1590 pass / 6 skip / 0 fail (baseline 1581 → 1590 with the 9 new tests; zero regression)
- `npx tsc --noEmit -p .`: no new type errors related to the changed files (pre-existing `@types/node` warning unchanged)

## Removed

- `src/services/compact/compactionDispatch.ts` and its test — superseded by the pinnedContext threading. No callers.

## Cache-stability invariant (still holds)

All three pinned-injection paths — full-compact prompt, SM update prompt, microcompact skip set — early-return empty when the pinned snapshot has no plan / no todos / no recent user requests. Sessions without a working set hit the same prompt-cache key as before this release.

## Files changed

```
src/constants/branding.ts                                 (version bump)
package.json                                              (version bump)
src/query.ts                                              (PR3 wiring)
src/services/compact/microCompact.ts                      (PR3 skip set)
src/services/compact/compact.ts                           (PR1 wiring)
src/services/compact/prompt.ts                            (PR1 addendum)
src/services/compact/autoCompact.ts                       (fix 1, 2)
src/services/compact/emergencyTruncate.ts                 (fix 3)
src/services/SessionMemory/sessionMemory.ts               (PR2 wiring)
src/services/SessionMemory/prompts.ts                     (PR2 addendum)
src/services/pinnedContext/pinnedContextCore.ts           (derivePinnedToolUseIds)
tests/compact-pinned-injection.test.mjs                   (new, 8 tests)
tests/sm-pinned-injection.test.mjs                        (new, 7 tests)
tests/microcompact-pinned-skip.test.mjs                   (new, 9 tests)
```
