Claude Code (v2.1.19)

编译产物分析

下载 cc node.js 编译后的产物:

npm pack @anthropic-ai/claude-code
tar -xzf anthropic-ai-claude-code-*.tgz

产物目录结构:

package/
├── LICENSE.md          (147B)     - 许可证文件
├── README.md           (2.0KB)    - 说明文档
├── package.json        (1.2KB)    - 包配置文件
├── cli.js              (11.6MB)   - 主要 CLI 入口文件(打包后的代码)
├── sdk-tools.d.ts      (67.7KB)   - TypeScript 类型定义文件
├── bun.lock            (551B)     - Bun 锁文件
├── resvg.wasm          (2.5MB)    - SVG 渲染 WebAssembly 模块
├── tree-sitter.wasm    (205KB)    - Tree-sitter 解析器核心
├── tree-sitter-bash.wasm (1.4MB)  - Bash 语法解析器
└── vendor/ripgrep/                - ripgrep 搜索工具(多平台二进制)
    ├── arm64-darwin/              - macOS ARM64 (M1/M2)
    ├── arm64-linux/               - Linux ARM64
    ├── x64-darwin/                - macOS x64
    ├── x64-linux/                 - Linux x64
    ├── x64-win32/                 - Windows x64
    └── COPYING                    - ripgrep 许可证


#!/usr/bin/env node
// (c) Anthropic PBC. All rights reserved. Use is subject to the Legal Agreements outlined here: https://code.claude.com/docs/en/legal-and-compliance.

// Version: 2.1.19

// Want to see the unminified source? We're hiring!
// https://job-boards.greenhouse.io/anthropic/jobs/4816199008

主要逻辑都在 cli.js 文件中,cli.js 是编译混淆过的代码。

压缩触发场景

Manual compact

┌─────────────────────────────────────────────────────────────────┐
│                      /compact 命令流程                           │
├─────────────────────────────────────────────────────────────────┤
│  1. 用户输入 /compact                                            │
│                           ↓                                      │
│  2. 设置 UI 状态: "Compacting conversation..."                   │
│                           ↓                                      │
│  3. 调用 AI 生成对话摘要 (Summary)                               │
│                           ↓                                      │
│  4. 用摘要替换原始对话历史                                        │
│                           ↓                                      │
│  5. 插入 compact_boundary 标记                                   │
│                           ↓                                      │
│  6. 记录 token 使用统计                                          │
│                           ↓                                      │
│  7. 显示 "Conversation compacted" 完成提示                       │
└─────────────────────────────────────────────────────────────────┘

Auto compact

var RI6 = 13000;   // autoCompact 预留空间
var IW2 = 20000;   // warningThreshold 预留空间
var SW2 = 20000;   // errorThreshold 预留空间 (与 warning 相同)
var yI6 = 3000;    // blockingLimit 预留空间

// 窗口计算大小还会依赖 DISABLE_AUTO_COMPACT、CLAUDE_CODE_MAX_OUTPUT_TOKENS 等环境变量配置
// 下面是假设都是默认配置

ContextWindow = 200,000 tokens
AvailableWindow = ContextWindow - OutputReserved(32K) = 178,000 tokens
AutoCompactThreshold = AvailableWindow - 13,000 = 155,000 tokens
有效空间 = rp() ? AutoCompactThreshold : AvailableWindow = 155,000 tokens
WarningThreshold = 有效空间 - 20,000 = 135,000 tokens
BlockingLimit = ContextWindow - 3,000 = 197,000 tokens

image.png

环境变量 说明
CLAUDE_AUTOCOMPACT_PCT_OVERRIDE 设置 Auto Compact 触发的百分比 (1-100)
CLAUDE_CODE_BLOCKING_LIMIT_OVERRIDE 覆盖阻塞限制的 token 数
DISABLE_COMPACT 禁用所有 compact 功能
DISABLE_AUTO_COMPACT 仅禁用 auto-compact
CLAUDE_CODE_MAX_OUTPUT_TOKENS 获取模型的默认最大输出 token 数
Context left until auto-compact: 0%
❯ /context
  ⎿   Context Usage
     ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛁   claude-haiku-4-5-20251001 · 172k/200k tokens (86%)
     ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛁
     ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛁   Estimated usage by category
     ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛁   ⛁ System prompt: 2.2k tokens (1.1%)
     ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛁   ⛁ System tools: 16.6k tokens (8.3%)
     ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛁   ⛁ Messages: 156.1k tokens (78.1%)
     ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛁   ⛝ Autocompact buffer: 45.0k tokens (22.5%)
     ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛁
     ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛝ ⛝ ⛝
     ⛝ ⛝ ⛝ ⛝ ⛝ ⛝ ⛝ ⛝ ⛝ ⛝  Context Usage
     ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛁   claude-haiku-4-5-20251001 · 172k/200k tokens (86%)
     ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛁
     ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛁   Estimated usage by category
     ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛁   ⛁ System prompt: 2.2k tokens (1.1%)
     ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛁   ⛁ System tools: 16.6k tokens (8.3%)
     ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛁   ⛁ Messages: 156.1k tokens (78.1%)
     ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛁   ⛝ Autocompact buffer: 45.0k tokens (22.5%)
     ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛁
     ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛁ ⛝ ⛝ ⛝
     ⛝ ⛝ ⛝ ⛝ ⛝ ⛝ ⛝ ⛝ ⛝ ⛝

Microcompact (消息发送前压缩)

  • 执行时机: 每次查询前
  • 流程: 用户输入 → Context Loading → Microcompact → Autocompact → Query Setup → API 请求
  • 环境变量: DISABLE_MICROCOMPACT 控制是否禁用 Microcompact


var BW2 = 20000;   // 触发阈值:需要节省至少 20000 tokens 才会执行压缩 
var uW2 = 40000;   // 目标 token 数:压缩后保留的最大 token 数 
var mW2 = 3;       // 保护最近 3 个工具调用不被压缩 
var GqK = 2000;    // 图片的 token 估算值 

// 可以被压缩的工具列表 
var gW2 = new Set([ 
  C5,   // Read (文件读取) 
  I7,   // Bash (命令执行) 
  ow,   // Grep (文本搜索) 
  uH,   // Glob (文件模式匹配) 
  gE,   // WebSearch (网络搜索) 
  z0,   // WebFetch (网页内容获取) 
  c5,   // Edit (文件编辑) 
  x2    // Write (文件写入) 
]);

async function gp(A, K, q) {
  // 1. 检查是否禁用 microcompact
  if (E1(process.env.DISABLE_MICROCOMPACT) || lK("tengu_cache_plum_violet", false)) {
    return { messages: A };
  }
  
  // 2. 确定目标 token 数
  let z = K !== undefined ? K : uW2;  // 默认 40000
  
  // 3. 收集可压缩的工具调用
  let w = [];  // 工具调用 ID 列表
  let H = new Map();  // 工具 ID -> token 数
  
  for (let W of A) {
    // 遍历消息中的 tool_use 和 tool_result
    if (D.type === "tool_use" && gW2.has(D.name)) {
      // 如果工具在可压缩列表中
      w.push(D.id);
    } else if (D.type === "tool_result") {
      // 计算 tool_result 的 token 数
      let j = FW2(D.tool_use_id, D);
      H.set(D.tool_use_id, j);
    }
  }
  
  // 4. 保护最近 3 个工具调用
  let J = w.slice(-mW2);  // 最近 3 个不压缩
  
  // 5. 计算需要压缩的工具
  let $ = new Set();  // 需要压缩的工具 ID
  for (let W of w) {
    if (J.includes(W)) continue;  // 跳过最近 3 个
    if (X - O > z) {
      $.add(W);
      O += H.get(W) || 0;
    }
  }
  
  // 6. 检查是否值得压缩
  if (!Y) {
    let W = zP(A);  // 计算总 token 数
    if (!mp(W).isAboveWarningThreshold || O < BW2) {
      $.clear();  // 如果节省不到 20000 tokens,不压缩
    }
  }
  
  // 7. 执行压缩 - 替换 tool_result 内容
  for (let W of A) {
    if (M.type === "tool_result" && _(M.tool_use_id)) {
      // 将内容保存到文件
      let V = await sKA(M.content, M.tool_use_id);
      // 替换为简短提示
      P = `${TP1}Tool result saved to: ${V.filepath}
Use ${C5} to view${xI6}`;
    }
  }
  
  // 8. 记录遥测数据
  if ($.size > 0) {
    n("tengu_microcompact", {
      toolsCompacted: $.size,
      totalUncompactedTokens: X,
      tokensAfterCompaction: X - O,
      tokensSaved: O,
      triggerType: Y ? "manual" : "auto"
    });
  }
}

