@raolin2025/claude-code-node 2.8.10 → 2.8.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/package.json +1 -1
- package/src/__tests__/compact-window.test.js +13 -0
- package/src/core/compact.js +4 -1
- package/src/core/query-engine.js +21 -6
package/README.md
CHANGED
|
@@ -142,6 +142,9 @@ cc-node 会自动感知当前所用模型的**上下文窗口长度**,并在
|
|
|
142
142
|
估算**判断是否超窗;超窗时把早期对话压缩为摘要(保留最近 4 轮 + 摘要),目标压到窗口
|
|
143
143
|
的 60%。摘要会**显式标注最早的 `Main goal`(核心任务主线)**,并保留最早的关键发现与
|
|
144
144
|
最新结果,避免例行内容淹没核心任务(防止 AI 压缩后失忆)。
|
|
145
|
+
> **保守触发(v2.8.11)**:由于启发式估算可能比模型真实 token 偏少,压缩**提前到
|
|
146
|
+
> 可用窗口的 85%** 即触发(`compressSafetyFactor`,默认 0.85),给 tokenization 差异
|
|
147
|
+
> 留余量,避免实际请求超过模型窗口(如 `exceed_context_size_error` 400 错误)。
|
|
145
148
|
2. **滑动窗口精确裁剪**(兜底,保证永不超窗):摘要压缩后仍超窗时,从**最早的消息**逐条
|
|
146
149
|
挤出,**最新信息始终保留在末尾**,直到总 token ≤ 窗口上限。system 提示永不裁剪;
|
|
147
150
|
**被裁剪的早期历史会压缩成一条摘要 system 保留**(避免 AI 丢失上下文主线);极端情况
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@raolin2025/claude-code-node",
|
|
3
|
-
"version": "2.8.
|
|
3
|
+
"version": "2.8.11",
|
|
4
4
|
"description": "Node.js AI Code Agent CLI - Zero dependencies, pure JavaScript, security hardened, multi-channel notifications, Telegram & QQ Bot remote programming, rich media upload, multi-account management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/core/index.js",
|
|
@@ -103,6 +103,19 @@ test('滑动窗口:keepSummary=false 时不保留摘要', () => {
|
|
|
103
103
|
assert.ok(!r.messages.some(m => m.role === 'system' && m.content.includes('Context Summary')))
|
|
104
104
|
})
|
|
105
105
|
|
|
106
|
+
test('滑动窗口:limitOverride 指定保守裁剪上限(提前触发,防实际超窗)', () => {
|
|
107
|
+
const tb = makeBudget(500)
|
|
108
|
+
const msgs = makeMessages(20) // 41 条
|
|
109
|
+
// 用 limitOverride 指定一个更保守的裁剪上限(模拟估算偏差预留余量)
|
|
110
|
+
const hardLimit = tb.maxTokens - tb.reservedForOutput // 490
|
|
111
|
+
const conservative = Math.floor(hardLimit * 0.85) // 416
|
|
112
|
+
const r = trimToWindow(msgs, { tokenBudget: tb, maxTokens: tb.maxTokens, keepSummary: true, limitOverride: conservative })
|
|
113
|
+
assert.equal(r.trimmed, true)
|
|
114
|
+
// 裁剪后估算 ≤ 保守上限
|
|
115
|
+
assert.ok(tb.estimateMessages(r.messages) <= conservative,
|
|
116
|
+
`裁剪后 ${tb.estimateMessages(r.messages)} 应 ≤ 保守上限 ${conservative}`)
|
|
117
|
+
})
|
|
118
|
+
|
|
106
119
|
test('滑动窗口:极端情况(单条消息超窗)仍保留 system + 最近一条', () => {
|
|
107
120
|
const tb = makeBudget(50) // 窗口很小
|
|
108
121
|
// 构造:每条消息都较大,导致任何单条都可能超窗
|
package/src/core/compact.js
CHANGED
|
@@ -276,7 +276,10 @@ export function trimToWindow(messages, options = {}) {
|
|
|
276
276
|
const maxTokens = options.maxTokens || 160_000
|
|
277
277
|
const reservedForOutput = options.reservedForOutput || (budget ? budget.reservedForOutput : 8192)
|
|
278
278
|
const keepSummary = options.keepSummary !== false
|
|
279
|
-
|
|
279
|
+
// 可直接指定裁剪上限(覆盖 maxTokens - reservedForOutput),用于保守触发场景
|
|
280
|
+
const limit = options.limitOverride != null
|
|
281
|
+
? options.limitOverride
|
|
282
|
+
: maxTokens - reservedForOutput
|
|
280
283
|
|
|
281
284
|
const estimate = (msgs) => budget
|
|
282
285
|
? budget.estimateMessages(msgs)
|
package/src/core/query-engine.js
CHANGED
|
@@ -32,6 +32,8 @@ export class QueryEngineConfig {
|
|
|
32
32
|
this.model = options.model || ''
|
|
33
33
|
this.maxTurns = options.maxTurns || 100
|
|
34
34
|
this.maxBudgetTokens = options.maxBudgetTokens || 1_000_000
|
|
35
|
+
// 压缩触发保守系数(0~1):估算到可用窗口的该比例即触发压缩,留出 tokenization 差异余量
|
|
36
|
+
this.compressSafetyFactor = options.compressSafetyFactor ?? 0.85
|
|
35
37
|
this.permissionMode = options.permissionMode || 'ask'
|
|
36
38
|
this.verbose = options.verbose || false
|
|
37
39
|
// API 配置 — 通用 OpenAI 兼容协议
|
|
@@ -109,29 +111,42 @@ export class QueryEngine {
|
|
|
109
111
|
* 4. 摘要后仍超窗 → 滑动窗口精确裁剪兜底:从最早完整 user 回合挤出,
|
|
110
112
|
* 并把被裁掉的历史压缩成摘要 system 保留,避免 AI 失忆。
|
|
111
113
|
* 保证最新信息(含刚加入的用户消息)保留在末尾,上下文永不超出窗口。
|
|
114
|
+
*
|
|
115
|
+
* 保守化(v2.8.11):
|
|
116
|
+
* estimateMessages 是启发式估算,可能比模型真实 token 偏少。若等到估算真正
|
|
117
|
+
* 超过 hardLimit 才压缩,实际 token 可能已超模型窗口(如 400 exceed_context_size)。
|
|
118
|
+
* 因此压缩触发点提前到 hardLimit × SAFETY_FACTOR(默认 85%),给 tokenization
|
|
119
|
+
* 差异留余量,确保实际请求永远 ≤ 模型窗口。
|
|
112
120
|
*/
|
|
113
121
|
_ensureFitWindow() {
|
|
114
122
|
if (!this.tokenBudget) return
|
|
115
|
-
const
|
|
123
|
+
const maxTokens = this.tokenBudget.maxTokens
|
|
124
|
+
const hardLimit = maxTokens - (this.tokenBudget.reservedForOutput || 0)
|
|
125
|
+
// 保守触发阈值:估算到可用窗口的 85% 就开始压缩,避免估算偏差导致实际超窗
|
|
126
|
+
const safetyFactor = this.config.compressSafetyFactor ?? 0.85
|
|
127
|
+
const triggerLimit = Math.max(1, Math.floor(hardLimit * safetyFactor))
|
|
116
128
|
const est = this.tokenBudget.estimateMessages(this.state.messages)
|
|
117
|
-
if (est <=
|
|
129
|
+
if (est <= triggerLimit) return
|
|
118
130
|
|
|
119
131
|
// 1) 摘要式压缩优先:用实时估算直接决定是否压缩(不再依赖滞后的 usagePercent)
|
|
132
|
+
// 压缩目标也用保守阈值(而非 hardLimit),确保压缩后实际 token 远离窗口上限
|
|
120
133
|
const summarized = compactMessages(this.state.messages, {
|
|
121
|
-
maxTokens: Math.floor(
|
|
134
|
+
maxTokens: Math.floor(maxTokens * 0.6),
|
|
122
135
|
})
|
|
123
136
|
const reEst = this.tokenBudget.estimateMessages(summarized)
|
|
124
|
-
if (reEst <=
|
|
137
|
+
if (reEst <= triggerLimit) {
|
|
125
138
|
this.state.messages = summarized
|
|
126
|
-
if (this.config.verbose) console.error(
|
|
139
|
+
if (this.config.verbose) console.error(`[compact] Context summarized to fit token budget (${est} → ${reEst}, trigger<${triggerLimit})`)
|
|
127
140
|
return
|
|
128
141
|
}
|
|
129
142
|
|
|
130
143
|
// 2) 滑动窗口精确裁剪兜底(摘要仍超窗):裁剪时保留被裁剪历史的摘要
|
|
131
144
|
const { trimmed, messages: trimmedMsgs, removed, summary } = trimToWindow(this.state.messages, {
|
|
132
145
|
tokenBudget: this.tokenBudget,
|
|
133
|
-
maxTokens
|
|
146
|
+
maxTokens,
|
|
134
147
|
keepSummary: true,
|
|
148
|
+
// 裁剪也需满足保守阈值(估算 ≤ triggerLimit),避免估算偏差导致实际超窗
|
|
149
|
+
limitOverride: triggerLimit,
|
|
135
150
|
})
|
|
136
151
|
if (trimmed) {
|
|
137
152
|
this.state.messages = trimmedMsgs
|