@tom2012/cc-web 1.5.111 → 1.5.113
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 +1 -1
- package/backend/dist/index.d.ts.map +1 -1
- package/backend/dist/index.js +21 -0
- package/backend/dist/index.js.map +1 -1
- package/backend/dist/information/condenser.d.ts.map +1 -1
- package/backend/dist/information/condenser.js +370 -141
- package/backend/dist/information/condenser.js.map +1 -1
- package/backend/dist/information/conversation-sync.d.ts +1 -1
- package/backend/dist/information/conversation-sync.d.ts.map +1 -1
- package/backend/dist/information/conversation-sync.js +241 -45
- package/backend/dist/information/conversation-sync.js.map +1 -1
- package/backend/dist/memory-pool/templates.d.ts +1 -0
- package/backend/dist/memory-pool/templates.d.ts.map +1 -1
- package/backend/dist/memory-pool/templates.js +19 -0
- package/backend/dist/memory-pool/templates.js.map +1 -1
- package/backend/dist/routes/information.d.ts.map +1 -1
- package/backend/dist/routes/information.js +2 -1
- package/backend/dist/routes/information.js.map +1 -1
- package/frontend/dist/assets/{GraphPreview-DYuRcUjF.js → GraphPreview-DDqsAxAk.js} +1 -1
- package/frontend/dist/assets/{OfficePreview-B6NucFpx.js → OfficePreview-cp6brXhA.js} +2 -2
- package/frontend/dist/assets/{PlanPanel-B_muilQ8.js → PlanPanel-CO1S2dHJ.js} +1 -1
- package/frontend/dist/assets/{ProjectPage-2Iq-zPKT.js → ProjectPage-C6-XZOBG.js} +3 -3
- package/frontend/dist/assets/{SettingsPage-Bal81q9c.js → SettingsPage-c7KqLCSE.js} +1 -1
- package/frontend/dist/assets/{ShareViewPage-CBuw4gRz.js → ShareViewPage-ZiiJW0Yk.js} +1 -1
- package/frontend/dist/assets/{SkillHubPage-Cu2lihcp.js → SkillHubPage-BjAX2MkD.js} +1 -1
- package/frontend/dist/assets/{bot-bkBE5Xrf.js → bot-Dd9Nyu-k.js} +1 -1
- package/frontend/dist/assets/{chevron-down-CSBT_L3w.js → chevron-down-C4H2eB9x.js} +1 -1
- package/frontend/dist/assets/{index-C3pyHC8a.js → index-CdeMLaWK.js} +3 -3
- package/frontend/dist/assets/{index-D-5MomMV.js → index-DKUfJ8AJ.js} +1 -1
- package/frontend/dist/assets/{jszip.min-DohPrhoA.js → jszip.min-CksP15LE.js} +1 -1
- package/frontend/dist/assets/{matter-Clog--f-.js → matter-DHeDWiZu.js} +1 -1
- package/frontend/dist/assets/{maximize-2-DFp4HnHz.js → maximize-2-3VVDJyWS.js} +1 -1
- package/frontend/dist/assets/{save-DzeZJzB3.js → save-XTZk8GMV.js} +1 -1
- package/frontend/dist/assets/{search-DPBqFbAe.js → search-DefreEPp.js} +1 -1
- package/frontend/dist/assets/{user-DHblJPz0.js → user-dRMLo6Cr.js} +1 -1
- package/frontend/dist/index.html +1 -1
- package/package.json +1 -1
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
// backend/src/information/condenser.ts
|
|
3
3
|
//
|
|
4
|
-
//
|
|
4
|
+
// Iterative condense and reorganize using Claude CLI (haiku).
|
|
5
|
+
// Implements: half-window segmentation, cohesion tracking, context summary injection,
|
|
6
|
+
// single-turn overflow handling, guard rails.
|
|
5
7
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
6
8
|
if (k2 === undefined) k2 = k;
|
|
7
9
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
@@ -43,10 +45,14 @@ const path = __importStar(require("path"));
|
|
|
43
45
|
const child_process_1 = require("child_process");
|
|
44
46
|
const config_1 = require("../config");
|
|
45
47
|
const conversation_sync_1 = require("./conversation-sync");
|
|
48
|
+
// Haiku context: ~200K tokens. Half window for input, half for output.
|
|
49
|
+
const HALF_WINDOW_TOKENS = 80000;
|
|
50
|
+
// Keywords that mark uncondensable user turns
|
|
51
|
+
const UNCONDENSABLE_RE = /不要|别|错了|改成|不是这样|必须|禁止|永远不要|以后请/;
|
|
46
52
|
function estimateTokens(text) {
|
|
47
53
|
return Math.ceil(text.length / 4);
|
|
48
54
|
}
|
|
49
|
-
|
|
55
|
+
// ── Claude CLI ──
|
|
50
56
|
function callHaiku(prompt) {
|
|
51
57
|
return new Promise((resolve, reject) => {
|
|
52
58
|
(0, child_process_1.execFile)('claude', ['-p', prompt, '--model', 'haiku'], { timeout: 120000, maxBuffer: 8 * 1024 * 1024 }, (err, stdout) => {
|
|
@@ -56,15 +62,11 @@ function callHaiku(prompt) {
|
|
|
56
62
|
});
|
|
57
63
|
});
|
|
58
64
|
}
|
|
59
|
-
/** Extract JSON array from Haiku response (handles code fences, extra text). */
|
|
60
65
|
function extractJsonArray(text) {
|
|
61
|
-
// Strip markdown code fences
|
|
62
66
|
let cleaned = text.replace(/```json\s*/gi, '').replace(/```\s*/g, '');
|
|
63
|
-
// Try to find the JSON array
|
|
64
67
|
const start = cleaned.indexOf('[');
|
|
65
68
|
if (start === -1)
|
|
66
69
|
throw new Error('No JSON array found');
|
|
67
|
-
// Find matching closing bracket
|
|
68
70
|
let depth = 0;
|
|
69
71
|
let end = -1;
|
|
70
72
|
for (let i = start; i < cleaned.length; i++) {
|
|
@@ -79,71 +81,285 @@ function extractJsonArray(text) {
|
|
|
79
81
|
}
|
|
80
82
|
}
|
|
81
83
|
if (end === -1) {
|
|
82
|
-
|
|
83
|
-
cleaned = cleaned.
|
|
84
|
-
// Remove any trailing incomplete object
|
|
85
|
-
cleaned = cleaned.replace(/,\s*\{[^}]*$/, ']');
|
|
84
|
+
cleaned = cleaned.slice(start);
|
|
85
|
+
cleaned = cleaned.replace(/,\s*\{[^}]*$/, '') + ']';
|
|
86
86
|
}
|
|
87
87
|
else {
|
|
88
88
|
cleaned = cleaned.slice(start, end + 1);
|
|
89
89
|
}
|
|
90
90
|
return JSON.parse(cleaned);
|
|
91
91
|
}
|
|
92
|
-
/** Parse v0.md into turn sections. */
|
|
93
92
|
function parseTurns(content) {
|
|
94
93
|
const sections = content.split(/(?=^## [UA]\d+)/m).filter(Boolean);
|
|
95
94
|
return sections.map(s => {
|
|
96
95
|
const match = s.match(/^(## [UA]\d+.*)\n/);
|
|
97
96
|
if (!match)
|
|
98
|
-
return
|
|
99
|
-
|
|
100
|
-
|
|
97
|
+
return null;
|
|
98
|
+
const header = match[1];
|
|
99
|
+
const id = header.replace(/^## /, '').split(/[\s\[]/)[0];
|
|
100
|
+
const body = s.slice(match[0].length).trim();
|
|
101
|
+
const hasMarker = /\[c\d+/.test(header);
|
|
102
|
+
return { id, header, body, tokens: estimateTokens(body), condensed: hasMarker, cohesion: null };
|
|
103
|
+
}).filter(Boolean);
|
|
101
104
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
105
|
+
function extractMarkerChain(header) {
|
|
106
|
+
const match = header.match(/\[(.+)\]\s*$/);
|
|
107
|
+
return match ? match[1] : '';
|
|
108
|
+
}
|
|
109
|
+
// ── Prompt building ──
|
|
110
|
+
const CONDENSE_RULES = `你是一个对话缩减器。目标:大幅压缩对话,只保留对未来 LLM 行为有影响的信息。
|
|
111
|
+
|
|
112
|
+
## 必须激进缩减为一句话的内容(这些占对话的 80%+):
|
|
113
|
+
- LLM 的工具调用和输出 → "执行了 X,结果:Y"
|
|
114
|
+
- 构建/发布日志 → "构建成功" 或 "发布 vX.Y.Z"
|
|
115
|
+
- 代码修改的详细描述 → "修改了 file.ts 的 funcName"
|
|
116
|
+
- 文件内容展示 → "读取了 file.ts"
|
|
117
|
+
- 搜索/grep 结果 → "搜索 X,找到 N 处"
|
|
118
|
+
- LLM 的解释性文字("让我检查一下""我来看看"等) → 删除
|
|
119
|
+
- 重复的同类操作(多次发版、多次构建) → 只保留最后一次的结果
|
|
120
|
+
|
|
121
|
+
## 必须保留原文的内容(condensed 设为 null):
|
|
122
|
+
- 用户纠正 LLM 的发言("不对""错了""改成""不要")
|
|
123
|
+
- 用户表达需求或偏好("我希望""请实现""以后请")
|
|
124
|
+
- 设计决策讨论("为什么选 A 不选 B")
|
|
125
|
+
- 错误诊断("报错 X,原因是 Y")
|
|
126
|
+
- 标记了 [已缩减] 的轮次
|
|
127
|
+
|
|
128
|
+
## 不得违反:
|
|
129
|
+
- 绝不改变语义方向(肯定↔否定)
|
|
130
|
+
- 保留版本号、文件路径等标识符
|
|
131
|
+
|
|
132
|
+
对每轮输出 JSON:
|
|
133
|
+
[{"turn":"U1","condensed":"缩减内容或null","cohesion":0.8},...]
|
|
134
|
+
cohesion: 与上一轮的话题相关性(0-1)`;
|
|
135
|
+
function buildSegmentPrompt(turns, condensedUpTo, contextSummary) {
|
|
136
|
+
const parts = [CONDENSE_RULES, ''];
|
|
137
|
+
if (contextSummary) {
|
|
138
|
+
parts.push(`[前文摘要:${contextSummary}]`, '');
|
|
139
|
+
}
|
|
140
|
+
parts.push('对话:');
|
|
141
|
+
for (let i = 0; i < turns.length; i++) {
|
|
142
|
+
const t = turns[i];
|
|
143
|
+
if (i < condensedUpTo) {
|
|
144
|
+
// Already condensed in previous iteration — mark as [已缩减]
|
|
145
|
+
parts.push(`## ${t.id} [已缩减]`);
|
|
146
|
+
parts.push(t.body);
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
parts.push(`## ${t.id}`);
|
|
150
|
+
parts.push(t.body);
|
|
151
|
+
}
|
|
152
|
+
parts.push('');
|
|
153
|
+
}
|
|
154
|
+
return parts.join('\n');
|
|
155
|
+
}
|
|
156
|
+
// ── Context summary generation ──
|
|
157
|
+
function generateContextSummary(turns) {
|
|
158
|
+
// Take first sentence of each turn, max 5 turns
|
|
159
|
+
const summaryParts = [];
|
|
160
|
+
for (const t of turns.slice(0, 5)) {
|
|
161
|
+
const firstSentence = t.body.split(/[。!?\n]/)[0].slice(0, 40);
|
|
162
|
+
summaryParts.push(`${t.id}: ${firstSentence}`);
|
|
163
|
+
}
|
|
164
|
+
if (turns.length > 5)
|
|
165
|
+
summaryParts.push(`...共${turns.length}轮`);
|
|
166
|
+
return summaryParts.join(';');
|
|
167
|
+
}
|
|
168
|
+
// ── Select segment that fits in half window ──
|
|
169
|
+
function selectSegment(turns, startIdx, halfWindow) {
|
|
170
|
+
let total = 0;
|
|
171
|
+
const promptOverhead = estimateTokens(CONDENSE_RULES) + 200; // rules + formatting
|
|
172
|
+
total += promptOverhead;
|
|
173
|
+
for (let i = startIdx; i < turns.length; i++) {
|
|
174
|
+
const turnCost = turns[i].tokens + 20; // header + formatting overhead
|
|
175
|
+
if (total + turnCost > halfWindow && i > startIdx) {
|
|
176
|
+
// Ensure we end on a complete U-A pair
|
|
177
|
+
let endIdx = i;
|
|
178
|
+
// If endIdx splits a U-A pair (ends on U without A), back up one
|
|
179
|
+
if (endIdx > startIdx && turns[endIdx - 1]?.id.startsWith('U')) {
|
|
180
|
+
endIdx--;
|
|
181
|
+
}
|
|
182
|
+
return { endIdx: Math.max(startIdx + 2, endIdx), totalTokens: total };
|
|
183
|
+
}
|
|
184
|
+
total += turnCost;
|
|
185
|
+
}
|
|
186
|
+
return { endIdx: turns.length, totalTokens: total };
|
|
187
|
+
}
|
|
188
|
+
// ── Find lowest cohesion cut point ──
|
|
189
|
+
function findLowestCohesionCut(turns, startIdx, endIdx) {
|
|
190
|
+
let minCohesion = 2;
|
|
191
|
+
let cutIdx = Math.floor((startIdx + endIdx) / 2); // fallback: midpoint
|
|
192
|
+
for (let i = startIdx + 2; i < endIdx; i += 2) { // step by 2 to cut at U-A pair boundaries
|
|
193
|
+
const c = turns[i].cohesion;
|
|
194
|
+
if (c !== null && c < minCohesion) {
|
|
195
|
+
minCohesion = c;
|
|
196
|
+
cutIdx = i;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
// If all cohesion > 0.8, use midpoint fallback
|
|
200
|
+
if (minCohesion > 0.8)
|
|
201
|
+
cutIdx = Math.floor((startIdx + endIdx) / 2);
|
|
202
|
+
return cutIdx;
|
|
203
|
+
}
|
|
204
|
+
// ── Pre-truncate oversized single turn ──
|
|
205
|
+
function truncateTurn(turn, maxTokens) {
|
|
206
|
+
const headTokens = Math.floor(maxTokens * 0.7);
|
|
207
|
+
const tailTokens = Math.floor(maxTokens * 0.2);
|
|
208
|
+
const headChars = headTokens * 4;
|
|
209
|
+
const tailChars = tailTokens * 4;
|
|
210
|
+
const omitted = estimateTokens(turn.body) - maxTokens;
|
|
211
|
+
const truncatedBody = turn.body.slice(0, headChars) +
|
|
212
|
+
`\n\n[...省略约 ${omitted} tokens...]\n\n` +
|
|
213
|
+
turn.body.slice(-tailChars);
|
|
214
|
+
return { ...turn, body: truncatedBody, tokens: estimateTokens(truncatedBody) };
|
|
215
|
+
}
|
|
216
|
+
// ── Iterative condense ──
|
|
217
|
+
// Strategy: slide a window through turns. Each window includes:
|
|
218
|
+
// 1. Context prefix: last CONTEXT_OVERLAP condensed turns from previous window (marked [已缩减])
|
|
219
|
+
// 2. New turns to condense (fills remaining window space)
|
|
220
|
+
// Haiku sees the context to understand conversation flow, but only condenses new turns.
|
|
221
|
+
const CONTEXT_OVERLAP = 6; // number of condensed turns to carry as context
|
|
222
|
+
const BUDGET_FOR_CONTEXT = 8000; // max tokens for context prefix
|
|
223
|
+
async function iterativeCondense(turns) {
|
|
224
|
+
const halfWindow = HALF_WINDOW_TOKENS;
|
|
225
|
+
const cohesionMap = {};
|
|
226
|
+
let condensedUpTo = 0;
|
|
227
|
+
// Pre-truncate any single turn larger than half window
|
|
228
|
+
for (let i = 0; i < turns.length; i++) {
|
|
229
|
+
if (turns[i].tokens > halfWindow * 0.8) {
|
|
230
|
+
turns[i] = truncateTurn(turns[i], Math.floor(halfWindow * 0.7));
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
console.log(`[condenser] starting iterative condense: ${turns.length} turns`);
|
|
234
|
+
while (condensedUpTo < turns.length) {
|
|
235
|
+
// 1. Build context prefix from last few condensed turns
|
|
236
|
+
let contextTurns = [];
|
|
237
|
+
let contextTokens = 0;
|
|
238
|
+
if (condensedUpTo > 0) {
|
|
239
|
+
const contextStart = Math.max(0, condensedUpTo - CONTEXT_OVERLAP);
|
|
240
|
+
for (let i = contextStart; i < condensedUpTo; i++) {
|
|
241
|
+
if (contextTokens + turns[i].tokens + 20 > BUDGET_FOR_CONTEXT)
|
|
242
|
+
break;
|
|
243
|
+
contextTurns.push(turns[i]);
|
|
244
|
+
contextTokens += turns[i].tokens + 20;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
// Also prepend a summary of everything before the context window
|
|
248
|
+
let contextSummary = '';
|
|
249
|
+
const contextStartIdx = condensedUpTo > CONTEXT_OVERLAP ? condensedUpTo - CONTEXT_OVERLAP : 0;
|
|
250
|
+
if (contextStartIdx > 0) {
|
|
251
|
+
contextSummary = generateContextSummary(turns.slice(0, contextStartIdx));
|
|
252
|
+
}
|
|
253
|
+
// 2. Fill remaining window with new turns
|
|
254
|
+
const availableForNew = halfWindow - contextTokens - estimateTokens(CONDENSE_RULES) - 500;
|
|
255
|
+
let newEnd = condensedUpTo;
|
|
256
|
+
let newTokens = 0;
|
|
257
|
+
while (newEnd < turns.length) {
|
|
258
|
+
const cost = turns[newEnd].tokens + 20;
|
|
259
|
+
if (newTokens + cost > availableForNew && newEnd > condensedUpTo)
|
|
260
|
+
break;
|
|
261
|
+
newTokens += cost;
|
|
262
|
+
newEnd++;
|
|
263
|
+
}
|
|
264
|
+
if (newEnd <= condensedUpTo)
|
|
265
|
+
break; // Stuck — no new turns fit
|
|
266
|
+
// 3. Build prompt: context (marked [已缩减]) + new turns (to condense)
|
|
267
|
+
const segmentTurns = [...contextTurns, ...turns.slice(condensedUpTo, newEnd)];
|
|
268
|
+
const prompt = buildSegmentPrompt(segmentTurns, contextTurns.length, contextSummary);
|
|
269
|
+
// 4. Call Haiku
|
|
270
|
+
const results = await callHaikuAndParse(prompt, turns, condensedUpTo, newEnd, condensedUpTo);
|
|
271
|
+
applyResults(turns, results, condensedUpTo, newEnd, cohesionMap);
|
|
272
|
+
console.log(`[condenser] iteration: turns ${condensedUpTo}-${newEnd} (context: ${contextTurns.length} turns), ${results.size} results from Haiku`);
|
|
273
|
+
condensedUpTo = newEnd;
|
|
274
|
+
}
|
|
275
|
+
console.log(`[condenser] done: ${condensedUpTo}/${turns.length} turns processed`);
|
|
276
|
+
return { condensedTurns: turns, cohesionMap };
|
|
277
|
+
}
|
|
278
|
+
async function callHaikuAndParse(prompt, allTurns, segStart, segEnd, condensedUpTo) {
|
|
279
|
+
let rawResult;
|
|
280
|
+
try {
|
|
281
|
+
rawResult = await callHaiku(prompt);
|
|
282
|
+
}
|
|
283
|
+
catch (err) {
|
|
284
|
+
// On failure, skip this segment (leave turns as-is)
|
|
285
|
+
console.error('[condenser] Haiku call failed:', err instanceof Error ? err.message : err);
|
|
286
|
+
return new Map();
|
|
287
|
+
}
|
|
288
|
+
let parsed;
|
|
289
|
+
try {
|
|
290
|
+
parsed = extractJsonArray(rawResult);
|
|
291
|
+
}
|
|
292
|
+
catch {
|
|
293
|
+
console.error('[condenser] JSON parse failed, skipping segment');
|
|
294
|
+
return new Map();
|
|
295
|
+
}
|
|
296
|
+
const resultMap = new Map();
|
|
297
|
+
for (const r of parsed) {
|
|
298
|
+
resultMap.set(r.turn, r);
|
|
299
|
+
}
|
|
300
|
+
return resultMap;
|
|
301
|
+
}
|
|
302
|
+
function applyResults(turns, results, condensedUpTo, segEnd, cohesionMap) {
|
|
303
|
+
for (let i = condensedUpTo; i < segEnd && i < turns.length; i++) {
|
|
304
|
+
const t = turns[i];
|
|
305
|
+
const r = results.get(t.id);
|
|
306
|
+
// Record cohesion
|
|
307
|
+
if (r?.cohesion !== undefined) {
|
|
308
|
+
t.cohesion = r.cohesion;
|
|
309
|
+
cohesionMap[t.id] = r.cohesion;
|
|
310
|
+
}
|
|
311
|
+
// Guard rail: protect uncondensable user turns
|
|
312
|
+
if (t.id.startsWith('U') && UNCONDENSABLE_RE.test(t.body)) {
|
|
313
|
+
t.condensed = true; // Mark as processed but keep original
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
// Guard rail: already condensed turns must not be re-condensed
|
|
317
|
+
if (t.condensed)
|
|
318
|
+
continue;
|
|
319
|
+
// Apply condensation
|
|
320
|
+
if (r && r.condensed !== null && r.condensed !== undefined) {
|
|
321
|
+
t.body = r.condensed;
|
|
322
|
+
t.tokens = estimateTokens(r.condensed);
|
|
323
|
+
}
|
|
324
|
+
t.condensed = true;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
// ── Build final content with markers ──
|
|
328
|
+
function buildFinalContent(originalTurns, // from v0.md (for token % calculation)
|
|
329
|
+
condensedTurns, prevMarkers, level) {
|
|
106
330
|
const lines = [];
|
|
107
|
-
for (
|
|
108
|
-
const
|
|
109
|
-
const
|
|
110
|
-
const origTokens =
|
|
111
|
-
const newTokens =
|
|
331
|
+
for (let i = 0; i < condensedTurns.length; i++) {
|
|
332
|
+
const ct = condensedTurns[i];
|
|
333
|
+
const ot = originalTurns.find(t => t.id === ct.id);
|
|
334
|
+
const origTokens = ot ? ot.tokens : ct.tokens;
|
|
335
|
+
const newTokens = ct.tokens;
|
|
112
336
|
let marker = '';
|
|
113
|
-
if (
|
|
114
|
-
//
|
|
115
|
-
const pct =
|
|
116
|
-
const prevChain = prevMarkers.get(
|
|
337
|
+
if (origTokens > 0 && newTokens < origTokens * 0.95) {
|
|
338
|
+
// Content was actually condensed (>5% reduction)
|
|
339
|
+
const pct = Math.round(newTokens / origTokens * 100);
|
|
340
|
+
const prevChain = prevMarkers.get(ct.id) || '';
|
|
117
341
|
marker = prevChain ? ` [c${level},${pct}%;${prevChain}]` : ` [c${level},${pct}%]`;
|
|
118
342
|
}
|
|
119
343
|
else {
|
|
120
|
-
|
|
121
|
-
const existing = prevMarkers.get(turn.id);
|
|
344
|
+
const existing = prevMarkers.get(ct.id);
|
|
122
345
|
if (existing)
|
|
123
346
|
marker = ` [${existing}]`;
|
|
124
347
|
}
|
|
125
|
-
lines.push(`## ${
|
|
126
|
-
lines.push(body);
|
|
348
|
+
lines.push(`## ${ct.id}${marker}`);
|
|
349
|
+
lines.push(ct.body);
|
|
127
350
|
lines.push('');
|
|
128
351
|
}
|
|
129
352
|
return lines.join('\n');
|
|
130
353
|
}
|
|
131
|
-
|
|
132
|
-
function extractMarkerChain(header) {
|
|
133
|
-
const match = header.match(/\[(.+)\]\s*$/);
|
|
134
|
-
return match ? match[1] : '';
|
|
135
|
-
}
|
|
136
|
-
// ── Condense ──
|
|
354
|
+
// ── Public: condenseConversation ──
|
|
137
355
|
async function condenseConversation(convDir) {
|
|
138
356
|
const meta = (0, conversation_sync_1.readMeta)(convDir);
|
|
139
357
|
if (!meta)
|
|
140
358
|
return null;
|
|
141
|
-
// Read the latest version as base
|
|
142
359
|
const latestEntry = meta.versions[meta.latest];
|
|
143
360
|
if (!latestEntry)
|
|
144
361
|
return null;
|
|
145
362
|
const baseContent = fs.readFileSync(path.join(convDir, latestEntry.file), 'utf-8');
|
|
146
|
-
// Read original for token comparison
|
|
147
363
|
const v0Content = fs.readFileSync(path.join(convDir, 'v0.md'), 'utf-8');
|
|
148
364
|
const originalTurns = parseTurns(v0Content);
|
|
149
365
|
const baseTurns = parseTurns(baseContent);
|
|
@@ -154,62 +370,15 @@ async function condenseConversation(convDir) {
|
|
|
154
370
|
if (chain)
|
|
155
371
|
prevMarkers.set(t.id, chain);
|
|
156
372
|
}
|
|
157
|
-
//
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
- 相同 → 大幅缩减
|
|
163
|
-
|
|
164
|
-
硬性规则:
|
|
165
|
-
1. 用户纠正/否定行为的发言 → 必须保留原文
|
|
166
|
-
2. 绝不改变语义方向(肯定↔否定)
|
|
167
|
-
3. 保留所有数字、标识符、文件路径
|
|
168
|
-
4. 构建/部署日志 → 一句话结果
|
|
169
|
-
5. 确认性回复 → 保留原文
|
|
170
|
-
|
|
171
|
-
对每轮输出 JSON 数组:
|
|
172
|
-
[{"turn":"U1","condensed":"缩减后的内容或null表示保留原文"},...]
|
|
173
|
-
|
|
174
|
-
对话(如果过长已截取最近部分):
|
|
175
|
-
${baseContent.length > 40000 ? '...[前文已省略]\n\n' + baseContent.slice(-40000) : baseContent}`;
|
|
176
|
-
let result;
|
|
177
|
-
try {
|
|
178
|
-
result = await callHaiku(prompt);
|
|
179
|
-
}
|
|
180
|
-
catch (err) {
|
|
181
|
-
throw new Error('Claude CLI 调用失败: ' + (err instanceof Error ? err.message : String(err)));
|
|
182
|
-
}
|
|
183
|
-
// Parse JSON from response (handles code fences, truncation)
|
|
184
|
-
let condensedArray;
|
|
185
|
-
try {
|
|
186
|
-
condensedArray = extractJsonArray(result);
|
|
187
|
-
}
|
|
188
|
-
catch (parseErr) {
|
|
189
|
-
throw new Error('无法解析 Haiku 返回的 JSON: ' + (parseErr instanceof Error ? parseErr.message : ''));
|
|
190
|
-
}
|
|
191
|
-
// Build condensed bodies map
|
|
192
|
-
const condensedBodies = new Map();
|
|
193
|
-
for (const item of condensedArray) {
|
|
194
|
-
condensedBodies.set(item.turn, item.condensed);
|
|
195
|
-
}
|
|
196
|
-
// Guard rail: protect uncondensable turns
|
|
197
|
-
for (const turn of originalTurns) {
|
|
198
|
-
if (turn.id.startsWith('U')) {
|
|
199
|
-
const body = turn.body.toLowerCase();
|
|
200
|
-
if (/不要|别|错了|改成|不是|必须|禁止/.test(body)) {
|
|
201
|
-
condensedBodies.set(turn.id, null); // Force keep original
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
// Determine next version number
|
|
206
|
-
const versionNumbers = Object.keys(meta.versions)
|
|
207
|
-
.map(v => parseInt(v.replace('v', '')))
|
|
208
|
-
.filter(n => !isNaN(n));
|
|
373
|
+
// Run iterative condense on base turns (deep copy bodies)
|
|
374
|
+
const workingTurns = baseTurns.map(t => ({ ...t, condensed: /\[c\d+/.test(t.header) }));
|
|
375
|
+
const { condensedTurns, cohesionMap } = await iterativeCondense(workingTurns);
|
|
376
|
+
// Determine next version
|
|
377
|
+
const versionNumbers = Object.keys(meta.versions).map(v => parseInt(v.replace('v', ''))).filter(n => !isNaN(n));
|
|
209
378
|
const nextNum = Math.max(...versionNumbers) + 1;
|
|
210
379
|
const nextVersion = `v${nextNum}`;
|
|
211
|
-
// Build
|
|
212
|
-
const condensedContent =
|
|
380
|
+
// Build final content with markers
|
|
381
|
+
const condensedContent = buildFinalContent(originalTurns, condensedTurns, prevMarkers, nextNum);
|
|
213
382
|
const afterTokens = estimateTokens(condensedContent);
|
|
214
383
|
const beforeTokens = latestEntry.tokens;
|
|
215
384
|
// Write file
|
|
@@ -224,23 +393,24 @@ ${baseContent.length > 40000 ? '...[前文已省略]\n\n' + baseContent.slice(-4
|
|
|
224
393
|
base: meta.latest,
|
|
225
394
|
};
|
|
226
395
|
meta.latest = nextVersion;
|
|
396
|
+
// Merge cohesion map
|
|
397
|
+
meta.cohesion_map = { ...meta.cohesion_map, ...cohesionMap };
|
|
227
398
|
(0, conversation_sync_1.writeMeta)(convDir, meta);
|
|
228
399
|
return { version: nextVersion, before_tokens: beforeTokens, after_tokens: afterTokens };
|
|
229
400
|
}
|
|
230
|
-
// ──
|
|
401
|
+
// ── Public: reorganizeConversation ──
|
|
231
402
|
async function reorganizeConversation(convDir) {
|
|
232
403
|
const meta = (0, conversation_sync_1.readMeta)(convDir);
|
|
233
404
|
if (!meta)
|
|
234
405
|
return null;
|
|
235
406
|
if (meta.reorganize_count >= 2)
|
|
236
|
-
return null;
|
|
237
|
-
// Classify turns by expand stats
|
|
407
|
+
return null;
|
|
238
408
|
const byTurn = meta.expand_stats.by_turn;
|
|
409
|
+
const v0Content = fs.readFileSync(path.join(convDir, 'v0.md'), 'utf-8');
|
|
410
|
+
const originalTurns = parseTurns(v0Content);
|
|
239
411
|
const highAttention = [];
|
|
240
412
|
const lowAttention = [];
|
|
241
413
|
const neverAccessed = [];
|
|
242
|
-
const v0Content = fs.readFileSync(path.join(convDir, 'v0.md'), 'utf-8');
|
|
243
|
-
const originalTurns = parseTurns(v0Content);
|
|
244
414
|
for (const turn of originalTurns) {
|
|
245
415
|
const count = byTurn[turn.id] || 0;
|
|
246
416
|
if (count >= 3)
|
|
@@ -251,54 +421,112 @@ async function reorganizeConversation(convDir) {
|
|
|
251
421
|
neverAccessed.push(turn.id);
|
|
252
422
|
}
|
|
253
423
|
if (highAttention.length === 0)
|
|
254
|
-
return null;
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
3. 保留所有数字、标识符、文件路径
|
|
266
|
-
|
|
267
|
-
对每轮输出 JSON 数组:
|
|
268
|
-
[{"turn":"U1","condensed":"缩减后的内容或null表示保留原文"},...]
|
|
269
|
-
|
|
270
|
-
原始对话:
|
|
271
|
-
${v0Content.length > 40000 ? '...[前文已省略]\n\n' + v0Content.slice(-40000) : v0Content}`;
|
|
272
|
-
let result;
|
|
273
|
-
try {
|
|
274
|
-
result = await callHaiku(prompt);
|
|
275
|
-
}
|
|
276
|
-
catch (err) {
|
|
277
|
-
throw new Error('Claude CLI 调用失败: ' + (err instanceof Error ? err.message : String(err)));
|
|
278
|
-
}
|
|
279
|
-
let condensedArray;
|
|
280
|
-
try {
|
|
281
|
-
condensedArray = extractJsonArray(result);
|
|
282
|
-
}
|
|
283
|
-
catch (parseErr) {
|
|
284
|
-
throw new Error('无法解析 Haiku 返回的 JSON: ' + (parseErr instanceof Error ? parseErr.message : ''));
|
|
285
|
-
}
|
|
286
|
-
const condensedBodies = new Map();
|
|
287
|
-
for (const item of condensedArray) {
|
|
288
|
-
condensedBodies.set(item.turn, item.condensed);
|
|
424
|
+
return null;
|
|
425
|
+
const workingTurns = originalTurns.map(t => ({ ...t, condensed: false }));
|
|
426
|
+
const highSet = new Set(highAttention);
|
|
427
|
+
const halfWindow = HALF_WINDOW_TOKENS;
|
|
428
|
+
let condensedUpTo = 0;
|
|
429
|
+
const cohesionMap = {};
|
|
430
|
+
// Pre-truncate oversized turns
|
|
431
|
+
for (let i = 0; i < workingTurns.length; i++) {
|
|
432
|
+
if (workingTurns[i].tokens > halfWindow * 0.8) {
|
|
433
|
+
workingTurns[i] = truncateTurn(workingTurns[i], Math.floor(halfWindow * 0.7));
|
|
434
|
+
}
|
|
289
435
|
}
|
|
290
|
-
//
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
436
|
+
// Iterative sliding window (same strategy as condense)
|
|
437
|
+
while (condensedUpTo < workingTurns.length) {
|
|
438
|
+
let contextTurns = [];
|
|
439
|
+
let contextTokens = 0;
|
|
440
|
+
if (condensedUpTo > 0) {
|
|
441
|
+
const ctxStart = Math.max(0, condensedUpTo - CONTEXT_OVERLAP);
|
|
442
|
+
for (let i = ctxStart; i < condensedUpTo; i++) {
|
|
443
|
+
if (contextTokens + workingTurns[i].tokens + 20 > BUDGET_FOR_CONTEXT)
|
|
444
|
+
break;
|
|
445
|
+
contextTurns.push(workingTurns[i]);
|
|
446
|
+
contextTokens += workingTurns[i].tokens + 20;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
let contextSummary = '';
|
|
450
|
+
const ctxStartIdx = condensedUpTo > CONTEXT_OVERLAP ? condensedUpTo - CONTEXT_OVERLAP : 0;
|
|
451
|
+
if (ctxStartIdx > 0)
|
|
452
|
+
contextSummary = generateContextSummary(workingTurns.slice(0, ctxStartIdx));
|
|
453
|
+
const reorgRulesTokens = 600; // approximate
|
|
454
|
+
const availableForNew = halfWindow - contextTokens - reorgRulesTokens - estimateTokens(CONDENSE_RULES);
|
|
455
|
+
let newEnd = condensedUpTo;
|
|
456
|
+
let newTokens = 0;
|
|
457
|
+
while (newEnd < workingTurns.length) {
|
|
458
|
+
const cost = workingTurns[newEnd].tokens + 20;
|
|
459
|
+
if (newTokens + cost > availableForNew && newEnd > condensedUpTo)
|
|
460
|
+
break;
|
|
461
|
+
newTokens += cost;
|
|
462
|
+
newEnd++;
|
|
463
|
+
}
|
|
464
|
+
if (newEnd <= condensedUpTo)
|
|
465
|
+
break;
|
|
466
|
+
// Build reorganize prompt with attention hints
|
|
467
|
+
const promptParts = [
|
|
468
|
+
`你是一个对话重整器。基于使用数据,激进缩减对话。`,
|
|
469
|
+
``,
|
|
470
|
+
`高关注轮次(用户反复查看,保留更多细节):${highAttention.join(', ')}`,
|
|
471
|
+
`低关注轮次(大幅缩减为一句话):${lowAttention.join(', ')}`,
|
|
472
|
+
`从未访问的轮次(高度缩减为几个字):${neverAccessed.join(', ')}`,
|
|
473
|
+
``,
|
|
474
|
+
`## 必须激进缩减的内容:`,
|
|
475
|
+
`- 工具调用和输出 → "执行了 X,结果:Y"`,
|
|
476
|
+
`- 构建/发布日志 → "构建成功" 或 "发布 vX.Y.Z"`,
|
|
477
|
+
`- 代码修改描述 → "修改了 file.ts"`,
|
|
478
|
+
`- 文件内容展示 → "读取了 file.ts"`,
|
|
479
|
+
`- LLM 解释性文字("让我检查""我来看看") → 删除`,
|
|
480
|
+
``,
|
|
481
|
+
`## 必须保留原文(condensed 设为 null):`,
|
|
482
|
+
`- 高关注轮次中的用户需求和决策讨论`,
|
|
483
|
+
`- 用户纠正/否定("不对""错了""改成")`,
|
|
484
|
+
`- 标记了 [已缩减] 的轮次`,
|
|
485
|
+
``,
|
|
486
|
+
`## 不得违反:绝不改变语义方向,保留版本号和文件路径`,
|
|
487
|
+
``,
|
|
488
|
+
`对每轮输出 JSON:[{"turn":"U1","condensed":"缩减内容或null","cohesion":0.8},...]`,
|
|
489
|
+
];
|
|
490
|
+
if (contextSummary)
|
|
491
|
+
promptParts.push(``, `[前文摘要:${contextSummary}]`);
|
|
492
|
+
promptParts.push(``, `对话:`);
|
|
493
|
+
for (const ct of contextTurns) {
|
|
494
|
+
promptParts.push(`## ${ct.id} [已缩减]`, ct.body, '');
|
|
495
|
+
}
|
|
496
|
+
for (let i = condensedUpTo; i < newEnd; i++) {
|
|
497
|
+
promptParts.push(`## ${workingTurns[i].id}`, workingTurns[i].body, '');
|
|
498
|
+
}
|
|
499
|
+
const results = await callHaikuAndParse(promptParts.join('\n'), workingTurns, condensedUpTo, newEnd, condensedUpTo);
|
|
500
|
+
for (let i = condensedUpTo; i < newEnd && i < workingTurns.length; i++) {
|
|
501
|
+
const t = workingTurns[i];
|
|
502
|
+
const r = results.get(t.id);
|
|
503
|
+
if (r?.cohesion !== undefined) {
|
|
504
|
+
t.cohesion = r.cohesion;
|
|
505
|
+
cohesionMap[t.id] = r.cohesion;
|
|
506
|
+
}
|
|
507
|
+
if (t.condensed)
|
|
508
|
+
continue;
|
|
509
|
+
if (t.id.startsWith('U') && UNCONDENSABLE_RE.test(t.body)) {
|
|
510
|
+
t.condensed = true;
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
if (highSet.has(t.id)) {
|
|
514
|
+
t.condensed = true;
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
if (r && r.condensed !== null && r.condensed !== undefined) {
|
|
518
|
+
t.body = r.condensed;
|
|
519
|
+
t.tokens = estimateTokens(r.condensed);
|
|
520
|
+
}
|
|
521
|
+
t.condensed = true;
|
|
294
522
|
}
|
|
523
|
+
condensedUpTo = newEnd;
|
|
295
524
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
.filter(n => !isNaN(n));
|
|
525
|
+
// Build final
|
|
526
|
+
const versionNumbers = Object.keys(meta.versions).map(v => parseInt(v.replace('v', ''))).filter(n => !isNaN(n));
|
|
299
527
|
const nextNum = Math.max(...versionNumbers) + 1;
|
|
300
528
|
const nextVersion = `v${nextNum}`;
|
|
301
|
-
const condensedContent =
|
|
529
|
+
const condensedContent = buildFinalContent(originalTurns, workingTurns, new Map(), nextNum);
|
|
302
530
|
const afterTokens = estimateTokens(condensedContent);
|
|
303
531
|
const latestEntry = meta.versions[meta.latest];
|
|
304
532
|
const beforeTokens = latestEntry?.tokens ?? meta.original_tokens;
|
|
@@ -315,6 +543,7 @@ ${v0Content.length > 40000 ? '...[前文已省略]\n\n' + v0Content.slice(-40000
|
|
|
315
543
|
meta.latest = nextVersion;
|
|
316
544
|
meta.reorganize_count += 1;
|
|
317
545
|
meta.last_reorganize_at = new Date().toISOString();
|
|
546
|
+
meta.cohesion_map = { ...meta.cohesion_map, ...cohesionMap };
|
|
318
547
|
(0, conversation_sync_1.writeMeta)(convDir, meta);
|
|
319
548
|
return { version: nextVersion, before_tokens: beforeTokens, after_tokens: afterTokens, high_attention_turns: highAttention };
|
|
320
549
|
}
|