工具输出裁剪

┌─────────────────────────────────────────────────────────────────┐
│                    Tool-Results 即时压缩流程                      │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  工具执行完成                                                     │
│       ↓                                                         │
│  jM1(tool, result, tool_use_id)                                │
│       ↓                                                         │
│  mapToolResultToToolResultBlockParam() 转换格式                  │
│       ↓                                                         │
│  ID2(toolResult, toolName, maxSize)                            │
│       ↓                                                         │
│  ┌─────────────────────────────────────┐                       │
│  │ 检查是否需要压缩:                      │                       │
│  │ 1. 内容为空? → 返回原内容              │                       │
│  │ 2. 包含图片? → 返回原内容              │                       │
│  │ 3. 长度 <= maxSize? → 返回原内容       │                       │
│  │ 4. 长度 > maxSize → 执行压缩          │                       │
│  └─────────────────────────────────────┘                       │
│       ↓ (需要压缩)                                               │
│  sKA(content, tool_use_id)                                     │
│       ↓                                                         │
│  ┌─────────────────────────────────────┐                       │
│  │ 1. 创建目录 tool-results/             │                       │
│  │ 2. 确定文件类型 (.txt 或 .json)        │                       │
│  │ 3. 写入文件                           │                       │
│  │ 4. 生成预览 (前 2KB)                   │                       │
│  └─────────────────────────────────────┘                       │
│       ↓                                                         │
│  yD2(persistResult)                                            │
│       ↓                                                         │
│  ┌─────────────────────────────────────┐                       │
│  │ 生成格式化消息:                        │                       │
│  │ <persisted-output>                   │                       │
│  │ Output too large (150 KB)...         │                       │
│  │ Preview (first 2 KB):                │                       │
│  │ [预览内容]                            │                       │
│  │ ...                                  │                       │
│  │ </persisted-output>                  │                       │
│  └─────────────────────────────────────┘                       │
│       ↓                                                         │
│  发送遥测 tengu_tool_result_persisted                           │
│       ↓                                                         │
│  返回 { ...toolResult, content: 占位符消息 }                     │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

消息压缩

压缩前给模型 Prompt 消息

{
  ... // 之前的上下文消息
  "role": "user",
  "content": [
    ... // 之前的上下文消息
    {
      "type": "text",
      "text": "{{$Prompt}}",
      "cache_control": {
        "type": "ephemeral"
      }
    }
  ]
}

Prompt

Your task is to create a detailed summary of the conversation so far, paying close attention to the user's explicit requests and your previous actions.
This summary should be thorough in capturing technical details, code patterns, and architectural decisions that would be essential for continuing development work without losing context.

Before providing your final summary, wrap your analysis in <analysis> tags to organize your thoughts and ensure you've covered all necessary points. In your analysis process:

1. Chronologically analyze each message and section of the conversation. For each section thoroughly identify:
   - The user's explicit requests and intents
   - Your approach to addressing the user's requests
   - Key decisions, technical concepts and code patterns
   - Specific details like:
     - file names
     - full code snippets
     - function signatures
     - file edits
  - Errors that you ran into and how you fixed them
  - Pay special attention to specific user feedback that you received, especially if the user told you to do something differently.
2. Double-check for technical accuracy and completeness, addressing each required element thoroughly.

Your summary should include the following sections:

1. Primary Request and Intent: Capture all of the user's explicit requests and intents in detail
2. Key Technical Concepts: List all important technical concepts, technologies, and frameworks discussed.
3. Files and Code Sections: Enumerate specific files and code sections examined, modified, or created. Pay special attention to the most recent messages and include full code snippets where applicable and include a summary of why this file read or edit is important.
4. Errors and fixes: List all errors that you ran into, and how you fixed them. Pay special attention to specific user feedback that you received, especially if the user told you to do something differently.
5. Problem Solving: Document problems solved and any ongoing troubleshooting efforts.
6. All user messages: List ALL user messages that are not tool results. These are critical for understanding the users' feedback and changing intent.
6. Pending Tasks: Outline any pending tasks that you have explicitly been asked to work on.
7. Current Work: Describe in detail precisely what was being worked on immediately before this summary request, paying special attention to the most recent messages from both user and assistant. Include file names and code snippets where applicable.
8. Optional Next Step: List the next step that you will take that is related to the most recent work you were doing. IMPORTANT: ensure that this step is DIRECTLY in line with the user's most recent explicit requests, and the task you were working on immediately before this summary request. If your last task was concluded, then only list next steps if they are explicitly in line with the users request. Do not start on tangential requests or really old requests that were already completed without confirming with the user first.
                       If there is a next step, include direct quotes from the most recent conversation showing exactly what task you were working on and where you left off. This should be verbatim to ensure there's no drift in task interpretation.

Here's an example of how your output should be structured:

<example>
<analysis>
[Your thought process, ensuring all points are covered thoroughly and accurately]
</analysis>

<summary>
1. Primary Request and Intent:
   [Detailed description]

2. Key Technical Concepts:
   - [Concept 1]
   - [Concept 2]
   - [...]

3. Files and Code Sections:
   - [File Name 1]
      - [Summary of why this file is important]
      - [Summary of the changes made to this file, if any]
      - [Important Code Snippet]
   - [File Name 2]
      - [Important Code Snippet]
   - [...]

4. Errors and fixes:
    - [Detailed description of error 1]:
      - [How you fixed the error]
      - [User feedback on the error if any]
    - [...]

5. Problem Solving:
   [Description of solved problems and ongoing troubleshooting]

6. All user messages: 
    - [Detailed non tool use user message]
    - [...]

7. Pending Tasks:
   - [Task 1]
   - [Task 2]
   - [...]

8. Current Work:
   [Precise description of current work]

9. Optional Next Step:
   [Optional Next step to take]

</summary>
</example>

Please provide your summary based on the conversation so far, following this structure and ensuring precision and thoroughness in your response. 

There may be additional summarization instructions provided in the included context. If so, remember to follow these instructions when creating the above summary. Examples of instructions include:
<example>
## Compact Instructions
When summarizing the conversation focus on typescript code changes and also remember the mistakes you made and how you fixed them.
</example>

<example>
# Summary instructions
When you are using compact - please focus on test output and code changes. Include file reads verbatim.
</example>

压缩后消息内容

This session is being continued from a previous conversation that ran outof context. The summary below covers the earlier portion of the conversation.

Analysis:
{{分析内容}}

Summary:
1. Primary Request and Intent:
// {{内容}} 没有相关的话,模型会写 N/A 

2. Key Technical Concepts:
// {{内容}} 没有相关的话,模型会写 N/A 

3. Files and Code Sections:
// {{内容}} 没有相关的话,模型会写 N/A 

4. Errors and fixes:
// {{内容}} 没有相关的话,模型会写 N/A 

6. All user messages:
// {{内容}} 没有相关的话,模型会写 N/A 

7. Pending Tasks:
// {{内容}} 没有相关的话,模型会写 N/A 

8. Current Work:
// {{内容}} 没有相关的话,模型会写 N/A 

9. Optional Next Step:
// {{内容}} 没有相关的话,模型会写 N/A 

