compact แบบไม่เรียก model: /compact micro ตัดผล Read เก่าและ thinking ทิ้ง โครงบทสนทนาอยู่ครบ เสร็จทันที ไม่เสีย token

compact แบบไม่เรียก model: /compact ปกติให้ model อ่านทั้งบทสนทนาแล้วเขียนสรุปใหม่ ทั้งเสียเงิน เสียเวลา และรายละเอียดหาย แต่ส่วนที่ทำให้ context บวมจริงๆ คือผลของ Read ไฟล์ยาวๆ ที่อ่านไปนานแล้ว Mod นี้ตัดผลของ Read เก่าออก เหลือป้ายบอกว่าเคยอ่านไฟล์ไหน กี่บรรทัด และตัด thinking ของรอบเก่าทิ้ง โครงบทสนทนายังอยู่ครบ (ทุกคำสั่ง ทุกคำตอบ ทุก tool call) เสร็จในเสี้ยววินาทีและไม่เสีย token ข้อความ 8 อันล่าสุดไม่แตะ
| คำสั่ง | ทำอะไร |
|---|---|
/compact micro | ตัดผล Read เก่าและ thinking (ไม่เรียก model) |
/micro | เหมือนกัน |
/micro all | ตัดผลยาวของ Bash, Grep, Glob, WebFetch ด้วย |
/micro last | ผลครั้งล่าสุด |
/plugin install micro-compaction-th --marketplace tksoft11/claude-code-mods-th
ต้องตั้ง CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 ก่อนเปิด claude (ดู README หลัก)
เปิด hooks/register.tsx แล้วอ่านตามบทที่เขียนไว้ในคอมเมนต์:
handle คือของเดิม ข้อความที่ไม่มีคือประกอบใหม่ (ไม่มี thinking)session.compact เองโดยไม่เรียก next (core และ model ไม่ได้รันเลย)ทดสอบจริงแล้ว: context ลดจาก 40,853 เหลือ 5,129 tokens ใน 47 ms ถ้า Claude ต้องใช้เนื้อไฟล์อีกก็อ่านใหม่ได้ (ไฟล์อาจเปลี่ยนไปแล้วด้วย อ่านใหม่ยิ่งถูก)
hooks/register.tsx 116 lines1/**
2 * ============================================================================
3 * MOD 101: Micro Compaction TH - compact แบบไม่เรียก model (ฉบับสอนทำ)
4 * ============================================================================
5 * แรงบันดาลใจจาก: micro-compaction ใน awesome-claude-code-mods
6 *
7 * /compact ปกติให้ model อ่านทั้งบทสนทนาแล้วเขียนสรุปใหม่: เสียเงิน เสียเวลา และรายละเอียดหาย
8 * แต่ context ส่วนใหญ่ที่บวมจริงๆ คือ "ผลของ Read" ไฟล์ยาวๆ ที่อ่านไปนานแล้ว
9 *
10 * หน้าที่ของ Mod นี้:
11 * 1. /compact micro (หรือ /micro) ตัดผลของ Read เก่าออก เหลือป้ายบอกว่าเคยอ่านไฟล์ไหน กี่บรรทัด
12 * และตัด thinking ของรอบเก่าทิ้ง โดยไม่เรียก model เลย เสร็จทันที ไม่เสีย token
13 * 2. โครงบทสนทนาอยู่ครบ: ทุกคำสั่ง ทุกคำตอบ ทุก tool call ยังอยู่ Claude รู้ว่าเคยทำอะไร
14 * ถ้าต้องใช้เนื้อไฟล์อีกก็อ่านใหม่ได้ (ไฟล์อาจเปลี่ยนไปแล้วด้วย อ่านใหม่ยิ่งถูก)
15 * 3. /micro all ตัดผลยาวของ Bash, Grep, Glob, WebFetch ด้วย
16 * 4. ข้อความ 8 อันล่าสุดไม่แตะ งานที่ทำอยู่ตอนนี้จึงไม่สะดุด
17 */
18
19import type { Register, SessionMessage, ToolResultSummary, ToolUseSummary } from 'claude-code'
20
21const KEEP_RECENT = 8
22const LONG = 1_500
23const OTHER_TOOLS = new Set(['Bash', 'Grep', 'Glob', 'WebFetch', 'WebSearch', 'PowerShell'])
24
25let last = ''
26
27function chars(m: SessionMessage): number {
28 return m.text.length + m.toolUses.reduce((n, u) => n + JSON.stringify(u.input).length + (u.text?.length ?? 0), 0) + (m.toolResults ?? []).reduce((n, r) => n + r.text.length, 0)
29}
30
31// ==========================================================================
32// บทที่ 1: ตัดสินว่าผลของ tool ไหนควรตัด
33// ==========================================================================
34function placeholder(use: ToolUseSummary | undefined, text: string): string {
35 const lines = text.split('\n').length
36 if (use?.tool === 'Read') return `[elided by micro-compaction: Read ${String(use.input.file_path ?? '')} (${lines} lines). Re-read the file if you need its content.]`
37 return `[elided by micro-compaction: ${use?.tool ?? 'tool'} output (${lines} lines). Run it again if you need it.]`
38}
39
40function shouldElide(use: ToolUseSummary | undefined, text: string, all: boolean): boolean {
41 if (!use || text.startsWith('[elided by micro-compaction')) return false
42 if (use.tool === 'Read') return text.length > 200
43 return all && OTHER_TOOLS.has(use.tool) && text.length > LONG
44}
45
46// ==========================================================================
47// บทที่ 2: สร้างบทสนทนาใหม่
48// ข้อความที่มี handle = ของเดิมของ engine (ใช้ทั้งก้อน)
49// ข้อความที่ไม่มี handle = engine ประกอบใหม่จาก role, text, toolUses, toolResults (ไม่มี thinking)
50// ==========================================================================
51function micro(messages: readonly SessionMessage[], all: boolean): { messages: SessionMessage[]; elided: number; before: number; after: number } {
52 const uses = new Map<string, ToolUseSummary>()
53 for (const m of messages) for (const u of m.toolUses) uses.set(u.tool_use_id, u)
54 const cut = Math.max(0, messages.length - KEEP_RECENT)
55 let elided = 0
56 const out = messages.map((m, i): SessionMessage => {
57 if (i >= cut) return m
58 if (m.role === 'assistant') {
59 // ประกอบใหม่ = ทิ้ง thinking และตัดผลที่แนบมากับ tool_use ด้วย
60 const toolUses = m.toolUses.map(u => (u.text !== undefined && shouldElide(u, u.text, all) ? { ...u, result: undefined, text: placeholder(u, u.text) } : u))
61 return { role: m.role, text: m.text, toolUses }
62 }
63 const results = m.toolResults ?? []
64 if (!results.some(r => shouldElide(uses.get(r.tool_use_id), r.text, all))) return m
65 const toolResults = results.map((r): ToolResultSummary => {
66 const use = uses.get(r.tool_use_id)
67 if (!shouldElide(use, r.text, all)) return r
68 elided++
69 return { tool_use_id: r.tool_use_id, isError: r.isError, text: placeholder(use, r.text) }
70 })
71 return { role: m.role, text: m.text, toolUses: m.toolUses, toolResults }
72 })
73 return { messages: out, elided, before: messages.reduce((n, m) => n + chars(m), 0), after: out.reduce((n, m) => n + chars(m), 0) }
74}
75
76export const register: Register = on => {
77 on('session.start', async ($, e, next) => {
78 await $.command.register({ name: 'micro', description: 'compact แบบไม่เรียก model: ตัดผล Read เก่าและ thinking ทิ้ง (/micro all ตัดผล Bash/Grep ยาวด้วย)', argumentHint: 'all | last' })
79 return next(e)
80 })
81
82 // ==========================================================================
83 // บทที่ 3: รับ /compact micro แล้วตอบผล compaction เอง ไม่ส่งต่อให้ core
84 // ==========================================================================
85 on('session.compact', async ($, e, next) => {
86 const words = (e.instructions ?? '').trim().toLowerCase().split(/\s+/)
87 if (e.agentId || words[0] !== 'micro') return next(e)
88 const r = micro(e.messages, words[1] === 'all')
89 if (r.elided === 0 && r.messages.every((m, i) => m === e.messages[i])) {
90 last = '🗜 ไม่มีอะไรให้ตัด (ผล Read เก่าถูกตัดไปแล้ว หรือบทสนทนายังสั้น)'
91 return { skip: 'micro-compaction: ไม่มีผลของ tool เก่าให้ตัด' }
92 }
93 const saved = Math.round((r.before - r.after) / 4)
94 last = `🗜 micro compaction: ตัดผลของ tool ${r.elided} ก้อน และ thinking รอบเก่า · ประหยัดประมาณ ${saved.toLocaleString('en-US')} tokens · ไม่เรียก model`
95 $.ui.log(last)
96 return { messages: r.messages, tokensBefore: Math.round(r.before / 4), tokensAfter: Math.round(r.after / 4) }
97 })
98
99 on('command.run', { command: 'micro' }, async ($, e) => {
100 const arg = e.args.trim()
101 if (arg === 'last') return { text: last || 'ยังไม่เคย micro compact ใน session นี้' }
102 if (arg !== '' && arg !== 'all') return { text: '🗜 Micro Compaction TH (Mod 101)\n/micro ตัดผล Read เก่า + thinking (ไม่เรียก model)\n/micro all ตัดผลยาวของ Bash/Grep/Glob/WebFetch ด้วย\n/micro last ผลครั้งล่าสุด\nหรือพิมพ์ /compact micro ก็ได้' }
103 const r = await $.session.compact({ instructions: arg === 'all' ? 'micro all' : 'micro' }).catch((err: unknown) => ({ skip: `compact ไม่ได้ตอนนี้: ${String(err)}` }))
104 return { text: 'skip' in r && r.skip ? `⏸ ${r.skip}` : last }
105 })
106}
107
108/**
109 * ========================================================================
110 * การบ้าน / ต่อยอด
111 * ========================================================================
112 * 1. ตัดเฉพาะไฟล์ที่ถูกแก้ไปแล้ว (เนื้อเก่าใช้ไม่ได้อยู่แล้ว) ดูจาก Edit/Write หลังจากนั้น
113 * 2. ทำ micro ให้เองเมื่อ context ถึง 70% ก่อน engine จะ auto compact (ต่อกับ Mod 80)
114 * 3. เก็บผลที่ตัดไว้ในไฟล์ ให้ Claude เปิดดูได้โดยไม่ต้องอ่านไฟล์ต้นฉบับใหม่
115 */
116