SLOPSHOPPER

compact-keeper

Tells every compaction to keep the owner's decisions, approvals, rules and open asks word for word

toast
v0.1.0no licenseupdated 2026-10-18Tatendaz/T-mods/compact-keeper
A shopper browsing a rack in a slop shop
README

compact-keeper

Tells every compaction of the main conversation to keep your decisions word for word.

Before this, decisions got lost across /compact. In one builtby session you had to ask "can you save important details before I run compact", and every swarm worker prompt repeated "Owner merge approval (2026-09-26, still in force...)".

What it does

On session.compact it collects the prompts you typed: up to the last 60, each cut at 600 characters, with tool results and injected blocks left out. It adds them to the summarizer's instructions with these rules:

  • Keep an "Owner decisions" section that quotes every decision, approval, permission, rule, choice of option and open question.
  • Mark a reversed decision as replaced.
  • List what still waits on you under "Waiting on the owner".
  • Keep PR numbers, branches, SHAs, paths and commands exactly.

Text you typed after /compact stays first. Subagent compactions are left alone.

How it relates to compact-state.py

~/.claude/hooks/compact-state.py already saves git state and your last 8 prompts to a file, and prints the file back after compaction. This mod works one step earlier: it changes what the summary itself holds. The two do not conflict.

Usage cost

No extra model call. The compaction runs anyway. Its input grows by the quoted prompts, at most about 36,000 characters.

Tests

tests/compact-keeper.test.ts covers which messages count as yours, the instruction text, and a compaction receiving it. It has not been watched on a real /compact yet.

claude plugin test ~/.claude/mods/compact-keeper
Source 1 files
hooks/register.ts 45 lines
1import type { Register, SessionMessage } from 'claude-code'
2
3// Compaction drops the owner's exact words: approvals get paraphrased, open
4// questions vanish, and workers end up re-told "Owner merge approval (...),
5// still in force". This hands the summarizer the owner's own prompts and asks
6// it to keep every decision in them verbatim. It runs inside the compaction
7// that was going to run anyway, so it costs no extra model call.
8
9const MAX_PROMPTS = 60
10const MAX_CHARS = 600
11
12/** The prompts the person typed, oldest first: no tool results, no injected blocks. */
13export function ownerPrompts(messages: readonly SessionMessage[]): string[] {
14  return messages
15    .filter(m => m.role === 'user' && (m.toolResults ?? []).length === 0)
16    .map(m => m.text.trim())
17    .filter(t => t.length > 0 && !t.startsWith('<') && !t.startsWith('This session is being continued'))
18    .slice(-MAX_PROMPTS)
19    .map(t => (t.length > MAX_CHARS ? `${t.slice(0, MAX_CHARS)} [cut]` : t))
20}
21
22export function keepInstructions(prompts: readonly string[], earlier?: string): string {
23  const rules = [
24    'Keep a section named "Owner decisions" in the summary.',
25    'In it, quote word for word every decision, approval, permission, rule, choice of option and open question from the owner\'s messages below, each with its date when the transcript shows one.',
26    'Mark a decision the owner later reversed as replaced, and keep the newer one.',
27    'List what still waits on the owner under "Waiting on the owner".',
28    'Keep PR numbers, branch names, commit SHAs, file paths and commands exactly as written.',
29  ]
30  const quoted = prompts.map((p, i) => `${i + 1}. ${p.replace(/\n+/g, ' ')}`).join('\n')
31  return [earlier?.trim(), rules.join(' '), prompts.length > 0 ? `The owner's messages, oldest first:\n${quoted}` : '']
32    .filter(Boolean)
33    .join('\n\n')
34}
35
36export const register: Register = on => {
37  on('session.compact', async ($, e, next) => {
38    if (e.agentId !== undefined) return next(e)
39    const prompts = ownerPrompts(e.messages)
40    const result = await next({ ...e, instructions: keepInstructions(prompts, e.instructions) })
41    if (!('skip' in result)) $.ui.toast(`compact-keeper: asked the summary to keep ${prompts.length} owner messages' decisions verbatim`)
42    return result
43  })
44}
45