If you need specific details from before compaction (like exact code snippets, error messages, or content you generated), read the full transcript at: /Users/fanlv/.claude/projects/-Users-fanlv-Desktop-test/69d56a2c-d9a9-433f-9832-a1d2174594ae.jsonl


// 自动触发的时候,才会有后面这段话
Please continue the conversation from where we left it off without askingthe user any further questions. Continue with the last task that you wereasked to work on.

测试

第一次压缩

压缩前

完整的请求数据:auto_compact.json

压缩后的产物

"This session is being continued from a previous conversation that ran out of context. The summary below covers the earlier portion of the conversation.

Analysis:
The user has asked a simple straightforward question: "一共多少个章节?" which translates to "How many chapters in total?"

Looking at the context:
1. The user previously provided a 40-chapter summary of a Chinese web novel called "元龙" (Yuan Long)
2. I provided a detailed summary of those 40 chapters
3. The user is now asking how many total chapters exist

The context window shows I'm at 86% capacity (172k/156.1k tokens used). The user's current request is just asking for a chapter count - this is a simple factual question about the material they provided.

From the text provided, the highest chapter number mentioned is Chapter 40 (第40章), which appears to be the final chapter in the excerpt they gave me. However, the user is asking how many chapters exist in total for the entire work, which I cannot determine from the material provided - I only know about chapters 1-40.

This is a straightforward informational question that doesn't require code analysis or technical summary.

Summary:
1. Primary Request and Intent:
   The user is asking how many total chapters exist in the "元龙" (Yuan Long) web novel.

2. Key Technical Concepts:
   - N/A (This is a straightforward informational question, not a technical request)

3. Files and Code Sections:
   - N/A

4. Errors and fixes:
   - N/A

5. Problem Solving:
   - N/A

