compaction で古いツール呼び出しと結果を消し、発言は原文のまま残す。削減が足りなければ標準の要約に回す

Claude Codeのcompactionを、古いツール呼び出しの削除で済ませるプラグインです。会話の要約を作らないので、圧縮にかかる時間は数ミリ秒で、API費用もかかりません。ユーザーとアシスタントの発言は原文のまま残ります。
削除だけでは十分に小さくならない会話は、Claude Code標準のcompaction(LLMによる要約)にそのまま回します。
このプラグインは、ターンが終わるたびにcontext windowの使用率を確認します。使用率が60%以上なら、compactionを始める仕組みです。/compactを手で実行した場合も、同じ処理を通ります。
compactionでは、次の範囲のメッセージを残します。
この範囲より古いツール呼び出しは、その結果と一緒に削除します。呼び出しと結果は必ず組で扱います。結果だけを残すと、呼び出しの無い結果になり、APIがリクエストを拒否するためです。
削除によって会話の文字数が25%以上減った場合は、削除後の会話でそのまま続けます。25%に届かない場合は、標準compactionの要約に処理を渡します。
実際のセッションで試した例では、48,694 tokenの会話が9,953 tokenになり、所要時間は10msでした。
session.compactイベント)が使える版のClaude Codeが必要です置き場所はどこでも構いません。ここでは~/.claude/local-pluginsの下に置く例を示します。
mkdir -p ~/.claude/local-plugins
git clone https://github.com/okamyuji/tool-trim-compaction.git ~/.claude/local-plugins/tool-trim-compaction
cd ~/.claude/local-plugins/tool-trim-compaction
git checkout "$(git describe --tags --abbrev=0)"
mainへのcommitごとに、v0.1.1のような版のタグが自動で付きます。上の手順で取り出すのは、その時点で最新のタグです。
このプラグインは、Claude Codeの中で会話の内容を書き換えます。タグで版を決めておけば、後から入った変更を確かめないまま読み込むことはありません。新しい版に上げるときは、git fetch --tagsのあと変更内容を読んでから、git checkout <タグ>で切り替えてください。
~/.claude/settings.jsonのenvに、CLAUDE_CODE_PLUGIN_DIRSを追加します。すでにenvがある場合は、その中に1行を足してください。
{
"env": {
"CLAUDE_CODE_PLUGIN_DIRS": "~/.claude/local-plugins/tool-trim-compaction"
}
}
ほかにもディレクトリから読み込むプラグインがある場合は、パスを:でつないで並べます。
設定は、次に起動したセッションから有効になります。
1回だけ試したい場合は、設定を変えずに--plugin-dirで指定することもできます。
claude --plugin-dir ~/.claude/local-plugins/tool-trim-compaction
ツールを何度か使った会話で/compactを実行してください。削除で圧縮できた場合は、次のような行が表示されます。
tool-trim: dropped 10 tool calls, 82% reduction, kept 8/28 messages
削減率が25%に届かなかった場合は、次の行が表示されたあと、標準compactionの要約が動きます。
tool-trim: 16% reduction, below 25%; standard summary
プラグインの検証とテストは、次のコマンドで実行できます。
claude plugin validate ~/.claude/local-plugins/tool-trim-compaction
claude plugin test ~/.claude/local-plugins/tool-trim-compaction
残す件数、削減率の閾値、compactionを始める使用率は、hooks/register.tsの先頭にある定数で決めています。
| 定数 | 既定値 | 意味 |
|---|---|---|
KEEP_RECENT_MESSAGES | 6 | 削除の対象から外す直近のメッセージの件数 |
MIN_REDUCTION | 0.25 | この削減率に届かなければ標準の要約に回す |
COMPACT_AT_PERCENT | 60 | ターン終了時に、この使用率以上ならcompactionを始める |
削除したツール呼び出しの結果は、後から参照できなくなります。例えば、何十ターンも前に出たエラーメッセージの原文は文脈に残りません。エラーがあったこと自体が残らないので、モデルが「エラーは出ていない」と答える場合もあります。古いエラーの経緯を後でたどる作業では、標準compactionのほうが向いています。
session.compactイベントを使うほかのcompactionプラグインとは、同時に有効にしないでください。どちらの処理が先に動くかによって、結果が変わるためです。
~/.claude/settings.jsonのenvからCLAUDE_CODE_PLUGIN_DIRSの該当パスを消し、Claude Codeを起動し直します。そのあと、取得したディレクトリを削除してください。
hooks/register.ts 49 lines1import type { Register } from 'claude-code';
2import { trimToolHistory } from './trim';
3
4const KEEP_RECENT_MESSAGES = 6;
5const MIN_REDUCTION = 0.25;
6const COMPACT_AT_PERCENT = 60;
7
8const pct = (r: number) => `${Math.round(r * 100)}%`;
9
10export const register: Register = (on) => {
11 let compacting = false;
12
13 on('session.compact', async ($, e, next) => {
14 if (e.trigger === 'precompute') return next(e);
15 let out;
16 try {
17 out = trimToolHistory(e.messages, KEEP_RECENT_MESSAGES);
18 } catch (error) {
19 $.ui.log(`tool-trim: failed (${error instanceof Error ? error.message : String(error)}); standard summary`);
20 return next(e);
21 }
22 if (out.reduction < MIN_REDUCTION) {
23 $.ui.log(`tool-trim: ${pct(out.reduction)} reduction, below ${pct(MIN_REDUCTION)}; standard summary`);
24 return next(e);
25 }
26 const line = `tool-trim: dropped ${out.dropped} tool calls, ${pct(out.reduction)} reduction, kept ${out.messages.length}/${e.messages.length} messages`;
27 $.ui.log(line);
28 $.ui.toast(line, { timeoutMs: 15_000 });
29 return { messages: out.messages };
30 });
31
32 // 標準の自動 compaction より早い 60% で切る。要約を作らないので、早めに切っても費用が増えない。
33 on('turn.complete', async ($, e, next) => {
34 if (compacting) return next(e);
35 try {
36 const { context } = await $.session.usage();
37 if ((context.percent ?? 0) >= COMPACT_AT_PERCENT) {
38 compacting = true;
39 await $.session.compact();
40 }
41 } catch (error) {
42 $.ui.log(`tool-trim: auto-compact skipped (${error instanceof Error ? error.message : String(error)})`);
43 } finally {
44 compacting = false;
45 }
46 return next(e);
47 });
48};
49hooks/trim.ts 47 lines1import type { SessionMessage } from 'claude-code';
2
3export type TrimResult = {
4 messages: SessionMessage[];
5 dropped: number;
6 charsBefore: number;
7 charsAfter: number;
8 reduction: number;
9};
10
11function chars(m: SessionMessage): number {
12 let n = m.text.length;
13 for (const t of m.toolUses) n += JSON.stringify(t.input ?? {}).length + (t.text?.length ?? 0);
14 for (const r of m.toolResults ?? []) n += r.text.length;
15 return n;
16}
17
18// 先頭メッセージと直近 keepRecent 件にある呼び出しだけを残す。結果は呼び出しと組で扱い、
19// 呼び出しを消したら結果も消す(呼び出しの無い結果を API に渡すと拒否されるため)。
20export function trimToolHistory(input: readonly SessionMessage[], keepRecent: number): TrimResult {
21 const isPinned = (i: number) => i === 0 || i >= input.length - keepRecent;
22 const pinnedCalls = new Set<string>();
23 input.forEach((m, i) => {
24 if (isPinned(i)) for (const t of m.toolUses) pinnedCalls.add(t.tool_use_id);
25 });
26
27 let dropped = 0;
28 const messages: SessionMessage[] = [];
29 input.forEach((m) => {
30 const toolUses = m.toolUses.filter((t) => pinnedCalls.has(t.tool_use_id));
31 const toolResults = (m.toolResults ?? []).filter((r) => pinnedCalls.has(r.tool_use_id));
32 dropped += m.toolUses.length - toolUses.length;
33 if (toolUses.length === m.toolUses.length && toolResults.length === (m.toolResults ?? []).length) {
34 messages.push(m);
35 return;
36 }
37 if (!m.text.trim() && toolUses.length === 0 && toolResults.length === 0) return;
38 const rebuilt: SessionMessage = { role: m.role, text: m.text, toolUses };
39 if (toolResults.length > 0) rebuilt.toolResults = toolResults;
40 messages.push(rebuilt);
41 });
42
43 const charsBefore = input.reduce((n, m) => n + chars(m), 0);
44 const charsAfter = messages.reduce((n, m) => n + chars(m), 0);
45 return { messages, dropped, charsBefore, charsAfter, reduction: charsBefore ? 1 - charsAfter / charsBefore : 0 };
46}
47