6. All user messages:
   - "你好" (Hello)
   - "今天天气怎么样?" (What's the weather like today?)
   - [Extended excerpt from Chapters 1-40 of "元龙" with request to summarize]
   - "一共多少个章节?" (How many chapters in total?)

7. Pending Tasks:
   - Clarify the total chapter count for "元龙"

8. Current Work:
   User provided 40 chapters of "元龙" (Yuan Long) and I created a comprehensive summary. Now user is asking about the total chapter count.

9. Optional Next Step:
   Clarify that the material provided contains chapters 1-40, but I cannot determine the total number of chapters in the complete work without additional information. Ask the user if they have information about the complete work's chapter count, or if they only want to know about the 40 chapters they've shared.

---

## Answer to Your Question:

Based on the text you provided, I can confirm there are **至少40个章节** (at least 40 chapters), as the excerpt you shared goes from Chapter 1 through Chapter 40.

However, I cannot determine the **total number of chapters** for the complete work from this material alone. 

If you're asking about:
- **The chapters you provided**: 40 chapters total
- **The complete "元龙" novel**: I would need additional information to tell you the exact total

Could you clarify if you're asking about:
1. Just the 40 chapters in the text you shared?
2. The total chapter count of the entire published novel?

If you need specific details from before compaction (like exact code snippets, error messages, or content you generated), read the full transcript at: /Users/fanlv/.claude/projects/-Users-fanlv-Desktop-test/69d56a2c-d9a9-433f-9832-a1d2174594ae.jsonl
Please continue the conversation from where we left it off without asking the user any further questions. Continue with the last task that you were asked to work on."

完整的内容 dump 到了文件中:/Users/fanlv/.claude/projects/-Users-fanlv-Desktop-test/69d56a2c-d9a9-433f-9832-a1d2174594ae.jsonl

完全的请求数据:auto_compact_after.json

第一次让模型总结我之前的问题,模型能够准确说出我问题了:

"根据之前对话的总结,你提供了以下内容:

1. **打招呼** - 你用中文跟我打招呼("你好")
2. **问天气** - 你问了"今天天气怎么样?"
3. **提供小说内容** - 你提供了一部中文网络小说《**元龙**》的40个章节的详细内容,并请我进行总结
4. **询问章节数** - 你问"一共多少个章节?"

我根据你提供的40个章节内容为你创建了一份详细的小说总结。

现在你有什么新的问题或任务需要我帮助吗?"
第二次压缩

压缩前

完整请求数据:auto_compact_second_time.json

压缩后

"This session is being continued from a previous conversation that ran out of context. The summary below covers the earlier portion of the conversation.

Analysis:
Let me chronologically analyze this conversation:

1. **Initial Context**: The user's previous conversation ran out of context. A summary was provided indicating:
   - The user had previously asked "一共多少个章节?" (How many chapters in total?)
   - They had provided a 40-chapter summary of a Chinese web novel called "元龙" (Yuan Long)
   - The highest chapter number mentioned was Chapter 40

2. **Current Conversation Flow**:
   - I was instructed to continue from where the previous conversation left off without asking further questions
   - The user asked: "我之前说了哪些内容?" (What did I say before?)
   - I provided a recap based on the summary provided
   - The user then asked: "元龙 第二章说了什么?" (What did the second chapter of Yuan Long say?)

3. **My Response to Latest Request**:
   - I attempted to access the previous conversation file stored at `/Users/fanlv/.claude/projects/-Users-fanlv-Desktop-test/69d56a2c-d9a9-433f-9832-a1d2174594ae.jsonl`
   - The file was too large (429.9KB / 188846 tokens), requiring offset and limit parameters
   - I used Bash commands with `jq` and `grep` to extract Chapter 2 content
   - Successfully retrieved and displayed the complete Chapter 2: "奇怪的能力" (Strange Abilities)

4. **Technical Details**:
   - File access challenges due to size constraints
   - Successfully used `jq -r '.message.content'` with grep to parse JSON and extract specific chapter content
   - Chapter 2 content was fully extracted and summarized

5. **No Errors or Fixes Needed**:
   - Initial file read attempt failed due to size
   - Switching to Bash command-line tools was successful
   - No user feedback indicating any issues with the approach or content

6. **Current Status**:
   - Successfully provided detailed summary of Chapter 2
   - No pending work explicitly requested by user
   - The conversation is informational - user is asking questions about the novel content

Summary:
1. Primary Request and Intent:
   - The user continued a previous conversation about the Chinese web novel "元龙" (Yuan Long)
   - After receiving context summary, the user asked: "我之前说了哪些内容?" (What content did I mention before?) - seeking a recap
   - The user then explicitly requested: "元龙 第二章说了什么?" (What did Chapter 2 of Yuan Long say?) - asking for detailed information about Chapter 2 content

2. Key Technical Concepts:
   - Chinese web novel analysis and summarization
   - Large JSON file handling and parsing
   - Context continuation from previous conversation session
   - File size constraints in LLM context windows (200k token budget with current usage at 9%)

3. Files and Code Sections:
   - `/Users/fanlv/.claude/projects/-Users-fanlv-Desktop-test/69d56a2c-d9a9-433f-9832-a1d2174594ae.jsonl`
      - This is the persistent conversation history file containing the full transcript of the previous conversation
      - File size: 429.9KB (188846 tokens)
      - Contains complete text of all 40 chapters of "元龙" (Yuan Long) web novel
      - Successfully accessed using: `jq -r '.message.content' /Users/fanlv/.claude/projects/-Users-fanlv-Desktop-test/69d56a2c-d9a9-433f-9832-a1d2174594ae.jsonl | grep -A 50 "第2章"`

4. Errors and fixes:
   - **Error 1 - Initial file read failure**: 
     - `Read` tool reported file content (429.9KB) exceeds maximum allowed size (256KB)
     - **Fix**: Switched to Bash command-line tools with size-aware parameters
   - **Error 2 - Token limit exceeded**:
     - First `Read` tool attempt reported 188846 tokens exceeds 25000 token limit
     - **Fix**: Used `jq` with `grep` piping in Bash to extract only relevant content without loading entire file
   - **No user feedback on errors** - the Bash approach worked successfully on first attempt

5. Problem Solving:
   - Successfully navigated file size constraints using appropriate tools
   - Extracted specific chapter content from large JSON conversation history without exceeding token limits
   - Provided comprehensive Chapter 2 summary including plot points, character development, and key abilities revealed

6. All user messages:
   - "我之前说了哪些内容?" (What content did I mention before?)
   - "元龙 第二章说了什么?" (What did Chapter 2 of Yuan Long say?)

7. Pending Tasks:
   - None explicitly requested. User appears to be exploring and asking questions about the novel content.

8. Current Work:
   - Just completed providing a detailed summary of **Chapter 2: "奇怪的能力" (Strange Abilities)** containing:
     - King (王胜/王牌狙击手) demonstrates 0.4-second gun draw speed and kills Dai Huan
     - Discovery of new ability: King can precisely analyze and predict combat movements and sword techniques
     - Realization that this ability emerged after arriving in this world, possibly from the mysterious dream girl
     - Song Yan and Elder Fish note King's ability to critique Song Yan's swordsmanship (a secret Song family technique)
     - King searches fallen bodies for valuables
     - Chapter ends with King questioning why Song Yan's group didn't act earlier to prevent Dai Huan from stealing the 9-star elemental spirit
   - The summary provided all major plot points, character interactions, and revealed King's mysterious new combat analysis ability

9. Optional Next Step:
   - Wait for user's next question about the novel. User may ask about:
     - Chapter 3 or subsequent chapters content
     - Character backstories or relationships
     - Plot summaries of other chapters
     - Or may have a completely different request
   - Currently no explicit next step since user is asking exploratory questions rather than requesting specific work

If you need specific details from before compaction (like exact code snippets, error messages, or content you generated), read the full transcript at: /Users/fanlv/.claude/projects/-Users-fanlv-Desktop-test/69d56a2c-d9a9-433f-9832-a1d2174594ae.jsonl"

完整压缩数据:auto_compact_second_time_after.json

压缩文件位置:/Users/fanlv/.claude/projects/-Users-fanlv-Desktop-test/69d56a2c-d9a9-433f-9832-a1d2174594ae.jsonl

导出的文件,是同一个文件。会把第一轮压缩后新的对话追加到文件中。

第二压缩后,让模型总结我之前的问题,已经丢失了我第一轮压缩前的问题。

"根据之前的对话记录,你提出的问题有:

1. **"一共多少个章节?"** - 问"元龙"一共有多少章
2. **"我之前说了哪些内容?"** - 要求总结你之前提供的内容
3. **"元龙 第二章说了什么?"** - 要求详细讲述第2章的内容

这些问题都是围绕中国网络文学作品"元龙"展开的内容提问。"

Offload 文件

目录
.
├── 0b28d950-c5c9-4543-a97c-635d487126df.jsonl
├── 3dd476bb-91c5-4f1a-ad14-081ddbf074fc.jsonl
├── 691518c5-5f90-434c-a233-58b167892874.jsonl
├── 69d56a2c-d9a9-433f-9832-a1d2174594ae          <-- session 目录
│   └── tool-results
│       ├── toolu_011FB334YmudsnxzixRg2Kza.txt
│       └── toolu_016AF6AZXYEaMTZdW7Ehsf5p.txt
├── 69d56a2c-d9a9-433f-9832-a1d2174594ae.jsonl   <-- session 文件
└── sessions-index.json
Session

会记录不同 session 对应的 offload 文件位置

{
  "version": 1,
  "entries": [
    {
      "sessionId": "69d56a2c-d9a9-433f-9832-a1d2174594ae",
      "fullPath": "/Users/fanlv/.claude/projects/-Users-fanlv-Desktop-test/69d56a2c-d9a9-433f-9832-a1d2174594ae.jsonl",
      "fileMtime": 1769237167619,
      "firstPrompt": "No prompt",
      "summary": "Chinese Web Novel Chapter Summary Analysis",
      "messageCount": 2,
      "created": "2026-01-24T06:33:36.804Z",
      "modified": "2026-01-24T06:33:44.737Z",
      "gitBranch": "",
      "projectPath": "/Users/fanlv/Desktop/test",
      "isSidechain": false
    }
  ],
  "originalPath": "/Users/fanlv/Desktop/test"
}%                                                                                                                                                                      ➜  -Users-fanlv-Desktop-test
用户完整上下文

完整的聊天消息内容会保存到 {$uuid}.jsonl 中,一个 session 会对应一个 {$uuid}.jsonl 多次 Compact都是 offload 同一个文件。消息格式如下:

{
    "parentUuid": null,
    "isSidechain": false,
    "userType": "external",
    "cwd": "/Users/fanlv/Desktop/test",
    "sessionId": "69d56a2c-d9a9-433f-9832-a1d2174594ae",
    "version": "2.1.19",
    "gitBranch": "",
    "type": "user",
    "message": {
        "role": "user",
        "content": "你好"
    },
    "uuid": "5ecb521f-6ebe-4a84-beee-e3f9d7f201c2",
    "timestamp": "2026-01-24T06:12:34.604Z",
    "thinkingMetadata": {
        "maxThinkingTokens": 31999
    },
    "todos": [],
    "permissionMode": "default"
}

Compact 以后的 summary 的消息,也会保存到 {$uuid}.jsonl 里面。多了两个字段isVisibleInTranscriptOnly** 和 **isCompactSummary

{
    "parentUuid": "3a4b6d76-4884-46dd-9cae-10c401240a52",
    "isSidechain": false,
    "userType": "external",
    "cwd": "/Users/fanlv/Desktop/test",
    "sessionId": "69d56a2c-d9a9-433f-9832-a1d2174594ae",
    "version": "2.1.19",
    "gitBranch": "",
    "slug": "wondrous-popping-liskov",
    "type": "user",
    "message": {
        "role": "user",
        "content": "{{$压缩的内容}}"
    },
    "isVisibleInTranscriptOnly": true,
    "isCompactSummary": true,
    "uuid": "99bf4c7f-57e4-4b67-b9a6-faf2d0cd3b8c",
    "timestamp": "2026-01-24T06:33:36.804Z"
}

工具压缩

消息发送前压缩

压缩后的格式
// 保存成功
<persisted-output>Tool result saved to: /Users/xxx/.claude/projects/abc/tool-results/toolu_01ABC.txt

Use Read to view</persisted-output>

// 保存失败
[Old tool result content cleared]
"type": "user",
  "message": {
    "role": "user",
    "content": [
      {
        "type": "tool_result",
        "tool_use_id": "toolu_01ABC123",
        "content": "<persisted-output>Tool result saved to: /Users/xxx/.claude/projects/xxx/tool-results/toolu_01ABC123.txt\n\nUse Read to view</persisted-output>"
      }
    ]
  }
}
压缩流程
**// 1. 常量定义**
var BW2 = 20000;   // 触发阈值:需要节省至少 20000 tokens 才会执行压缩
var uW2 = 40000;   // 目标 token 数:压缩后保留的最大 token 数
var mW2 = 3;       // 保护最近 3 个工具调用不被压缩
var GqK = 2000;    // 图片的 token 估算值

// 可以被压缩的工具列表
var gW2 = new Set([
  C5,   // Read (文件读取)
  I7,   // Bash (命令执行)
  ow,   // Grep (文本搜索)
  uH,   // Glob (文件模式匹配)
  gE,   // WebSearch (网络搜索)
  z0,   // WebFetch (网页内容获取)
  c5,   // Edit (文件编辑)
  x2    // Write (文件写入)
]);

// 状态变量
var VjA = new Set();  // 已压缩的工具调用 ID 集合
var fjA = new Set();  // 已清除的附件 UUID 集合
var NP1 = new Map();  // token 数量缓存 (tool_use_id -> token_count)

// 占位符字符串
var BI6 = "[Old tool result content cleared]";
var TP1 = "<persisted-output>";

**// 2. 主函数 `gp()` 流程**

async function gp(A, K, q) {
  // A = 消息数组
  // K = 手动指定的目标 token 数 (可选)
  // q = toolUseContext (工具使用上下文)


**### 步骤 1:检查是否禁用**

// 重置压缩状态标志
_qK();

// 检查环境变量是否禁用 Microcompact
if (E1(process.env.DISABLE_MICROCOMPACT) || lK("tengu_cache_plum_violet", false)) {
  return { messages: A };  // 直接返回原消息,不压缩
}

**### 步骤 2:确定目标 token 数**

let Y = K !== undefined;           // 是否手动触发
let z = Y ? K : uW2;               // 目标 token 数,默认 40000

**### 步骤 3:收集可压缩的工具调用**

let w = [];  // 可压缩的 tool_use_id 列表
let H = new Map();  // tool_use_id -> token 数量

for (let W of A) {
  if ((W.type === "user" || W.type === "assistant") && Array.isArray(W.message.content)) {
    for (let D of W.message.content) {
      // 收集可压缩工具的 tool_use
      if (D.type === "tool_use" && gW2.has(D.name)) {
        if (!VjA.has(D.id)) {  // 排除已压缩的
          w.push(D.id);
        }
      }
      // 收集对应的 tool_result 的 token 数
      else if (D.type === "tool_result" && w.includes(D.tool_use_id)) {
        let j = FW2(D.tool_use_id, D);  // 计算/获取缓存的 token 数
        H.set(D.tool_use_id, j);
      }
    }
  }
}

**### 步骤 4:保护最近的工具调用**
// 保护最近 3 个工具调用不被压缩
let J = w.slice(-mW2);  // 取最后 3 个


**### 步骤 5:计算需要压缩哪些工具输出**
let X = Array.from(H.values()).reduce((W, D) => W + D, 0);  // 总 token 数
let O = 0;  // 已节省的 token 数
let $ = new Set();  // 待压缩的 tool_use_id 集合

for (let W of w) {
  if (J.includes(W)) continue;  // 跳过受保护的
  
  if (X - O > z) {  // 如果当前 token 数仍超过目标
    $.add(W);       // 标记为待压缩
    O += H.get(W) || 0;  // 累加节省的 token 数
  }
}

**### 步骤 6:检查是否满足压缩条件(自动模式)**
if (!Y) {  // 非手动触发
  let W = zP(A);  // 计算当前消息总 token 数
  
  // 如果未达到警告阈值,或节省的 token 数不足 20000
  if (!mp(W).isAboveWarningThreshold || O < BW2) {
    $.clear();  // 清空待压缩集合,不执行压缩
    O = 0;
  }
}

**### 步骤 7:清除附件(memory 类型)**
let Z = new Set();  // 待清除的附件 UUID

if ($.size > 0) {
  // 收集 memory 类型的附件并标记清除
  A.filter(D => D && D.type === "attachment" && D.attachment.type === "memory" && !fjA.has(D.uuid))
   .map(D => ({ uuid: D.uuid }))
   .forEach(D => {
     fjA.add(D.uuid);
     Z.add(D.uuid);
   });
}


**### 步骤 8:执行压缩,生成新消息数组**
let G = [];  // 新消息数组

for (let W of A) {
  // 跳过已清除的附件
  if (W.type === "attachment" && fjA.has(W.uuid)) continue;
  
  // 非 user/assistant 消息直接保留
  if (W.type !== "user" && W.type !== "assistant") {
    G.push(W);
    continue;
  }
  
  // 处理 user 消息中的 tool_result
  if (W.type === "user") {
    let D = [];
    let j = false;
    
    for (let M of W.message.content) {
      if (M.type === "tool_result" && _(M.tool_use_id) && M.content && !xW2(M.content)) {
        j = true;
        
        // 尝试持久化内容到文件
        let P = BI6;  // 默认占位符
        let V = await sKA(M.content, M.tool_use_id);  // 保存到文件
        
        if (!tKA(V)) {  // 保存成功
          P = `${TP1}Tool result saved to: ${V.filepath}\n\nUse ${C5} to view${xI6}`;
        }
        
        D.push({ ...M, content: P });  // 替换内容为占位符
      } else {
        D.push(M);
      }
    }
    
    if (D.length > 0) {
      let M = j ? undefined : W.toolUseResult;
      G.push({ ...W, message: { ...W.message, content: D }, toolUseResult: M });
    }
  }
  // assistant 消息直接保留
  else {
    let D = [];
    for (let j of W.message.content) D.push(j);
    G.push({ ...W, message: { ...W.message, content: D } });
  }
}

**### 步骤 9:更新 Read 文件状态缓存**
if (q && $.size > 0) {
  let W = new Map();  // 被压缩的 Read 调用的文件路径
  let D = new Set();  // 未被压缩的 Read 调用的文件路径
  
  for (let j of A) {
    if ((j.type === "user" || j.type === "assistant") && Array.isArray(j.message.content)) {
      for (let M of j.message.content) {
        if (M.type === "tool_use" && M.name === C5) {  // Read 工具
          let P = M.input?.file_path;
          if (typeof P === "string") {
            if ($.has(M.id)) W.set(P, M.id);
            else D.add(P);
          }
        }
      }
    }
  }
  
  // 从缓存中删除被压缩且没有后续读取的文件
  for (let [j] of W) {
    if (!D.has(j)) q.readFileState.delete(j);
  }
}

**### 步骤 10:更新状态并发送遥测**

// 将新压缩的 ID 加入已压缩集合
for (let W of $) VjA.add(W);

// 如果有压缩发生
if ($.size > 0) {
  // 发送遥测数据
  n("tengu_microcompact", {
    toolsCompacted: $.size,           // 压缩的工具数量
    totalUncompactedTokens: X,        // 压缩前总 token 数
    tokensAfterCompaction: X - O,     // 压缩后 token 数
    tokensSaved: O,                   // 节省的 token 数
    triggerType: Y ? "manual" : "auto"  // 触发类型
  });
  
  // 更新 UI 状态
  PjA();
  
  // 生成边界标记消息
  let W = DqK(Y ? "manual" : "auto", X, O, Array.from($), Array.from(Z));
  
  return { messages: G, compactionInfo: { boundaryMessage: W } };
}

return { messages: G };

---

**## 3. 辅助函数**

**### Token 计算函数**

// 计算 tool_result 的 token 数
function WqK(A) {
  if (!A.content) return 0;
  if (typeof A.content === "string") return bY(A.content);
  
  return A.content.reduce((K, q) => {
    if (q.type === "text") return K + bY(q.text);
    else if (q.type === "image") return K + GqK;  // 图片固定 2000 tokens
    return K;
  }, 0);
}

// 带缓存的 token 计算
function FW2(A, K) {
  let q = NP1.get(A);
  if (q === undefined) {
    q = WqK(K);
    NP1.set(A, q);
  }
  return q;
}

// 计算消息数组的总 token 数(带 1.333 倍系数)
function KQA(A) {
  let K = 0;
  for (let q of A) {
    // ... 累加各类内容的 token
  }
  return Math.ceil(K * 1.3333333333333333);
}


**### 状态检查函数**
// 检查内容是否已被压缩
function xW2(A) {
  return typeof A === "string" && (A === BI6 || A.includes(TP1));
}

// 重置 Microcompact 状态
function N8K() {
  VjA.clear();
  fjA.clear();
  NP1.clear();
  ZqK();
}

// 从历史消息恢复状态
function bI6(A) {
  VjA.clear();
  fjA.clear();
  NP1.clear();
  
  let K = jv(A);
  for (let Y of K) {
    if (hI6(Y)) {  // 检查是否有 microcompact 元数据
      let { compactedToolIds, clearedAttachmentUUIDs } = Y.microcompactMetadata;
      for (let z of compactedToolIds ?? []) VjA.add(z);
      for (let w of clearedAttachmentUUIDs ?? []) fjA.add(w);
    }
  }
}

工具输出压缩

压缩后的格式
<persisted-output>
Output too large (50.2 KB). Full output saved to: /path/to/file.txt

Preview (first 2 KB):
[前 2000 字节的内容预览]
...
</persisted-output>
{
      "role": "user",
      "content": [
        {
          "tool_use_id": "toolu_011FB334YmudsnxzixRg2Kza",
          "type": "tool_result",
          "content": "<persisted-output>\nOutput too large (134.7KB). Full output saved to: /Users/fanlv/.claude/projects/-Users-fanlv-Desktop-test/69d56a2c-d9a9-433f-9832-a1d2174594ae/tool-results/toolu_011FB334YmudsnxzixRg2Kza.txt\n\nPreview (first 2KB):\n1:{\"parentUuid\":\"c1a237a3-9cca-4cfa-8747-fe286dca1035\",\"isSidechain\":false,\"userType\":\"external\",\"cwd\":\"/Users/fanlv/Desktop/test\",\"sessionId\":\"69d56a2c-d9a9-433f-9832-a1d2174594ae\",\"version\":\"2.1.19\",\"gitBranch\":\"\",\"type\":\"user\",\"message\":{\"role\":\"user\",\"content\":\"《元龙》全集\\n\\n作者:任怨\\n\\n声明:本书由酷书网(www.Kusuu.com)网友收集整理于网络,仅供交流学习使用,版权归原作者和出版社所有,如果喜欢,请支持正版.\\n\\n感谢您在【酷书网】下载小说,祝您阅读愉快,记住要好好爱护您的眼睛,别让它太累了哦!!!\\n\\n简介\\n\\n  元魂世界,玄幻神奇,家族丛生,宗门耸立,强者纷出,高手如云。\\n  王牌狙击手王胜穿越到元魂的世界,一头撞上了最不入流的鲤鱼残魂,成了人见人欺的废物。\\n  我来是杀人的,不是来被你们嘲笑的。\\n  既然能让麻雀变凤凰,且看我如何鲤鱼跃龙门。\\n  等我站在云端的时候,再低头俯瞰芸芸众生。\\n\\n\\n楔子\\n  “狼巢,狼巢,独狼已跳伞!重复,独狼已跳伞!”\\n  “狼巢收到!”\\n  “狼巢,狼巢,装备仓放出!重复,装备仓放出!”\\n  “狼巢收到!”\\n  ……\\n  “杀了我!”\\n  脑海中突然传来了那个熟悉的飘渺的声音,独狼的眼前仿佛又看到了那双美丽的眼睛。\\n  ……\\n  “狼巢,独狼终端信号突然消失!独狼终端信号在半空中突然消失!”\\n  “狼巢,装备仓信号突然消失!装备仓信号突然消失!”\\n  ……\\n\\n\\n第1章 初来贵地\\n  独狼是王胜的代号,王胜是最强的战士,王牌狙击手,身经百战。\\n  王胜从未想到过,自己刚刚还在夜间突降,眼前突然就变成了白天,而自己的身份,也从一个孤独的单身狗,突然就变成了某个人的未婚夫,某个家族秘密培养的继承人,以及更多的其他身份,尽管到目前为止,王胜自己都不知道这些。\\n  更糟糕的是,王胜发现自己背着的降落伞突然消失了。身体忽然之间就进入了失重状态,开始自由落体。一刹那的大惊之下,王胜开始自救。\\n  王胜是久经训练的强大战士,再大的危机,王胜也只当是对自己的考验,哪怕再不合理再不可能的状况。王牌狙击手的强悍心理素质让他第一时间冷静的开始观察周围的情形。\\n  离地一百六十米左右,是地球上著名的自杀圣地金门大桥离水面高度的两倍多。还好,下面有个不小的湖,稍微调整一下,就能掉进湖中。\\n  眼角光芒闪过,装备仓在远处掠过,同样没有了降落伞,重重的摔向了远处的山区。王胜只能匆忙的记忆了一下方向,再多的装备,也只能安全落地以后再说。\\n  调整了一下姿势,将身体大大的张开,增加迎风面积,王胜向着下方的湖面落去。\\n  下方的湖边,一群人正在对峙,确切的说,是两个势力的人手。严格的说,也并不是对峙,很明显的一方势大,已经控制了局面,另一边的人已经沦为阶下囚,完全被对方控制。\\n  “戴欢,这里是我宋家禁地,里面的元魂也是我宋家老祖宗耗费心力培养的,你真敢冒天下之大不韪强夺?”势弱的一方领头的却是个女子,此刻正大声的喝斥着另一方的那个年轻首领。\\n  “宋嫣,我连你都敢抓,闯你家个禁地算什么?”被叫做戴欢的那个衣着华丽的年轻人一看身份就不简单,面对宋嫣的喝斥,毫不在意,上前几步,饶有兴味的看着面前女子明艳的面孔,笑着说道:“你是我看中的女人,等你成了我的人,你家的东西就是我的,还分什么宋家戴家?乖,告诉我,这上百个元魂中,哪个才是你们精心培育的那个?”\\n  戴欢其实有点气急败坏了,费尽心机从宋家的叛徒口中得到了这么一个绝密消息,本来打着人财两得的期待过来的,却碰上个这样的局面。\\n  宋嫣肯定是自己的女人,这不用说,现在人都在手上了,什么时候想收就能什么时候收。关键是宋家据传从数代之前就开始培养的秘密元魂。该死的宋家人,在这个湖泊中竟然有着不多不少一百个元魂,乍一眼看去,全部都是最不入流的废魂,没一个看起来是自己要谋夺的那个。\\n  不能不说,宋家人的掩藏手法很不错。自己已经让手下把所有的元魂都集中在这个十丈方圆的区域中,可一百个一模一样的废魂,里面只有一个是自己要的,不是宋家核心人物,根本就不知道是哪个。\\n  “我是有未婚夫的,你敢动我,你就不怕我未婚夫和他背后的家族?”宋嫣知道自己已经无法幸免了,可她并不想放弃,无论如何也要争取一下,哪怕希望很渺茫。\\n  “真以为我在宋家没有眼线吗?还是你觉得我只靠着运气就找到了你家的这个外人从不知道的禁区?你宋家的七个长老中,至少\n...\n</persisted-output>",
          "is_error": false
        }
      ]
    }
压缩流程
**## 1. 整体架构**

工具执行完成
    ↓
jM1() - 工具结果处理入口
    ↓
ID2() - 判断是否需要压缩
    ↓
sKA() - 保存内容到文件
    ↓
yD2() - 生成带预览的占位符消息
    ↓
返回压缩后的 tool_result

---

**## 2. 各函数详解**

**### 2.1 入口函数 `jM1()`**

async function jM1(A, K, q) {
  // A = 工具对象 (包含 mapToolResultToToolResultBlockParam 方法)
  // K = 工具执行结果数据
  // q = tool_use_id
  
  // 将工具结果转换为标准的 tool_result 格式
  let Y = A.mapToolResultToToolResultBlockParam(K, q);
  
  // 调用 ID2 进行压缩处理
  return ID2(Y, A.name, A.maxResultSizeChars);
}

**### 2.2 压缩判断函数 `ID2()`**

async function ID2(A, K, q) {
  // A = tool_result 对象 { tool_use_id, type, content }
  // K = 工具名称 (如 "Read", "Bash")
  // q = 该工具的最大允许大小 (maxResultSizeChars)
  
  let Y = A.content;
  
  // 检查 1: 无内容则直接返回
  if (!Y) return A;
  
  // 检查 2: 如果包含图片,不压缩(图片有自己的处理方式)
  if (Array.isArray(Y)) {
    if (Y.some(O => typeof O === "object" && O.type === "image")) 
      return A;
  }
  
  // 检查 3: 计算内容长度
  let contentLength = typeof Y === "string" ? Y.length : UA(Y).length;
  
  // 检查 4: 如果长度 <= 限制,不压缩
  // q 是工具自定义的限制,FyA 是默认值 (400000)
  if (contentLength <= (q ?? FyA)) 
    return A;
  
  // ========== 执行压缩 ==========
  
  // 保存内容到文件
  let H = await sKA(Y, A.tool_use_id);
  
  // 如果保存失败,返回原内容
  if (tKA(H)) return A;  // tKA 检查是否有 error 字段
  
  // 生成带预览的占位符消息
  let J = yD2(H);
  
  // 发送遥测数据
  n("tengu_tool_result_persisted", {
    toolName: VK(K),                                    // 工具名称
    originalSizeBytes: H.originalSize,                  // 原始大小
    persistedSizeBytes: J.length,                       // 压缩后大小
    estimatedOriginalTokens: Math.ceil(H.originalSize / f36),  // 估算原始 token
    estimatedPersistedTokens: Math.ceil(J.length / f36)        // 估算压缩后 token
  });
  
  // 返回压缩后的结果
  return { ...A, content: J };
}

**### 2.3 文件保存函数 `sKA()`**

async function sKA(A, K) {
  // A = 要保存的内容 (string 或 array)
  // K = tool_use_id (用作文件名)
  
  // 确保目录存在
  await RD2();  // 创建 ~/.claude/projects///tool-results/
  
  // 判断内容类型,决定文件扩展名
  let q = Array.isArray(A);           // 是否是数组
  let Y = q ? "json" : "txt";         // 扩展名
  
  // 构建文件路径: ~/.claude/projects///tool-results/.txt
  let z = HS6(RP1(), `${K}.${Y}`);
  
  // 序列化内容
  let w = q ? UA(A, null, 2) : A;     // 数组用 JSON 格式化,字符串直接用
  
  // 检查文件是否已存在(避免重复写入)
  let H = false;
  try {
    await CD2(z);  // stat 检查
    H = true;      // 文件已存在
  } catch {}
  
  // 如果文件不存在,写入
  if (!H) {
    try {
      await kD2(z, w, "utf-8");  // writeFile
    } catch (O) {
      let $ = O instanceof Error ? O : Error(String(O));
      qA($);  // 记录错误
      return { error: hD2($) };  // 返回错误信息
    }
    h(`Persisted tool result to ${z} (${IX(w.length)})`);  // 日志
  }
  
  // 生成预览(前 2000 字节)
  let { preview: J, hasMore: X } = SD2(w, sqK);  // sqK = 2000
  
  // 返回持久化结果
  return {
    filepath: z,           // 文件路径
    originalSize: w.length, // 原始大小
    isJson: q,             // 是否是 JSON
    preview: J,            // 预览内容
    hasMore: X             // 是否有更多内容
  };
}

**### 2.4 预览截取函数 `SD2()`**

function SD2(A, K) {
  // A = 完整内容
  // K = 预览大小限制 (2000)
  
  // 如果内容本身就小于限制,直接返回全部
  if (A.length <= K) 
    return { preview: A, hasMore: false };
  
  // 找到前 K 个字符中最后一个换行符的位置
  let Y = A.slice(0, K).lastIndexOf("\n");
  
  // 如果换行符位置在前半部分,用 K;否则用换行符位置(保持完整行)
  let z = Y > K * 0.5 ? Y : K;
  
  return {
    preview: A.slice(0, z),  // 截取预览
    hasMore: true            // 标记还有更多内容
  };
}

**### 2.5 占位符生成函数 `yD2()`**

function yD2(A) {
  // A = sKA 返回的结果 { filepath, originalSize, preview, hasMore }
  
  let K = `${TP1}
`;  // "\n"

  // 添加大小和路径信息
  K += `Output too large (${IX(A.originalSize)}). Full output saved to: ${A.filepath}

`;

  // 添加预览标题
  K += `Preview (first ${IX(sqK)}):
`;  // "Preview (first 2 KB):\n"

  // 添加预览内容
  K += A.preview;
  
  // 如果有更多内容,添加省略号
  K += A.hasMore ? `
...
` : `
`;

  // 添加结束标签
  K += xI6;  // ""
  
  return K;
}

---

**## 3. 常量定义**

var JS6 = "tool-results";           // 保存目录名
var TP1 = "";     // 开始标签
var xI6 = "";    // 结束标签
var BI6 = "[Old tool result content cleared]";  // Microcompact 用的简单占位符
var sqK = 2000;                     // 预览大小 (2000 字节)
var FyA = 400000;                   // 默认最大允许大小 (400KB)

---

Offload 文件

tool_result 会存在 seesion 同名的文件夹下面,一个 tool_id 作为文件名,里面存了完整的 tool_result 结果。

.
├── 0b28d950-c5c9-4543-a97c-635d487126df.jsonl
├── 3dd476bb-91c5-4f1a-ad14-081ddbf074fc.jsonl
├── 691518c5-5f90-434c-a233-58b167892874.jsonl
├── 69d56a2c-d9a9-433f-9832-a1d2174594ae
│   └── tool-results                              <-- 工具结果目录
│       ├── toolu_011FB334YmudsnxzixRg2Kza.txt   <-- 工具结果文件
│       └── toolu_016AF6AZXYEaMTZdW7Ehsf5p.txt
├── 69d56a2c-d9a9-433f-9832-a1d2174594ae.jsonl
└── sessions-index.json

其他

工具列表占用 Token 计算

Claude code 启动的时候会请求接口:

https://api.anthropic.com/v1/messages/count_tokens?beta=true

请求的内容:

{
  "model": "claude-haiku-4-5-20251001",
  "messages": [
    {
      "role": "user",
      "content": "foo"
    }
  ],
  "tools": [
    // ... 所有 tool 列表
  ]
}

返回内容:

{
  "input_tokens": 16148
}

OpenCode

https://github.com/anomalyco/opencode

CommitID: 8c4bf225f29f26635158333785dbdb4e6923bfb7

消息压缩

压缩 Prompt

summary.txt

You are a helpful AI assistant tasked with summarizing conversations.

When asked to summarize, provide a detailed but concise summary of the conversation. 
Focus on information that would be helpful for continuing the conversation, including:
- What was done
- What is currently being worked on
- Which files are being modified
- What needs to be done next
- Key user requests, constraints, or preferences that should persist
- Important technical decisions and why they were made

Your summary should be comprehensive enough to provide context but concise enough to be quickly understood.

手动压缩流程

支持通过 /compact 或者 /summarize 命令去压缩。没啥好说的。

自动压缩流程

触发场景:

  1. Session Loop 开始时,每次循环开始,检查上一次完成的消息是否导致溢出。

  2. Processor 每步完成时,LLM 每个步骤完成后,实时检测是否需要压缩 。

触发条件:

(input_tokens + cache_read_tokens + output_tokens) > (model.limit.input || (context - output_reserve))
  • input_tokens : 包括系统提示词 + 历史消息 + 当前用户消息

  • cache_read_tokens : Anthropic 的 prompt caching 功能,缓存命中的部分不重复计费但仍占用上下文

  • output_tokens : AI 回复的内容 + 工具调用的 token 数

  • context : 模型的总上下文窗口大小. 一般 200 K

  • output_reserve : 系统默认的输出预留量, 默认 32,000

  • model.limit.input:如果设置了这个值,表示模型对输入有单独的限制。大多数模型不设置此值(为 0)。

没有 offloading

不支持去查压缩之前的数据

┌─────────────────────────────────────────────────────────────────────────────┐
│                         Storage (持久化存储)                                 │
│                                                                             │
│  ┌─────────────────────────────────────────────────────────────────────────┐│
│  │ 所有消息和工具输出都保存在这里                                            ││
│  │                                                                         ││
│  │  [msg1] [msg2] [msg3] ... [compaction] [summary] [msg_n] ... [latest]   ││
│  │                                                                         ││
│  │  工具输出: 原始数据保留,只添加 time.compacted 标记                       ││
│  └─────────────────────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────────────────┘
                                    │
                                    │ filterCompacted()
                                    ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│                         过滤后的消息 (发送给 LLM)                             │
│                                                                             │
│  ┌─────────────────────────────────────────────────────────────────────────┐│
│  │  [summary] [msg_n] ... [latest]                                         ││
│  │                                                                         ││
│  │  被压缩的工具输出 → "[Old tool result content cleared]"                  ││
│  │  未压缩的工具输出 → 原始内容                                              ││
│  └─────────────────────────────────────────────────────────────────────────┘│
│                                                                             │
│  AI 只能看到这部分内容,无法访问 summary 之前的详细数据                        │
└─────────────────────────────────────────────────────────────────────────────┘

# Storage 位置 (macOS)
~/.local/share/opencode/storage/

# 消息存储路径
~/.local/share/opencode/storage/message/{sessionID}/{messageID}.json

# Part 存储路径
~/.local/share/opencode/storage/part/{messageID}/{partID}.json

工具压缩

从后向前遍历对话历史,保留最近 40K tokens 的工具输出,将更早的工具输出标记为已压缩。

  • 保护最近内容

    • 跳过最近 2 轮对话
    • 保留最近 40,000 tokens 的工具输出
  • 修剪旧内容

    • 超过 40K 阈值后的工具输出加入待修剪列表
    • 只有待修剪量 > 20,000 tokens 时才执行修剪
  • 特殊保护

    • skill 工具永不修剪
    • 遇到摘要消息或已压缩的工具时停止遍历
  • 修剪效果

    • 被修剪的工具输出在发送给 LLM 时显示为 "[Old tool result content cleared]",大幅减少 token 使用量。
消息历史(从旧到新):
┌─────────────────────────────────────────────────────────────────┐
│  旧消息                                              新消息      │
│                                                                 │
│  [Tool A] [Tool B] [Tool C] [Tool D] [Tool E] [Tool F] [Tool G] │
│  ↑                         ↑                                    │
│  │                         │                                    │
│  │                         └── 累计 40,000 tokens 的边界        │
│  │                                                              │
│  └── 这些会被标记为 compacted                                   │
│      (如果总量 > 20,000 tokens)                                 │
│                                                                 │
│  [保护区: 最近 2 轮对话的工具不会被修剪]                         │
└─────────────────────────────────────────────────────────────────┘
  • 最近 2 轮对话的工具、受保护的工具(skill)、未完成的工具

  • 需要能节省 20k tokens 才会执行

其他

系统提示词管理

多模型提示词适配:

根据不同的 LLM 模型自动选择最优的提示词模板:

export function provider(model: Provider.Model) {
  if (model.api.id.includes("gpt-5")) return [PROMPT_CODEX]
  if (model.api.id.includes("gpt-") || model.api.id.includes("o1") || model.api.id.includes("o3"))
    return [PROMPT_BEAST]
  if (model.api.id.includes("gemini-")) return [PROMPT_GEMINI]
  if (model.api.id.includes("claude")) return [PROMPT_ANTHROPIC]
  return [PROMPT_ANTHROPIC_WITHOUT_TODO]
}

提示词模板文件 :

环境上下文注入

export async function environment(model: Provider.Model) {
  return [
    `You are powered by the model named ${model.api.id}...`,
    `<env>`,
    `  Working directory: ${Instance.directory}`,
    `  Is directory a git repo: ${project.vcs === "git" ? "yes" : "no"}`,
    `  Platform: ${process.platform}`,
    `  Today's date: ${new Date().toDateString()}`,
    `</env>`,
  ]
}

自定义指令加载

支持从多个来源加载用户自定义指令:

const LOCAL_RULE_FILES = ["AGENTS.md", "CLAUDE.md", "CONTEXT.md"]
const GLOBAL_RULE_FILES = [
  path.join(Global.Path.config, "AGENTS.md"),
  path.join(os.homedir(), ".claude", "CLAUDE.md")
]

还支持从 URL 加载远程指令和通过配置文件指定指令路径。

上下文提醒插入

针对不同模式(Plan Mode / Build Mode)插入特定的上下文提醒:

async function insertReminders(input: { messages, agent, session }) {
  // Plan Mode 提示
  if (input.agent.name === "plan") {
    userMessage.parts.push({
      type: "text",
      text: PROMPT_PLAN,  // 详细的计划模式指导
      synthetic: true,
    })
  }
  
  // 从 Plan 切换到 Build 模式
  if (wasPlan && input.agent.name === "build") {
    userMessage.parts.push({
      type: "text",
      text: BUILD_SWITCH,
      synthetic: true,
    })
  }
}

中途用户消息包装

//system-reminder 包装中途消息,提醒 AI 继续任务
part.text = [
  "<system-reminder>",
  "The user sent the following message:",
  part.text,
  "",
  "Please address this message and continue with your tasks.",
  "</system-reminder>",
].join("\n")

Prompt Caching 支持

为 Anthropic 等支持缓存的提供商添加缓存控制:

function applyCaching(msgs: ModelMessage[], providerID: string) {
  // 为系统消息和最近消息添加缓存标记
  const system = msgs.filter((msg) => msg.role === "system").slice(0, 2)
  const final = msgs.filter((msg) => msg.role !== "system").slice(-2)
  
  for (const msg of unique([...system, ...final])) {
    msg.providerOptions = {
      anthropic: { cacheControl: { type: "ephemeral" } },
      bedrock: { cachePoint: { type: "ephemeral" } },
    }
  }
}

总结

Claude Code(v2.1.19)

  • 消息压缩(/compact、AutoCompact)

    • 触发条件: 支持手动触发和自动触发
    • 压缩范围: 所有 user 和 assistant 的消息
    • 自动触发条件: 上下文窗口使用百分比超过 77.5% 以后,再次对话会自动触发 Compact。
    • offload 文件: 一个 session 会对应一个 {$session_id}.jsonl 文件内容,多次压缩会把完整的上下文内容追加到 {$session_id}.jsonl 里面。
  • 工具压缩(Microcompact)

    • 触发条件: 发送消息前,compact 之前检查,tool_result 超过 40,000 tokens,压缩能节省 20,000 tokens
    • 压缩范围: tool_result 中的消息,tool_use 不被压缩。
    • 可压缩工具: Read, Bash, Grep, Glob, WebSearch, WebFetch, Edit, Write
    • 保护机制: 最近 3 个工具结果不压缩
    • 压缩方式: 将内容保存到文件,替换为占位符引用
    • 替换逻辑:
      • 文件保存成功:Tool result saved to: ${V.filepath}Use Read to view
      • 文件保存失败:[Old tool result content cleared]
  • 工具输出裁剪

    • 触发条件: 工具调用返回结果后,返回的结果字符长度大于 > 400,000
    • 压缩范围: tool_result 中的消息
    • 压缩策略: offload 完整内容到 tool-results 下,tool_result 只返回前 2000个字符
    • 替换逻辑:
      • 文件保存成功:
        Output too large (150 KB). Full output saved to: /Users/xxx/.claude/projects/abc123/def456/tool-results/toolu_01ABC.txt
        这是文件的前 2000 内容
      • 文件保存失败:直接返回 tool result 所有数据。

OpenCode (6923bfb7)

  • 消息压缩(/compact、AutoCompact)

    • 触发条件: 支持手动触发和自动触发
    • 压缩范围: 所有 user 和 assistant 的消息
    • 自动压缩触发条件: 使用的 token 数 > contexWindow - output_reserve (默认 32K) 时候触发。
    • offload 文件: 不支持 AI 去 offload 文件查询之前的消息。
  • 工具压缩(Pruning)

    • 触发条件: 上下文超过 40,000 tokens,压缩能节省 20,000 tokens
    • 压缩范围: tool_result 中的消息。
    • 可压缩工具: 去除了 skill,其他都会被压缩。
    • 保护机制: 保护最近 2 轮对话中的工具调用不被修剪,skill 工具被保护,永远不会被修剪。
    • 压缩方式: 直接替换成 [Old tool result content cleared]