billion-context-dsh 0.2.3 → 0.2.5
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.en.md +5 -5
- package/README.md +5 -5
- package/dist/index.js +2769 -64
- package/dist/index.js.map +1 -1
- package/dist/nudge.d.ts +3 -0
- package/dist/region.d.ts +13 -2
- package/package.json +3 -5
package/dist/index.js
CHANGED
|
@@ -3,10 +3,2637 @@ import {
|
|
|
3
3
|
CompactionEngine,
|
|
4
4
|
ManualCompactionError
|
|
5
5
|
} from "@deepseek-ai/dsh-compaction";
|
|
6
|
-
import { createCore } from "acp-kernel";
|
|
7
6
|
|
|
8
|
-
//
|
|
9
|
-
import {
|
|
7
|
+
// node_modules/acp-kernel/dist/index.js
|
|
8
|
+
import { createRequire } from "module";
|
|
9
|
+
var REF_WIDTH = 5;
|
|
10
|
+
var MIN_INDEX = 1;
|
|
11
|
+
var MAX_INDEX = 99999;
|
|
12
|
+
var REF_PATTERN = /^m0*(\d{1,5})$/;
|
|
13
|
+
var BLOCKED_REF = "BLOCKED";
|
|
14
|
+
function indexToRef(index) {
|
|
15
|
+
if (!Number.isInteger(index) || index < MIN_INDEX || index > MAX_INDEX) {
|
|
16
|
+
throw new RangeError(
|
|
17
|
+
`ref index out of bounds: ${index} (allowed ${MIN_INDEX}-${MAX_INDEX})`
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
return `m${String(index).padStart(REF_WIDTH, "0")}`;
|
|
21
|
+
}
|
|
22
|
+
function refToIndex(ref) {
|
|
23
|
+
const match = REF_PATTERN.exec(ref.trim().toLowerCase());
|
|
24
|
+
if (!match) return null;
|
|
25
|
+
const index = Number(match[1]);
|
|
26
|
+
if (index < MIN_INDEX || index > MAX_INDEX) return null;
|
|
27
|
+
return index;
|
|
28
|
+
}
|
|
29
|
+
function refForRaw(map, rawId) {
|
|
30
|
+
return map.byRaw[rawId] ?? null;
|
|
31
|
+
}
|
|
32
|
+
function assignRefs(messages, options) {
|
|
33
|
+
const map = {
|
|
34
|
+
byRaw: { ...options.existing.byRaw },
|
|
35
|
+
byRef: { ...options.existing.byRef }
|
|
36
|
+
};
|
|
37
|
+
let cursor = Number.isInteger(options.nextIndex) && options.nextIndex >= MIN_INDEX ? options.nextIndex : MIN_INDEX;
|
|
38
|
+
let newlyAssigned = 0;
|
|
39
|
+
for (const message of messages) {
|
|
40
|
+
if (!message.id || options.shouldSkip?.(message)) continue;
|
|
41
|
+
if (map.byRaw[message.id]) continue;
|
|
42
|
+
if (options.isProtected?.(message)) {
|
|
43
|
+
map.byRaw[message.id] = BLOCKED_REF;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const ref = allocateFreeRef(map, cursor);
|
|
47
|
+
cursor = ref.index + 1;
|
|
48
|
+
map.byRaw[message.id] = ref.text;
|
|
49
|
+
map.byRef[ref.text] = message.id;
|
|
50
|
+
newlyAssigned++;
|
|
51
|
+
}
|
|
52
|
+
return { map, nextIndex: cursor, newlyAssigned };
|
|
53
|
+
}
|
|
54
|
+
function allocateFreeRef(map, start) {
|
|
55
|
+
let candidate = Math.max(start, MIN_INDEX);
|
|
56
|
+
while (candidate <= MAX_INDEX) {
|
|
57
|
+
const text = indexToRef(candidate);
|
|
58
|
+
if (!map.byRef[text]) {
|
|
59
|
+
return { text, index: candidate };
|
|
60
|
+
}
|
|
61
|
+
candidate++;
|
|
62
|
+
}
|
|
63
|
+
throw new Error(
|
|
64
|
+
`ref capacity exhausted: cannot allocate beyond ${indexToRef(MAX_INDEX)}`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
function highestUsedIndex(map) {
|
|
68
|
+
let highest = 0;
|
|
69
|
+
for (const ref of Object.values(map.byRaw)) {
|
|
70
|
+
const index = ref === BLOCKED_REF ? null : refToIndex(ref);
|
|
71
|
+
if (index !== null && index > highest) highest = index;
|
|
72
|
+
}
|
|
73
|
+
return highest;
|
|
74
|
+
}
|
|
75
|
+
function createInitialState() {
|
|
76
|
+
return {
|
|
77
|
+
blocks: [],
|
|
78
|
+
messageRefs: { byRaw: {}, byRef: {} },
|
|
79
|
+
tokenSnapshot: {},
|
|
80
|
+
nudge: {
|
|
81
|
+
lastPerMessageNudgeTokens: 0,
|
|
82
|
+
lastNudgeShownTokens: 0,
|
|
83
|
+
baselineTokens: 0,
|
|
84
|
+
anchors: {},
|
|
85
|
+
lastShownByTier: {}
|
|
86
|
+
},
|
|
87
|
+
stats: { tokensCompressed: 0, compressionCount: 0 },
|
|
88
|
+
nextBlockId: 1,
|
|
89
|
+
nextRunId: 1
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function allocateBlockId(state) {
|
|
93
|
+
const id = state.nextBlockId;
|
|
94
|
+
state.nextBlockId = Math.max(1, id) + 1;
|
|
95
|
+
return `b${id}`;
|
|
96
|
+
}
|
|
97
|
+
function allocateRunId(state) {
|
|
98
|
+
const id = state.nextRunId;
|
|
99
|
+
state.nextRunId = Math.max(1, id) + 1;
|
|
100
|
+
return `r${id}`;
|
|
101
|
+
}
|
|
102
|
+
function blockById(state, blockId) {
|
|
103
|
+
return state.blocks.find((block) => block.blockId === blockId);
|
|
104
|
+
}
|
|
105
|
+
function activeBlocks(state) {
|
|
106
|
+
return state.blocks.filter((block) => block.active);
|
|
107
|
+
}
|
|
108
|
+
function coveredMessageIds(state) {
|
|
109
|
+
const covered = /* @__PURE__ */ new Set();
|
|
110
|
+
for (const block of state.blocks) {
|
|
111
|
+
if (!block.active) continue;
|
|
112
|
+
for (const id of block.effectiveMessageIds) covered.add(id);
|
|
113
|
+
}
|
|
114
|
+
return covered;
|
|
115
|
+
}
|
|
116
|
+
function advanceSurvival(state, promotionThreshold) {
|
|
117
|
+
for (const block of state.blocks) {
|
|
118
|
+
if (!block.active) continue;
|
|
119
|
+
block.survivedCount += 1;
|
|
120
|
+
if (block.survivedCount >= promotionThreshold) {
|
|
121
|
+
block.generation = "old";
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
var SUMMARY_HEADER = "[Compressed conversation section]";
|
|
126
|
+
function prune(messages, state, options = {}) {
|
|
127
|
+
const covered = coveredMessageIds(state);
|
|
128
|
+
if (covered.size === 0) return [...messages];
|
|
129
|
+
const inject = options.injectSummaries ?? true;
|
|
130
|
+
const firstUserIndex = messages.findIndex(
|
|
131
|
+
(message) => message.role === "user"
|
|
132
|
+
);
|
|
133
|
+
const indexById = /* @__PURE__ */ new Map();
|
|
134
|
+
messages.forEach((message, index) => indexById.set(message.id, index));
|
|
135
|
+
const anchors = inject ? collectSummaryAnchors(state, indexById) : [];
|
|
136
|
+
return stripOrphanedReasoning(
|
|
137
|
+
stripOrphanedToolResults(
|
|
138
|
+
stripOrphanedToolCalls(
|
|
139
|
+
rebuildMessages(messages, covered, firstUserIndex, anchors)
|
|
140
|
+
)
|
|
141
|
+
)
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
function collectSummaryAnchors(state, indexById) {
|
|
145
|
+
const anchors = [];
|
|
146
|
+
for (const block of activeBlocks(state)) {
|
|
147
|
+
let earliest = null;
|
|
148
|
+
for (const id of block.effectiveMessageIds) {
|
|
149
|
+
const index = indexById.get(id);
|
|
150
|
+
if (index !== void 0 && (earliest === null || index < earliest)) {
|
|
151
|
+
earliest = index;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
anchors.push({
|
|
155
|
+
blockId: block.blockId,
|
|
156
|
+
summary: block.summary,
|
|
157
|
+
topic: block.topic,
|
|
158
|
+
insertAt: earliest ?? 0
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
anchors.sort((left, right) => left.insertAt - right.insertAt);
|
|
162
|
+
return anchors;
|
|
163
|
+
}
|
|
164
|
+
function rebuildMessages(messages, covered, firstUserIndex, anchors) {
|
|
165
|
+
const result = [];
|
|
166
|
+
const pending = [...anchors];
|
|
167
|
+
for (let index = 0; index < messages.length; index++) {
|
|
168
|
+
while (pending.length > 0 && pending[0].insertAt === index) {
|
|
169
|
+
result.push(renderSummary(pending.shift()));
|
|
170
|
+
}
|
|
171
|
+
if (index === firstUserIndex && firstUserIndex >= 0) {
|
|
172
|
+
result.push(messages[index]);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (covered.has(messages[index].id)) continue;
|
|
176
|
+
result.push(messages[index]);
|
|
177
|
+
}
|
|
178
|
+
while (pending.length > 0) {
|
|
179
|
+
result.push(renderSummary(pending.shift()));
|
|
180
|
+
}
|
|
181
|
+
return result;
|
|
182
|
+
}
|
|
183
|
+
function renderSummary(anchor) {
|
|
184
|
+
const body = anchor.summary.trim();
|
|
185
|
+
const topicLine = anchor.topic ? `${SUMMARY_HEADER} \u2014 ${anchor.topic}` : SUMMARY_HEADER;
|
|
186
|
+
const text = body.length === 0 ? topicLine : `${topicLine}
|
|
187
|
+
${body}`;
|
|
188
|
+
return {
|
|
189
|
+
id: `acp_summary_${anchor.blockId}`,
|
|
190
|
+
role: "system",
|
|
191
|
+
contentType: "text",
|
|
192
|
+
text
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
function stripOrphanedToolResults(messages) {
|
|
196
|
+
const knownCallIds = /* @__PURE__ */ new Set();
|
|
197
|
+
for (const m of messages) {
|
|
198
|
+
if (m.contentType === "tool-call" && m.toolCallId) {
|
|
199
|
+
knownCallIds.add(m.toolCallId);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return messages.filter(
|
|
203
|
+
(m) => m.contentType !== "tool-result" || !m.toolCallId || knownCallIds.has(m.toolCallId)
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
function stripOrphanedToolCalls(messages) {
|
|
207
|
+
const knownResultIds = /* @__PURE__ */ new Set();
|
|
208
|
+
for (const m of messages) {
|
|
209
|
+
if (m.contentType === "tool-result" && m.toolCallId) {
|
|
210
|
+
knownResultIds.add(m.toolCallId);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return messages.filter(
|
|
214
|
+
(m) => m.contentType !== "tool-call" || !m.toolCallId || m.toolName === "compress" || knownResultIds.has(m.toolCallId)
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
function stripOrphanedReasoning(messages) {
|
|
218
|
+
const drop = /* @__PURE__ */ new Set();
|
|
219
|
+
for (let i = 0; i < messages.length; i++) {
|
|
220
|
+
if (drop.has(i)) continue;
|
|
221
|
+
if (messages[i].contentType !== "reasoning") continue;
|
|
222
|
+
let j = i;
|
|
223
|
+
while (j + 1 < messages.length && messages[j + 1].contentType === "reasoning") {
|
|
224
|
+
j++;
|
|
225
|
+
}
|
|
226
|
+
const companion = messages[j + 1];
|
|
227
|
+
const hasCompanion = companion !== void 0 && companion.role === "assistant" && (companion.contentType === "text" || companion.contentType === "tool-call");
|
|
228
|
+
if (!hasCompanion) {
|
|
229
|
+
for (let k = i; k <= j; k++) drop.add(k);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (drop.size === 0) return messages;
|
|
233
|
+
return messages.filter((_, i) => !drop.has(i));
|
|
234
|
+
}
|
|
235
|
+
function syncBlocks(messages, state) {
|
|
236
|
+
const presentIds = new Set(messages.map((message) => message.id));
|
|
237
|
+
const deactivated = [];
|
|
238
|
+
const result = {
|
|
239
|
+
blocks: state.blocks.map((block) => ({
|
|
240
|
+
...block,
|
|
241
|
+
directMessageIds: [...block.directMessageIds],
|
|
242
|
+
effectiveMessageIds: [...block.effectiveMessageIds],
|
|
243
|
+
directBlockIds: [...block.directBlockIds]
|
|
244
|
+
})),
|
|
245
|
+
messageRefs: {
|
|
246
|
+
byRaw: { ...state.messageRefs.byRaw },
|
|
247
|
+
byRef: { ...state.messageRefs.byRef }
|
|
248
|
+
},
|
|
249
|
+
// Snapshot is keyed by ref with primitive values — shallow copy suffices.
|
|
250
|
+
tokenSnapshot: { ...state.tokenSnapshot ?? {} },
|
|
251
|
+
nudge: { ...state.nudge, anchors: { ...state.nudge.anchors } },
|
|
252
|
+
stats: { ...state.stats },
|
|
253
|
+
nextBlockId: state.nextBlockId,
|
|
254
|
+
nextRunId: state.nextRunId
|
|
255
|
+
};
|
|
256
|
+
const liveRefs = new Set(
|
|
257
|
+
messages.map((m) => result.messageRefs.byRaw[m.id]).filter((r) => typeof r === "string")
|
|
258
|
+
);
|
|
259
|
+
if (Object.keys(result.tokenSnapshot).length !== liveRefs.size) {
|
|
260
|
+
const pruned = {};
|
|
261
|
+
for (const [ref, n] of Object.entries(result.tokenSnapshot)) {
|
|
262
|
+
if (liveRefs.has(ref)) pruned[ref] = n;
|
|
263
|
+
}
|
|
264
|
+
result.tokenSnapshot = pruned;
|
|
265
|
+
}
|
|
266
|
+
const consumedBlockIds = /* @__PURE__ */ new Set();
|
|
267
|
+
for (const block of result.blocks) {
|
|
268
|
+
for (const consumedId of block.directBlockIds) {
|
|
269
|
+
consumedBlockIds.add(consumedId);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
for (const block of result.blocks) {
|
|
273
|
+
if (consumedBlockIds.has(block.blockId)) {
|
|
274
|
+
block.active = false;
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
block.active = true;
|
|
278
|
+
const stillPresent = block.effectiveMessageIds.some(
|
|
279
|
+
(id) => presentIds.has(id)
|
|
280
|
+
);
|
|
281
|
+
if (!stillPresent) {
|
|
282
|
+
block.active = false;
|
|
283
|
+
deactivated.push(block.blockId);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return { state: result, deactivated };
|
|
287
|
+
}
|
|
288
|
+
var require2 = createRequire(import.meta.url);
|
|
289
|
+
function defaultCountTokens(text) {
|
|
290
|
+
if (!text) return 0;
|
|
291
|
+
const cjk = text.match(/[\u4e00-\u9fff\u3040-\u30ff\uac00-\ud7af]/g);
|
|
292
|
+
const cjkCount = cjk?.length ?? 0;
|
|
293
|
+
return cjkCount + Math.ceil((text.length - cjkCount) / 4);
|
|
294
|
+
}
|
|
295
|
+
function defaultConfig(modelContextLimit, overrides = {}) {
|
|
296
|
+
const base = {
|
|
297
|
+
tiers: { enabled: true, tier2Trigger: 5, tier3Trigger: 10 },
|
|
298
|
+
nudge: {
|
|
299
|
+
maxContextLimitPct: 0.75,
|
|
300
|
+
minContextLimitPct: 0.45,
|
|
301
|
+
frequency: 5,
|
|
302
|
+
iterationThreshold: 15,
|
|
303
|
+
force: "soft",
|
|
304
|
+
growthRatio: 0.05,
|
|
305
|
+
growthFloor: 5e4,
|
|
306
|
+
growthCap: 5e4,
|
|
307
|
+
minGrowthFloor: 2e4,
|
|
308
|
+
minGrowthRatio: 0.45,
|
|
309
|
+
emergencyThresholdPct: 0.95,
|
|
310
|
+
tier2GrowthMultiplier: 1.5
|
|
311
|
+
},
|
|
312
|
+
promotionThreshold: 5,
|
|
313
|
+
truncate: { threshold: 0.95 },
|
|
314
|
+
compress: {
|
|
315
|
+
minCompressRange: 5e3,
|
|
316
|
+
maxSummaryLength: 2e4,
|
|
317
|
+
minSummaryLength: 50
|
|
318
|
+
},
|
|
319
|
+
protectedTools: [],
|
|
320
|
+
preserveRecentMessages: 5,
|
|
321
|
+
preserveRecentTokens: 5e3,
|
|
322
|
+
modelContextLimit
|
|
323
|
+
};
|
|
324
|
+
return {
|
|
325
|
+
...base,
|
|
326
|
+
...overrides,
|
|
327
|
+
tiers: { ...base.tiers, ...overrides.tiers },
|
|
328
|
+
nudge: { ...base.nudge, ...overrides.nudge },
|
|
329
|
+
truncate: { ...base.truncate, ...overrides.truncate },
|
|
330
|
+
compress: { ...base.compress, ...overrides.compress }
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
function validateConfig(config) {
|
|
334
|
+
const errors = [];
|
|
335
|
+
if (!Number.isFinite(config.modelContextLimit) || config.modelContextLimit <= 0) {
|
|
336
|
+
errors.push("modelContextLimit must be a positive number");
|
|
337
|
+
}
|
|
338
|
+
if (config.nudge.minContextLimitPct > config.nudge.maxContextLimitPct) {
|
|
339
|
+
errors.push(
|
|
340
|
+
"nudge.minContextLimitPct must not exceed nudge.maxContextLimitPct"
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
if (config.nudge.maxContextLimitPct > config.nudge.emergencyThresholdPct) {
|
|
344
|
+
errors.push(
|
|
345
|
+
"nudge.maxContextLimitPct must not exceed nudge.emergencyThresholdPct"
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
if (config.promotionThreshold < 1) {
|
|
349
|
+
errors.push("promotionThreshold must be >= 1");
|
|
350
|
+
}
|
|
351
|
+
if (config.truncate.threshold <= 0 || config.truncate.threshold > 1) {
|
|
352
|
+
errors.push("truncate.threshold must be in (0, 1]");
|
|
353
|
+
}
|
|
354
|
+
for (const tier of [config.tiers.tier2Trigger, config.tiers.tier3Trigger]) {
|
|
355
|
+
if (tier < 1) errors.push("tier triggers must be >= 1");
|
|
356
|
+
}
|
|
357
|
+
if (config.tiers.tier3Trigger <= config.tiers.tier2Trigger) {
|
|
358
|
+
errors.push("tiers.tier3Trigger must be greater than tiers.tier2Trigger");
|
|
359
|
+
}
|
|
360
|
+
return errors;
|
|
361
|
+
}
|
|
362
|
+
var MESSAGE_REF_PATTERN = /^m0*(\d{1,5})$/;
|
|
363
|
+
var BLOCK_REF_PATTERN = /^b(\d{1,9})$/;
|
|
364
|
+
function parseBoundary(ref) {
|
|
365
|
+
const normalized = ref.trim().toLowerCase();
|
|
366
|
+
const messageMatch = MESSAGE_REF_PATTERN.exec(normalized);
|
|
367
|
+
if (messageMatch) {
|
|
368
|
+
const numericId = Number(messageMatch[1]);
|
|
369
|
+
if (numericId >= 1 && numericId <= 99999) {
|
|
370
|
+
return { kind: "message", numericId, raw: normalized };
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
const blockMatch = BLOCK_REF_PATTERN.exec(normalized);
|
|
374
|
+
if (blockMatch) {
|
|
375
|
+
const numericId = Number(blockMatch[1]);
|
|
376
|
+
if (numericId >= 1) return { kind: "block", numericId, raw: normalized };
|
|
377
|
+
}
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
var BoundaryNotFoundError = class extends Error {
|
|
381
|
+
code = "BOUNDARY_NOT_FOUND";
|
|
382
|
+
kind;
|
|
383
|
+
endpoint;
|
|
384
|
+
constructor(kind, endpoint, message) {
|
|
385
|
+
super(message);
|
|
386
|
+
this.name = "BoundaryNotFoundError";
|
|
387
|
+
this.code = "BOUNDARY_NOT_FOUND";
|
|
388
|
+
this.kind = kind;
|
|
389
|
+
this.endpoint = endpoint;
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
function resolveBoundaries(input) {
|
|
393
|
+
const start = parseBoundary(input.startRef);
|
|
394
|
+
const end = parseBoundary(input.endRef);
|
|
395
|
+
if (!start || !end) {
|
|
396
|
+
throw new Error(
|
|
397
|
+
`Invalid boundary ref(s): startId="${input.startRef}", endId="${input.endRef}". Use mNNNNN or bN.`
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
const indexByRawId = /* @__PURE__ */ new Map();
|
|
401
|
+
input.messages.forEach(
|
|
402
|
+
(message, index) => indexByRawId.set(message.id, index)
|
|
403
|
+
);
|
|
404
|
+
let startIndex = resolveAnchorIndex(start, input.state, indexByRawId, "start");
|
|
405
|
+
let endIndex = resolveAnchorIndex(end, input.state, indexByRawId, "end");
|
|
406
|
+
if (startIndex > endIndex) {
|
|
407
|
+
[startIndex, endIndex] = [endIndex, startIndex];
|
|
408
|
+
}
|
|
409
|
+
const messageIds = [];
|
|
410
|
+
for (let index = startIndex; index <= endIndex; index++) {
|
|
411
|
+
const message = input.messages[index];
|
|
412
|
+
if (message) messageIds.push(message.id);
|
|
413
|
+
}
|
|
414
|
+
const boundaryKind = start.kind === "block" || end.kind === "block" ? "block" : "message";
|
|
415
|
+
const nestedBlockIds = [];
|
|
416
|
+
const nestedSeen = /* @__PURE__ */ new Set();
|
|
417
|
+
for (const block of activeBlocks(input.state)) {
|
|
418
|
+
const anchor = earliestIndexOfIds(block.effectiveMessageIds, indexByRawId);
|
|
419
|
+
if (anchor !== null && anchor >= startIndex && anchor <= endIndex) {
|
|
420
|
+
if (!nestedSeen.has(block.blockId)) {
|
|
421
|
+
nestedSeen.add(block.blockId);
|
|
422
|
+
nestedBlockIds.push(block.blockId);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
const protectedGaps = [];
|
|
427
|
+
return {
|
|
428
|
+
startIndex,
|
|
429
|
+
endIndex,
|
|
430
|
+
messageIds,
|
|
431
|
+
nestedBlockIds,
|
|
432
|
+
boundaryKind,
|
|
433
|
+
protectedGaps
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
function resolveAnchorIndex(boundary, state, indexByRawId, endpoint) {
|
|
437
|
+
const label = endpoint === "start" ? "startId" : "endId";
|
|
438
|
+
if (boundary.kind === "message") {
|
|
439
|
+
const rawId = state.messageRefs.byRef[boundary.raw] ?? state.messageRefs.byRef[formatPaddedRef(boundary.numericId)];
|
|
440
|
+
if (!rawId) {
|
|
441
|
+
throw new BoundaryNotFoundError(
|
|
442
|
+
"unknown",
|
|
443
|
+
endpoint,
|
|
444
|
+
`${label}="${boundary.raw}" does not exist in this session (typo or wrong session) \u2014 run acp_status for current refs.`
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
const index = indexByRawId.get(rawId);
|
|
448
|
+
if (index === void 0) {
|
|
449
|
+
throw new BoundaryNotFoundError(
|
|
450
|
+
"consumed",
|
|
451
|
+
endpoint,
|
|
452
|
+
`${label}="${boundary.raw}" not found in visible context (likely consumed by an existing block).`
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
return index;
|
|
456
|
+
}
|
|
457
|
+
const block = blockById(state, `b${boundary.numericId}`);
|
|
458
|
+
if (!block) {
|
|
459
|
+
throw new BoundaryNotFoundError(
|
|
460
|
+
"unknown",
|
|
461
|
+
endpoint,
|
|
462
|
+
`${label}="b${boundary.numericId}" does not exist in this session (typo or wrong session) \u2014 run acp_status for current refs.`
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
if (!block.active) {
|
|
466
|
+
throw new BoundaryNotFoundError(
|
|
467
|
+
"consumed",
|
|
468
|
+
endpoint,
|
|
469
|
+
`${label}="b${boundary.numericId}" not found in visible context (block distilled/consumed by a higher-tier block).`
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
const anchor = earliestIndexOfIds(block.effectiveMessageIds, indexByRawId);
|
|
473
|
+
if (anchor === null) {
|
|
474
|
+
throw new BoundaryNotFoundError(
|
|
475
|
+
"consumed",
|
|
476
|
+
endpoint,
|
|
477
|
+
`${label}="b${boundary.numericId}" not found in visible context (block messages consumed by a higher-tier block).`
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
return anchor;
|
|
481
|
+
}
|
|
482
|
+
function formatPaddedRef(index) {
|
|
483
|
+
return `m${String(index).padStart(5, "0")}`;
|
|
484
|
+
}
|
|
485
|
+
function earliestIndexOfIds(ids, indexByRawId) {
|
|
486
|
+
let earliest = null;
|
|
487
|
+
for (const id of ids) {
|
|
488
|
+
const index = indexByRawId.get(id);
|
|
489
|
+
if (index !== void 0 && (earliest === null || index < earliest)) {
|
|
490
|
+
earliest = index;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
return earliest;
|
|
494
|
+
}
|
|
495
|
+
var TRUNCATION_MARKER = "[truncated for context space]";
|
|
496
|
+
var DEFAULTS = {
|
|
497
|
+
minOutputTokens: 1e3,
|
|
498
|
+
keepPrefixChars: 2e3,
|
|
499
|
+
keepSuffixChars: 2e3,
|
|
500
|
+
protectRecentMessages: 3
|
|
501
|
+
};
|
|
502
|
+
function truncateLargeToolOutputs(messages, tokenCount, config, countTokens, options = {}) {
|
|
503
|
+
const opts = { ...DEFAULTS, ...options };
|
|
504
|
+
if (config.modelContextLimit <= 0) return { messages, truncatedCount: 0, savedTokens: 0 };
|
|
505
|
+
const threshold = config.truncate.threshold * config.modelContextLimit;
|
|
506
|
+
if (tokenCount < threshold) return { messages, truncatedCount: 0, savedTokens: 0 };
|
|
507
|
+
const protectedIndex = messages.length - opts.protectRecentMessages;
|
|
508
|
+
const candidates = [];
|
|
509
|
+
for (let index = 0; index < messages.length; index++) {
|
|
510
|
+
if (index >= protectedIndex) break;
|
|
511
|
+
const message = messages[index];
|
|
512
|
+
if (message.contentType !== "tool-result") continue;
|
|
513
|
+
const text = message.text ?? "";
|
|
514
|
+
if (text.length === 0 || text.includes(TRUNCATION_MARKER)) continue;
|
|
515
|
+
const tokens = countTokens(text);
|
|
516
|
+
if (tokens < opts.minOutputTokens) continue;
|
|
517
|
+
candidates.push({ index, tokens });
|
|
518
|
+
}
|
|
519
|
+
if (candidates.length === 0) return { messages, truncatedCount: 0, savedTokens: 0 };
|
|
520
|
+
candidates.sort((left, right) => right.tokens - left.tokens);
|
|
521
|
+
const targetTokens = threshold * 0.9;
|
|
522
|
+
let savedTokens = 0;
|
|
523
|
+
const edits = /* @__PURE__ */ new Map();
|
|
524
|
+
let truncatedCount = 0;
|
|
525
|
+
for (const candidate of candidates) {
|
|
526
|
+
if (tokenCount - savedTokens <= targetTokens) break;
|
|
527
|
+
const original = messages[candidate.index].text ?? "";
|
|
528
|
+
if (original.length <= opts.keepPrefixChars + opts.keepSuffixChars) continue;
|
|
529
|
+
const prefix = original.slice(0, opts.keepPrefixChars);
|
|
530
|
+
const suffix = original.slice(-opts.keepSuffixChars);
|
|
531
|
+
const replacement = prefix + `
|
|
532
|
+
|
|
533
|
+
...${TRUNCATION_MARKER} \u2014 original ~${candidate.tokens} tokens]...
|
|
534
|
+
|
|
535
|
+
` + suffix;
|
|
536
|
+
edits.set(candidate.index, replacement);
|
|
537
|
+
savedTokens += candidate.tokens - countTokens(replacement);
|
|
538
|
+
truncatedCount++;
|
|
539
|
+
}
|
|
540
|
+
if (truncatedCount === 0) return { messages, truncatedCount: 0, savedTokens: 0 };
|
|
541
|
+
const updated = messages.map(
|
|
542
|
+
(message, index) => edits.has(index) ? { ...message, text: edits.get(index) } : message
|
|
543
|
+
);
|
|
544
|
+
return { messages: updated, truncatedCount, savedTokens };
|
|
545
|
+
}
|
|
546
|
+
var KEEP_LAST_ORPHANED = 0;
|
|
547
|
+
function rangeKey(startRef, endRef) {
|
|
548
|
+
return `${startRef}::${endRef}`;
|
|
549
|
+
}
|
|
550
|
+
function rewriteCompressText(text, liveKeys) {
|
|
551
|
+
let parsed;
|
|
552
|
+
try {
|
|
553
|
+
parsed = JSON.parse(text ?? "");
|
|
554
|
+
} catch {
|
|
555
|
+
return null;
|
|
556
|
+
}
|
|
557
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
558
|
+
const obj = parsed;
|
|
559
|
+
const content = obj.content;
|
|
560
|
+
if (!Array.isArray(content) || content.length === 0) return null;
|
|
561
|
+
const kept = content.filter((entry) => {
|
|
562
|
+
if (!entry || typeof entry !== "object") return false;
|
|
563
|
+
const s = typeof entry.startId === "string" ? entry.startId : typeof entry.messageId === "string" ? entry.messageId : "";
|
|
564
|
+
const e = typeof entry.endId === "string" ? entry.endId : typeof entry.messageId === "string" ? entry.messageId : "";
|
|
565
|
+
return liveKeys.has(rangeKey(s, e));
|
|
566
|
+
});
|
|
567
|
+
if (kept.length === content.length || kept.length === 0) return null;
|
|
568
|
+
return JSON.stringify({ ...obj, content: kept });
|
|
569
|
+
}
|
|
570
|
+
function hideConsumedCompressCalls(state, messages) {
|
|
571
|
+
const allBlockCallIds = /* @__PURE__ */ new Set();
|
|
572
|
+
const activeCallIds = /* @__PURE__ */ new Set();
|
|
573
|
+
const liveRangeKeysByCallId = /* @__PURE__ */ new Map();
|
|
574
|
+
const legacyLiveByCallId = /* @__PURE__ */ new Set();
|
|
575
|
+
for (const block of state.blocks) {
|
|
576
|
+
if (!block.compressCallId) continue;
|
|
577
|
+
allBlockCallIds.add(block.compressCallId);
|
|
578
|
+
if (!block.active) continue;
|
|
579
|
+
activeCallIds.add(block.compressCallId);
|
|
580
|
+
if (block.startRef === void 0 || block.endRef === void 0) {
|
|
581
|
+
legacyLiveByCallId.add(block.compressCallId);
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
let keys = liveRangeKeysByCallId.get(block.compressCallId);
|
|
585
|
+
if (!keys) {
|
|
586
|
+
keys = /* @__PURE__ */ new Set();
|
|
587
|
+
liveRangeKeysByCallId.set(block.compressCallId, keys);
|
|
588
|
+
}
|
|
589
|
+
keys.add(rangeKey(block.startRef, block.endRef));
|
|
590
|
+
}
|
|
591
|
+
const lastOrphanedCallIds = [];
|
|
592
|
+
for (let i = messages.length - 1; i >= 0 && lastOrphanedCallIds.length < KEEP_LAST_ORPHANED; i--) {
|
|
593
|
+
const message = messages[i];
|
|
594
|
+
if (message.toolName !== "compress" || message.contentType !== "tool-call") continue;
|
|
595
|
+
const callId = message.toolCallId;
|
|
596
|
+
if (callId && !allBlockCallIds.has(callId)) {
|
|
597
|
+
lastOrphanedCallIds.push(callId);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
const keepCallIds = /* @__PURE__ */ new Set([...activeCallIds, ...lastOrphanedCallIds]);
|
|
601
|
+
const hiddenCallIds = /* @__PURE__ */ new Set();
|
|
602
|
+
for (const message of messages) {
|
|
603
|
+
if (message.toolName === "compress" && message.contentType === "tool-call" && (!message.toolCallId || !keepCallIds.has(message.toolCallId))) {
|
|
604
|
+
if (message.toolCallId) hiddenCallIds.add(message.toolCallId);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
let hidden = 0;
|
|
608
|
+
const result = [];
|
|
609
|
+
for (const message of messages) {
|
|
610
|
+
if (message.toolName === "compress" && message.contentType === "tool-call" && (!message.toolCallId || !keepCallIds.has(message.toolCallId))) {
|
|
611
|
+
hidden++;
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
614
|
+
if (message.contentType === "tool-result" && message.toolCallId && hiddenCallIds.has(message.toolCallId)) {
|
|
615
|
+
hidden++;
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
618
|
+
if (message.toolName === "compress" && message.contentType === "tool-call" && message.toolCallId && keepCallIds.has(message.toolCallId)) {
|
|
619
|
+
const liveKeys = liveRangeKeysByCallId.get(message.toolCallId);
|
|
620
|
+
if (liveKeys && liveKeys.size > 0 && !legacyLiveByCallId.has(message.toolCallId)) {
|
|
621
|
+
const rewritten = rewriteCompressText(message.text, liveKeys);
|
|
622
|
+
if (rewritten !== null) {
|
|
623
|
+
result.push({ ...message, text: rewritten });
|
|
624
|
+
continue;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
result.push(message);
|
|
629
|
+
}
|
|
630
|
+
return { messages: result, hidden };
|
|
631
|
+
}
|
|
632
|
+
var registry = /* @__PURE__ */ new Map();
|
|
633
|
+
function listMessageFilters() {
|
|
634
|
+
return [...registry.values()];
|
|
635
|
+
}
|
|
636
|
+
function applyMessageFilters(messages, config) {
|
|
637
|
+
if (!config?.enabled) {
|
|
638
|
+
return { messages, partsFiltered: 0, partsDropped: 0, partsModified: 0 };
|
|
639
|
+
}
|
|
640
|
+
const active = listMessageFilters().filter(
|
|
641
|
+
(filter) => config.filters?.[filter.name]?.enabled !== false
|
|
642
|
+
);
|
|
643
|
+
if (active.length === 0) {
|
|
644
|
+
return { messages, partsFiltered: 0, partsDropped: 0, partsModified: 0 };
|
|
645
|
+
}
|
|
646
|
+
let working = messages.map((message) => ({ ...message }));
|
|
647
|
+
const tally = { partsFiltered: 0, partsDropped: 0, partsModified: 0 };
|
|
648
|
+
const total = working.length;
|
|
649
|
+
const immediate = active.filter((filter) => !filter.keepLastOnly);
|
|
650
|
+
for (let index = 0; index < working.length; index++) {
|
|
651
|
+
const message = working[index];
|
|
652
|
+
const text = message.text ?? "";
|
|
653
|
+
if (text.length === 0) continue;
|
|
654
|
+
let current = text;
|
|
655
|
+
const baseCtx = {
|
|
656
|
+
text: current,
|
|
657
|
+
role: message.role,
|
|
658
|
+
messageIndex: index,
|
|
659
|
+
totalMessages: total,
|
|
660
|
+
toolName: message.toolName
|
|
661
|
+
};
|
|
662
|
+
for (const filter of immediate) {
|
|
663
|
+
let decision;
|
|
664
|
+
try {
|
|
665
|
+
decision = filter.filter(baseCtx);
|
|
666
|
+
} catch {
|
|
667
|
+
continue;
|
|
668
|
+
}
|
|
669
|
+
if (decision.action === "keep") continue;
|
|
670
|
+
tally.partsFiltered++;
|
|
671
|
+
if (decision.action === "drop") {
|
|
672
|
+
current = "";
|
|
673
|
+
tally.partsDropped++;
|
|
674
|
+
} else if (decision.action === "modify" && decision.text !== void 0) {
|
|
675
|
+
current = decision.text;
|
|
676
|
+
tally.partsModified++;
|
|
677
|
+
}
|
|
678
|
+
baseCtx.text = current;
|
|
679
|
+
}
|
|
680
|
+
if (current !== text) working[index] = { ...message, text: current };
|
|
681
|
+
}
|
|
682
|
+
const keepLast = active.filter((filter) => filter.keepLastOnly);
|
|
683
|
+
for (const filter of keepLast) {
|
|
684
|
+
let foundLast = false;
|
|
685
|
+
for (let index = working.length - 1; index >= 0; index--) {
|
|
686
|
+
const message = working[index];
|
|
687
|
+
const text = message.text ?? "";
|
|
688
|
+
if (text.length === 0) continue;
|
|
689
|
+
const ctx = {
|
|
690
|
+
text,
|
|
691
|
+
role: message.role,
|
|
692
|
+
messageIndex: index,
|
|
693
|
+
totalMessages: total,
|
|
694
|
+
toolName: message.toolName
|
|
695
|
+
};
|
|
696
|
+
let decision;
|
|
697
|
+
try {
|
|
698
|
+
decision = filter.filter(ctx);
|
|
699
|
+
} catch {
|
|
700
|
+
continue;
|
|
701
|
+
}
|
|
702
|
+
if (decision.action !== "drop" && decision.action !== "modify") continue;
|
|
703
|
+
if (foundLast) {
|
|
704
|
+
tally.partsFiltered++;
|
|
705
|
+
tally.partsDropped++;
|
|
706
|
+
working[index] = { ...message, text: "" };
|
|
707
|
+
} else {
|
|
708
|
+
foundLast = true;
|
|
709
|
+
if (decision.action === "modify" && decision.text !== void 0) {
|
|
710
|
+
tally.partsFiltered++;
|
|
711
|
+
tally.partsModified++;
|
|
712
|
+
working[index] = { ...message, text: decision.text };
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
return { messages: working, ...tally };
|
|
718
|
+
}
|
|
719
|
+
function formatTokens(tokens) {
|
|
720
|
+
if (tokens < 1e3) return String(tokens);
|
|
721
|
+
if (tokens < 1e4) return (tokens / 1e3).toFixed(1) + "K";
|
|
722
|
+
return Math.round(tokens / 1e3) + "K";
|
|
723
|
+
}
|
|
724
|
+
function classifyType(message) {
|
|
725
|
+
if (message.contentType === "tool-call" || message.contentType === "tool-result") {
|
|
726
|
+
return message.toolName || "tool";
|
|
727
|
+
}
|
|
728
|
+
return message.contentType;
|
|
729
|
+
}
|
|
730
|
+
function escapeRegex(s) {
|
|
731
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
732
|
+
}
|
|
733
|
+
var LT = "<";
|
|
734
|
+
var GT = ">";
|
|
735
|
+
var TAG_OPEN = LT + "acp ";
|
|
736
|
+
var TAG_CLOSE = LT + "/acp" + GT;
|
|
737
|
+
function acpTag(ref, tokens, type) {
|
|
738
|
+
return TAG_OPEN + 'tokens="' + formatTokens(tokens) + '" type="' + type + '"' + GT + ref + TAG_CLOSE;
|
|
739
|
+
}
|
|
740
|
+
function renderMessage(message, map, countTokens, strategy, snapshot = null) {
|
|
741
|
+
const ref = refForRaw(map, message.id);
|
|
742
|
+
if (!ref || ref === BLOCKED_REF) return message;
|
|
743
|
+
if (strategy === "none") return message;
|
|
744
|
+
if (strategy === "text-only" && message.contentType !== "text") {
|
|
745
|
+
return message;
|
|
746
|
+
}
|
|
747
|
+
const ownTagRe = new RegExp(
|
|
748
|
+
"^" + escapeRegex(TAG_OPEN) + "[^>]*" + GT + escapeRegex(ref) + escapeRegex(TAG_CLOSE) + "\\n?"
|
|
749
|
+
);
|
|
750
|
+
const cleanText = (message.text || "").replace(ownTagRe, "");
|
|
751
|
+
const tokens = snapshot ? snapshot[ref] ?? (snapshot[ref] = countTokens(cleanText)) : countTokens(cleanText);
|
|
752
|
+
const type = classifyType(message);
|
|
753
|
+
const prefix = acpTag(ref, tokens, type) + "\n";
|
|
754
|
+
if (!cleanText) return { ...message, text: prefix };
|
|
755
|
+
return { ...message, text: prefix + cleanText };
|
|
756
|
+
}
|
|
757
|
+
function renderWithSnapshot(messages, state, countTokens = (text) => Math.ceil(text.length / 4), strategy = "all") {
|
|
758
|
+
const map = state.messageRefs;
|
|
759
|
+
const snapshot = { ...state.tokenSnapshot ?? {} };
|
|
760
|
+
const rendered = messages.map(
|
|
761
|
+
(message) => renderMessage(message, map, countTokens, strategy, snapshot)
|
|
762
|
+
);
|
|
763
|
+
return { messages: rendered, tokenSnapshot: snapshot };
|
|
764
|
+
}
|
|
765
|
+
function createRenderRefsNode(strategy) {
|
|
766
|
+
return {
|
|
767
|
+
name: "render-refs",
|
|
768
|
+
run(io, ctx) {
|
|
769
|
+
const { messages, tokenSnapshot } = renderWithSnapshot(
|
|
770
|
+
io.messages,
|
|
771
|
+
io.state,
|
|
772
|
+
ctx.countTokens,
|
|
773
|
+
strategy
|
|
774
|
+
);
|
|
775
|
+
const prev = io.state.tokenSnapshot;
|
|
776
|
+
const changed = !prev || Object.keys(tokenSnapshot).length !== Object.keys(prev).length;
|
|
777
|
+
return changed ? { ...io, messages, state: { ...io.state, tokenSnapshot } } : { ...io, messages };
|
|
778
|
+
}
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
var renderRefsNode = createRenderRefsNode("all");
|
|
782
|
+
var ALWAYS_PROTECTED_TOOLS = ["compress"];
|
|
783
|
+
var NEVER_PRESERVE_RECENT_TOOLS = [
|
|
784
|
+
"decompress",
|
|
785
|
+
"search_context",
|
|
786
|
+
"read",
|
|
787
|
+
"bash"
|
|
788
|
+
];
|
|
789
|
+
function isNeverPreserveRecent(msg) {
|
|
790
|
+
if (msg.contentType !== "tool-call" && msg.contentType !== "tool-result") {
|
|
791
|
+
return false;
|
|
792
|
+
}
|
|
793
|
+
if (!msg.toolName) return false;
|
|
794
|
+
return NEVER_PRESERVE_RECENT_TOOLS.includes(msg.toolName);
|
|
795
|
+
}
|
|
796
|
+
function matchToolPattern(toolName, pattern) {
|
|
797
|
+
if (pattern.endsWith("*")) {
|
|
798
|
+
return toolName.startsWith(pattern.slice(0, -1));
|
|
799
|
+
}
|
|
800
|
+
return toolName === pattern;
|
|
801
|
+
}
|
|
802
|
+
function isMessageProtected(msg, config) {
|
|
803
|
+
if (msg.contentType !== "tool-call" && msg.contentType !== "tool-result" || !msg.toolName) {
|
|
804
|
+
return false;
|
|
805
|
+
}
|
|
806
|
+
if (ALWAYS_PROTECTED_TOOLS.includes(msg.toolName)) {
|
|
807
|
+
return true;
|
|
808
|
+
}
|
|
809
|
+
for (const pattern of config.protectedTools) {
|
|
810
|
+
if (matchToolPattern(msg.toolName, pattern)) return true;
|
|
811
|
+
}
|
|
812
|
+
if (config.isToolProtected?.(msg.toolName, msg.text)) return true;
|
|
813
|
+
return false;
|
|
814
|
+
}
|
|
815
|
+
function collectProtectedToolCallIds(messages, config) {
|
|
816
|
+
const ids = /* @__PURE__ */ new Set();
|
|
817
|
+
for (const m of messages) {
|
|
818
|
+
if (m.contentType === "tool-call" && m.toolCallId && isMessageProtected(m, config)) {
|
|
819
|
+
ids.add(m.toolCallId);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
return ids;
|
|
823
|
+
}
|
|
824
|
+
function isMessageProtectedWithPairing(msg, config, protectedCallIds) {
|
|
825
|
+
if (isMessageProtected(msg, config)) return true;
|
|
826
|
+
if (msg.contentType === "tool-result" && msg.toolCallId && protectedCallIds.has(msg.toolCallId)) {
|
|
827
|
+
return true;
|
|
828
|
+
}
|
|
829
|
+
return false;
|
|
830
|
+
}
|
|
831
|
+
function adjustBoundariesForToolPairs(startIndex, endIndex, messages, maxScan = 20) {
|
|
832
|
+
const callIdsInRange = /* @__PURE__ */ new Set();
|
|
833
|
+
for (let i = startIndex; i <= endIndex; i++) {
|
|
834
|
+
const msg = messages[i];
|
|
835
|
+
if (!msg || !msg.toolCallId) continue;
|
|
836
|
+
if (msg.toolName === "compress") continue;
|
|
837
|
+
callIdsInRange.add(msg.toolCallId);
|
|
838
|
+
}
|
|
839
|
+
if (callIdsInRange.size === 0) {
|
|
840
|
+
return { startIndex, endIndex };
|
|
841
|
+
}
|
|
842
|
+
let newEndIndex = endIndex;
|
|
843
|
+
for (let i = endIndex + 1; i < messages.length && i <= endIndex + maxScan; i++) {
|
|
844
|
+
const msg = messages[i];
|
|
845
|
+
if (!msg) break;
|
|
846
|
+
if (msg.toolCallId && callIdsInRange.has(msg.toolCallId)) {
|
|
847
|
+
newEndIndex = i;
|
|
848
|
+
} else if (newEndIndex > endIndex) {
|
|
849
|
+
break;
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
let newStartIndex = startIndex;
|
|
853
|
+
for (let i = startIndex - 1; i >= 0 && i >= startIndex - maxScan; i--) {
|
|
854
|
+
const msg = messages[i];
|
|
855
|
+
if (!msg) break;
|
|
856
|
+
if (msg.toolCallId && callIdsInRange.has(msg.toolCallId)) {
|
|
857
|
+
newStartIndex = i;
|
|
858
|
+
} else if (newStartIndex < startIndex) {
|
|
859
|
+
break;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
return { startIndex: newStartIndex, endIndex: newEndIndex };
|
|
863
|
+
}
|
|
864
|
+
function adjustBoundariesForReasoningPairs(startIndex, endIndex, messages) {
|
|
865
|
+
if (startIndex > endIndex) {
|
|
866
|
+
return { startIndex, endIndex };
|
|
867
|
+
}
|
|
868
|
+
let newStartIndex = startIndex;
|
|
869
|
+
let newEndIndex = endIndex;
|
|
870
|
+
for (let i = startIndex; i <= endIndex && i < messages.length; i++) {
|
|
871
|
+
const msg = messages[i];
|
|
872
|
+
if (!msg) continue;
|
|
873
|
+
if (msg.contentType === "reasoning") {
|
|
874
|
+
let j = i;
|
|
875
|
+
while (j + 1 < messages.length && messages[j + 1].contentType === "reasoning") {
|
|
876
|
+
j++;
|
|
877
|
+
}
|
|
878
|
+
const companion = messages[j + 1];
|
|
879
|
+
if (companion !== void 0 && companion.role === "assistant" && (companion.contentType === "text" || companion.contentType === "tool-call") && j + 1 > newEndIndex) {
|
|
880
|
+
newEndIndex = j + 1;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
if (msg.role === "assistant" && (msg.contentType === "text" || msg.contentType === "tool-call")) {
|
|
884
|
+
let k = i - 1;
|
|
885
|
+
while (k >= 0 && messages[k].contentType === "reasoning") {
|
|
886
|
+
k--;
|
|
887
|
+
}
|
|
888
|
+
const runStart = k + 1;
|
|
889
|
+
if (runStart < i && runStart >= 0 && messages[runStart].contentType === "reasoning" && runStart < newStartIndex) {
|
|
890
|
+
newStartIndex = runStart;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
return { startIndex: newStartIndex, endIndex: newEndIndex };
|
|
895
|
+
}
|
|
896
|
+
function refNum(ref) {
|
|
897
|
+
const n = parseInt(ref.slice(1), 10);
|
|
898
|
+
return Number.isNaN(n) ? -1 : n;
|
|
899
|
+
}
|
|
900
|
+
function estimateTextTokens(text) {
|
|
901
|
+
return Math.ceil(text.length / 4);
|
|
902
|
+
}
|
|
903
|
+
function isToolMessage(message) {
|
|
904
|
+
return message.contentType === "tool-call" || message.contentType === "tool-result";
|
|
905
|
+
}
|
|
906
|
+
function isSyntheticOrPruned(message, state) {
|
|
907
|
+
if (message.text?.startsWith("[Compressed conversation section]")) return true;
|
|
908
|
+
for (const block of state.blocks) {
|
|
909
|
+
if (block.active && block.effectiveMessageIds.includes(message.id)) return true;
|
|
910
|
+
}
|
|
911
|
+
return false;
|
|
912
|
+
}
|
|
913
|
+
function computeProtectedRefs(messages, state, config, countTokens = estimateTextTokens) {
|
|
914
|
+
const preserveN = config.preserveRecentMessages;
|
|
915
|
+
const preserveTokens = config.preserveRecentTokens;
|
|
916
|
+
const result = /* @__PURE__ */ new Set();
|
|
917
|
+
const visible = [];
|
|
918
|
+
for (const msg of messages) {
|
|
919
|
+
if (isSyntheticOrPruned(msg, state)) continue;
|
|
920
|
+
if (isNeverPreserveRecent(msg)) continue;
|
|
921
|
+
const ref = state.messageRefs.byRaw[msg.id];
|
|
922
|
+
if (!ref || ref === "BLOCKED") continue;
|
|
923
|
+
visible.push({ ref, tokens: countTokens(msg.text ?? "") });
|
|
924
|
+
}
|
|
925
|
+
if (preserveN > 0) {
|
|
926
|
+
for (const m of visible.slice(-preserveN)) {
|
|
927
|
+
result.add(m.ref);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
if (preserveTokens > 0) {
|
|
931
|
+
let tokenAccum = 0;
|
|
932
|
+
for (let i = visible.length - 1; i >= 0 && tokenAccum < preserveTokens; i--) {
|
|
933
|
+
result.add(visible[i].ref);
|
|
934
|
+
tokenAccum += visible[i].tokens;
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
if (preserveN > 0) {
|
|
938
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
939
|
+
const msg = messages[i];
|
|
940
|
+
if (msg.role !== "user" || isSyntheticOrPruned(msg, state)) continue;
|
|
941
|
+
const ref = state.messageRefs.byRaw[msg.id];
|
|
942
|
+
if (ref && ref !== "BLOCKED") result.add(ref);
|
|
943
|
+
break;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
return result;
|
|
947
|
+
}
|
|
948
|
+
function buildCompressibleRanges(messages, state, config, protectedZoneRefs, countTokens = estimateTextTokens) {
|
|
949
|
+
const compressibleMsgs = [];
|
|
950
|
+
const protectedMsgs = [];
|
|
951
|
+
const protectedCallIds = collectProtectedToolCallIds(messages, config);
|
|
952
|
+
for (const msg of messages) {
|
|
953
|
+
if (isSyntheticOrPruned(msg, state)) continue;
|
|
954
|
+
const ref = state.messageRefs.byRaw[msg.id];
|
|
955
|
+
if (!ref || ref === "BLOCKED") continue;
|
|
956
|
+
const rn = refNum(ref);
|
|
957
|
+
if (isMessageProtectedWithPairing(msg, config, protectedCallIds)) {
|
|
958
|
+
protectedMsgs.push({
|
|
959
|
+
ref,
|
|
960
|
+
refNum: rn,
|
|
961
|
+
tokens: countTokens(msg.text ?? ""),
|
|
962
|
+
tools: msg.toolName ? [msg.toolName] : []
|
|
963
|
+
});
|
|
964
|
+
continue;
|
|
965
|
+
}
|
|
966
|
+
if (protectedZoneRefs?.has(ref)) {
|
|
967
|
+
continue;
|
|
968
|
+
}
|
|
969
|
+
compressibleMsgs.push({
|
|
970
|
+
ref,
|
|
971
|
+
refNum: rn,
|
|
972
|
+
tokens: countTokens(msg.text ?? ""),
|
|
973
|
+
chars: (msg.text ?? "").length,
|
|
974
|
+
isTool: isToolMessage(msg),
|
|
975
|
+
isUser: msg.role === "user"
|
|
976
|
+
});
|
|
977
|
+
}
|
|
978
|
+
const compressible = [];
|
|
979
|
+
let cur = null;
|
|
980
|
+
let prevRefNum = -2;
|
|
981
|
+
for (const info of compressibleMsgs) {
|
|
982
|
+
const hasGap = info.refNum > prevRefNum + 1;
|
|
983
|
+
if (cur && (info.isUser && cur.count >= 3 || hasGap)) {
|
|
984
|
+
compressible.push(cur);
|
|
985
|
+
cur = null;
|
|
986
|
+
}
|
|
987
|
+
prevRefNum = info.refNum;
|
|
988
|
+
if (!cur) {
|
|
989
|
+
cur = {
|
|
990
|
+
startRef: info.ref,
|
|
991
|
+
endRef: info.ref,
|
|
992
|
+
count: 1,
|
|
993
|
+
tokens: info.tokens,
|
|
994
|
+
chars: info.chars,
|
|
995
|
+
toolPct: info.isTool ? 100 : 0,
|
|
996
|
+
textPct: info.isTool ? 0 : 100
|
|
997
|
+
};
|
|
998
|
+
} else {
|
|
999
|
+
cur.endRef = info.ref;
|
|
1000
|
+
cur.count++;
|
|
1001
|
+
cur.tokens += info.tokens;
|
|
1002
|
+
cur.chars = (cur.chars ?? 0) + info.chars;
|
|
1003
|
+
if (info.isTool) {
|
|
1004
|
+
cur.toolPct = Math.round((cur.toolPct * (cur.count - 1) + 100) / cur.count);
|
|
1005
|
+
} else {
|
|
1006
|
+
cur.toolPct = Math.round(cur.toolPct * (cur.count - 1) / cur.count);
|
|
1007
|
+
}
|
|
1008
|
+
cur.textPct = 100 - cur.toolPct;
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
if (cur) compressible.push(cur);
|
|
1012
|
+
const protectedRanges = [];
|
|
1013
|
+
let pcur = null;
|
|
1014
|
+
let pPrevRefNum = -2;
|
|
1015
|
+
for (const info of protectedMsgs) {
|
|
1016
|
+
const hasGap = info.refNum > pPrevRefNum + 1;
|
|
1017
|
+
if (pcur && hasGap) {
|
|
1018
|
+
protectedRanges.push(pcur);
|
|
1019
|
+
pcur = null;
|
|
1020
|
+
}
|
|
1021
|
+
pPrevRefNum = info.refNum;
|
|
1022
|
+
if (!pcur) {
|
|
1023
|
+
pcur = {
|
|
1024
|
+
startRef: info.ref,
|
|
1025
|
+
endRef: info.ref,
|
|
1026
|
+
count: 1,
|
|
1027
|
+
tokens: info.tokens,
|
|
1028
|
+
tools: [...info.tools]
|
|
1029
|
+
};
|
|
1030
|
+
} else {
|
|
1031
|
+
pcur.endRef = info.ref;
|
|
1032
|
+
pcur.count++;
|
|
1033
|
+
pcur.tokens += info.tokens;
|
|
1034
|
+
for (const t of info.tools) {
|
|
1035
|
+
if (!pcur.tools.includes(t)) pcur.tools.push(t);
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
if (pcur) protectedRanges.push(pcur);
|
|
1040
|
+
return {
|
|
1041
|
+
compressible: compressible.filter((g) => g.tokens > 0),
|
|
1042
|
+
protected: protectedRanges
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
function mergeBatch(batch) {
|
|
1046
|
+
const first = batch[0];
|
|
1047
|
+
const last = batch[batch.length - 1];
|
|
1048
|
+
const count = batch.reduce((s, r) => s + r.count, 0);
|
|
1049
|
+
const tokens = batch.reduce((s, r) => s + r.tokens, 0);
|
|
1050
|
+
const chars = batch.reduce((s, r) => s + rangeChars(r), 0);
|
|
1051
|
+
const toolPct = Math.round(
|
|
1052
|
+
batch.reduce((s, r) => s + r.toolPct * r.count, 0) / count
|
|
1053
|
+
);
|
|
1054
|
+
const merged = {
|
|
1055
|
+
startRef: first.startRef,
|
|
1056
|
+
endRef: last.endRef,
|
|
1057
|
+
count,
|
|
1058
|
+
tokens,
|
|
1059
|
+
chars,
|
|
1060
|
+
toolPct,
|
|
1061
|
+
textPct: 100 - toolPct
|
|
1062
|
+
};
|
|
1063
|
+
if (batch.some((r) => r.dangerous === true)) {
|
|
1064
|
+
merged.dangerous = true;
|
|
1065
|
+
}
|
|
1066
|
+
return merged;
|
|
1067
|
+
}
|
|
1068
|
+
function rangeChars(r) {
|
|
1069
|
+
return r.chars ?? r.tokens * 4;
|
|
1070
|
+
}
|
|
1071
|
+
function mergeRangesToThreshold(ranges, minChars) {
|
|
1072
|
+
if (minChars <= 0 || ranges.length === 0) return ranges;
|
|
1073
|
+
const result = [];
|
|
1074
|
+
let batch = [];
|
|
1075
|
+
let batchChars = 0;
|
|
1076
|
+
for (const r of ranges) {
|
|
1077
|
+
batch.push(r);
|
|
1078
|
+
batchChars += rangeChars(r);
|
|
1079
|
+
if (batchChars >= minChars) {
|
|
1080
|
+
result.push(mergeBatch(batch));
|
|
1081
|
+
batch = [];
|
|
1082
|
+
batchChars = 0;
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
if (batch.length > 0) {
|
|
1086
|
+
result.push(mergeBatch(batch));
|
|
1087
|
+
}
|
|
1088
|
+
return result;
|
|
1089
|
+
}
|
|
1090
|
+
function runPipeline(nodes, initial, ctx) {
|
|
1091
|
+
let io = initial;
|
|
1092
|
+
for (const node of nodes) {
|
|
1093
|
+
if (node.enabled && !node.enabled(io, ctx)) continue;
|
|
1094
|
+
io = node.run(io, ctx);
|
|
1095
|
+
}
|
|
1096
|
+
return io;
|
|
1097
|
+
}
|
|
1098
|
+
function rangeError(spec, message) {
|
|
1099
|
+
return `range ${spec.startRef}..${spec.endRef}: ${message}`;
|
|
1100
|
+
}
|
|
1101
|
+
function createCore(ports = {}) {
|
|
1102
|
+
const countTokens = ports.countTokens ?? defaultCountTokens;
|
|
1103
|
+
function applyCompression(input) {
|
|
1104
|
+
const state = cloneState(input.state);
|
|
1105
|
+
const runId = allocateRunId(state);
|
|
1106
|
+
let blocksCreated = 0;
|
|
1107
|
+
let tokensCompressed = 0;
|
|
1108
|
+
const errors = [];
|
|
1109
|
+
const warnings = [];
|
|
1110
|
+
const protectedMessageIds = input.protectedMessageIds ?? computeProtectedRefs(input.messages, input.state, input.config, countTokens);
|
|
1111
|
+
const preExistingCoverage = collectCoverage(state);
|
|
1112
|
+
const classifications = /* @__PURE__ */ new Map();
|
|
1113
|
+
const classificationErrors = [];
|
|
1114
|
+
const consumedRanges = [];
|
|
1115
|
+
for (const spec of input.ranges) {
|
|
1116
|
+
try {
|
|
1117
|
+
const resolved = resolveBoundaries({
|
|
1118
|
+
startRef: spec.startRef,
|
|
1119
|
+
endRef: spec.endRef,
|
|
1120
|
+
messages: input.messages,
|
|
1121
|
+
state
|
|
1122
|
+
});
|
|
1123
|
+
classifications.set(spec, { status: "ok", resolved });
|
|
1124
|
+
} catch (error) {
|
|
1125
|
+
if (error instanceof BoundaryNotFoundError) {
|
|
1126
|
+
classifications.set(
|
|
1127
|
+
spec,
|
|
1128
|
+
error.kind === "unknown" ? { status: "unknown", error } : { status: "consumed", error }
|
|
1129
|
+
);
|
|
1130
|
+
if (error.kind === "consumed") {
|
|
1131
|
+
consumedRanges.push(spec);
|
|
1132
|
+
} else {
|
|
1133
|
+
classificationErrors.push(rangeError(spec, error.message));
|
|
1134
|
+
}
|
|
1135
|
+
} else {
|
|
1136
|
+
classifications.set(spec, {
|
|
1137
|
+
status: "invalid",
|
|
1138
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
1139
|
+
});
|
|
1140
|
+
classificationErrors.push(
|
|
1141
|
+
rangeError(spec, error instanceof Error ? error.message : String(error))
|
|
1142
|
+
);
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
const rangeIndexSets = [];
|
|
1147
|
+
for (const [spec, resolution] of classifications) {
|
|
1148
|
+
if (resolution.status !== "ok") continue;
|
|
1149
|
+
const indices = resolution.resolved.messageIds.map(
|
|
1150
|
+
(id) => input.messages.findIndex((m) => m.id === id)
|
|
1151
|
+
).filter((i) => i >= 0);
|
|
1152
|
+
rangeIndexSets.push({ spec, indices });
|
|
1153
|
+
}
|
|
1154
|
+
const sortedRanges = [...rangeIndexSets].sort((a, b) => {
|
|
1155
|
+
const aMin = a.indices.length > 0 ? Math.min(...a.indices) : Infinity;
|
|
1156
|
+
const bMin = b.indices.length > 0 ? Math.min(...b.indices) : Infinity;
|
|
1157
|
+
return aMin - bMin;
|
|
1158
|
+
});
|
|
1159
|
+
const skipSpecs = /* @__PURE__ */ new Set();
|
|
1160
|
+
let acceptedMaxIndex = -1;
|
|
1161
|
+
for (const entry of sortedRanges) {
|
|
1162
|
+
const entryMax = entry.indices.length > 0 ? Math.max(...entry.indices) : -1;
|
|
1163
|
+
const entryMin = entry.indices.length > 0 ? Math.min(...entry.indices) : -1;
|
|
1164
|
+
if (entryMin >= 0 && entryMin <= acceptedMaxIndex) {
|
|
1165
|
+
skipSpecs.add(entry.spec);
|
|
1166
|
+
warnings.push(
|
|
1167
|
+
`Skipped range (${entry.spec.startRef}..${entry.spec.endRef}) \u2014 overlaps an earlier range in the batch; the earlier range takes precedence. Keep ranges disjoint.`
|
|
1168
|
+
);
|
|
1169
|
+
continue;
|
|
1170
|
+
}
|
|
1171
|
+
if (entryMax > acceptedMaxIndex) acceptedMaxIndex = entryMax;
|
|
1172
|
+
}
|
|
1173
|
+
if (input.config.compress.minCompressRange > 0 && input.ranges.length > 0) {
|
|
1174
|
+
let totalRangeChars = 0;
|
|
1175
|
+
let hasBlockBoundaryRange = false;
|
|
1176
|
+
let countedRanges = 0;
|
|
1177
|
+
for (const [spec, resolution] of classifications) {
|
|
1178
|
+
if (resolution.status !== "ok" || skipSpecs.has(spec)) continue;
|
|
1179
|
+
if (resolution.resolved.boundaryKind === "block") {
|
|
1180
|
+
hasBlockBoundaryRange = true;
|
|
1181
|
+
continue;
|
|
1182
|
+
}
|
|
1183
|
+
countedRanges++;
|
|
1184
|
+
for (const id of resolution.resolved.messageIds) {
|
|
1185
|
+
const msg = input.messages.find((m) => m.id === id);
|
|
1186
|
+
totalRangeChars += msg?.text?.length ?? 0;
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
if (!hasBlockBoundaryRange && totalRangeChars < input.config.compress.minCompressRange) {
|
|
1190
|
+
const gateMessage = consumedRanges.length > 0 ? `Requested range(s) already compressed (e.g. ${consumedRanges[0].startRef}..${consumedRanges[0].endRef}); remaining compressible content ${totalRangeChars} chars < min ${input.config.compress.minCompressRange}. Nothing to do \u2014 run acp_status to see current compressible ranges.` : `Total compressible content too small (${totalRangeChars} chars across ${countedRanges} range(s), min ${input.config.compress.minCompressRange}). Combine more messages into your range(s) to meet the threshold.`;
|
|
1191
|
+
return {
|
|
1192
|
+
state: input.state,
|
|
1193
|
+
result: {
|
|
1194
|
+
blocksCreated: 0,
|
|
1195
|
+
tokensCompressed: 0,
|
|
1196
|
+
errors: [gateMessage, ...classificationErrors],
|
|
1197
|
+
warnings: []
|
|
1198
|
+
}
|
|
1199
|
+
};
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
for (const spec of input.ranges) {
|
|
1203
|
+
if (skipSpecs.has(spec)) continue;
|
|
1204
|
+
const resolution = classifications.get(spec);
|
|
1205
|
+
if (resolution === void 0) continue;
|
|
1206
|
+
if (resolution.status === "consumed") {
|
|
1207
|
+
warnings.push(
|
|
1208
|
+
`Skipped range (${spec.startRef}..${spec.endRef}) \u2014 already compressed (messages consumed by existing block(s)); nothing to compress.`
|
|
1209
|
+
);
|
|
1210
|
+
continue;
|
|
1211
|
+
}
|
|
1212
|
+
if (resolution.status === "unknown" || resolution.status === "invalid") {
|
|
1213
|
+
errors.push(rangeError(spec, resolution.error.message));
|
|
1214
|
+
continue;
|
|
1215
|
+
}
|
|
1216
|
+
try {
|
|
1217
|
+
const outcome = applySingleRange({
|
|
1218
|
+
spec,
|
|
1219
|
+
messages: input.messages,
|
|
1220
|
+
state,
|
|
1221
|
+
runId,
|
|
1222
|
+
config: input.config,
|
|
1223
|
+
protectedMessageIds,
|
|
1224
|
+
countTokens,
|
|
1225
|
+
preExistingCoverage
|
|
1226
|
+
});
|
|
1227
|
+
blocksCreated++;
|
|
1228
|
+
tokensCompressed += outcome.tokens;
|
|
1229
|
+
warnings.push(...outcome.warnings);
|
|
1230
|
+
} catch (error) {
|
|
1231
|
+
errors.push(rangeError(spec, error instanceof Error ? error.message : String(error)));
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
state.stats.compressionCount += blocksCreated;
|
|
1235
|
+
state.stats.tokensCompressed += tokensCompressed;
|
|
1236
|
+
if (blocksCreated > 0) {
|
|
1237
|
+
state.nudge.lastPerMessageNudgeTokens = 0;
|
|
1238
|
+
state.nudge.lastNudgeShownTokens = 0;
|
|
1239
|
+
state.nudge.lastShownByTier = {};
|
|
1240
|
+
}
|
|
1241
|
+
return { state, result: { blocksCreated, tokensCompressed, errors, warnings } };
|
|
1242
|
+
}
|
|
1243
|
+
function processTurn(input) {
|
|
1244
|
+
const configErrors = validateConfig(input.config);
|
|
1245
|
+
if (configErrors.length > 0) {
|
|
1246
|
+
console.warn(`[acp-kernel] Config validation warnings: ${configErrors.join("; ")}. Thresholds may not fire correctly.`);
|
|
1247
|
+
}
|
|
1248
|
+
const ctx = {
|
|
1249
|
+
config: input.config,
|
|
1250
|
+
tokenCount: input.tokenCount,
|
|
1251
|
+
countTokens
|
|
1252
|
+
};
|
|
1253
|
+
const initial = {
|
|
1254
|
+
messages: input.messages,
|
|
1255
|
+
state: input.state,
|
|
1256
|
+
effects: {}
|
|
1257
|
+
};
|
|
1258
|
+
const strategy = input.renderTags ?? "all";
|
|
1259
|
+
const nodes = buildNodes(strategy);
|
|
1260
|
+
const result = runPipeline(nodes, initial, ctx);
|
|
1261
|
+
return {
|
|
1262
|
+
messages: result.messages,
|
|
1263
|
+
state: result.state,
|
|
1264
|
+
nudge: result.effects.nudge
|
|
1265
|
+
};
|
|
1266
|
+
}
|
|
1267
|
+
function decompress(blockId, state) {
|
|
1268
|
+
return blockById(state, blockId);
|
|
1269
|
+
}
|
|
1270
|
+
function search(query, state) {
|
|
1271
|
+
const terms = query.toLowerCase().split(/\s+/).filter((term) => term.length > 0);
|
|
1272
|
+
if (terms.length === 0) return [];
|
|
1273
|
+
const scored = activeBlocks(state).map((block) => ({ block, score: scoreRelevance(block, terms) })).filter((entry) => entry.score > 0.1).sort((left, right) => right.score - left.score);
|
|
1274
|
+
return scored.map((entry) => entry.block);
|
|
1275
|
+
}
|
|
1276
|
+
function status(state, tokenCount, config) {
|
|
1277
|
+
const active = activeBlocks(state);
|
|
1278
|
+
const usage = config.modelContextLimit > 0 ? tokenCount / config.modelContextLimit : 0;
|
|
1279
|
+
return {
|
|
1280
|
+
contextUsage: usage,
|
|
1281
|
+
tokenCount,
|
|
1282
|
+
modelContextLimit: config.modelContextLimit,
|
|
1283
|
+
activeBlocks: active.length,
|
|
1284
|
+
totalBlocks: state.blocks.length,
|
|
1285
|
+
tokensCompressed: state.stats.tokensCompressed,
|
|
1286
|
+
breakdown: { active: active.length, total: state.blocks.length }
|
|
1287
|
+
};
|
|
1288
|
+
}
|
|
1289
|
+
function defaultNodes() {
|
|
1290
|
+
return buildNodes("all");
|
|
1291
|
+
}
|
|
1292
|
+
function buildNodes(strategy) {
|
|
1293
|
+
const base = [
|
|
1294
|
+
assignRefsNode,
|
|
1295
|
+
syncBlocksNode,
|
|
1296
|
+
pruneNode,
|
|
1297
|
+
filterNode,
|
|
1298
|
+
hideCompressCallsNode,
|
|
1299
|
+
recommendNode,
|
|
1300
|
+
nudgeNode,
|
|
1301
|
+
emergencyTruncateNode
|
|
1302
|
+
];
|
|
1303
|
+
if (strategy === "none") return base;
|
|
1304
|
+
return [...base, createRenderRefsNode(strategy)];
|
|
1305
|
+
}
|
|
1306
|
+
return { processTurn, applyCompression, defaultNodes, decompress, search, status };
|
|
1307
|
+
}
|
|
1308
|
+
var assignRefsNode = {
|
|
1309
|
+
name: "assign-refs",
|
|
1310
|
+
run(io, ctx) {
|
|
1311
|
+
const hasProtection = ctx.config.protectedTools.length > 0 || !!ctx.config.isToolProtected;
|
|
1312
|
+
const protectedFn = hasProtection ? (m) => isMessageProtected(m, ctx.config) : void 0;
|
|
1313
|
+
const refResult = assignRefs(io.messages, {
|
|
1314
|
+
existing: io.state.messageRefs,
|
|
1315
|
+
nextIndex: highestUsedIndex(io.state.messageRefs) + 1,
|
|
1316
|
+
isProtected: protectedFn
|
|
1317
|
+
});
|
|
1318
|
+
return { ...io, state: { ...io.state, messageRefs: refResult.map } };
|
|
1319
|
+
}
|
|
1320
|
+
};
|
|
1321
|
+
var syncBlocksNode = {
|
|
1322
|
+
name: "sync-blocks",
|
|
1323
|
+
run(io, ctx) {
|
|
1324
|
+
const synced = syncBlocks(io.messages, io.state);
|
|
1325
|
+
advanceSurvival(synced.state, ctx.config.promotionThreshold);
|
|
1326
|
+
return { ...io, state: synced.state };
|
|
1327
|
+
}
|
|
1328
|
+
};
|
|
1329
|
+
var pruneNode = {
|
|
1330
|
+
name: "prune",
|
|
1331
|
+
run(io) {
|
|
1332
|
+
return { ...io, messages: prune(io.messages, io.state) };
|
|
1333
|
+
}
|
|
1334
|
+
};
|
|
1335
|
+
var filterNode = {
|
|
1336
|
+
name: "filter",
|
|
1337
|
+
enabled: (_io, ctx) => !!ctx.config.messageFilters?.enabled && listMessageFilters().length > 0,
|
|
1338
|
+
run(io, ctx) {
|
|
1339
|
+
const applied = applyMessageFilters(io.messages, ctx.config.messageFilters);
|
|
1340
|
+
return { ...io, messages: applied.messages };
|
|
1341
|
+
}
|
|
1342
|
+
};
|
|
1343
|
+
var hideCompressCallsNode = {
|
|
1344
|
+
name: "hide-compress-calls",
|
|
1345
|
+
run(io) {
|
|
1346
|
+
const hidden = hideConsumedCompressCalls(io.state, io.messages);
|
|
1347
|
+
return { ...io, messages: hidden.messages };
|
|
1348
|
+
}
|
|
1349
|
+
};
|
|
1350
|
+
var recommendNode = {
|
|
1351
|
+
name: "recommend",
|
|
1352
|
+
run(io, ctx) {
|
|
1353
|
+
const protectedRefs = computeProtectedRefs(
|
|
1354
|
+
io.messages,
|
|
1355
|
+
io.state,
|
|
1356
|
+
ctx.config,
|
|
1357
|
+
ctx.countTokens
|
|
1358
|
+
);
|
|
1359
|
+
const contextRanges = buildCompressibleRanges(
|
|
1360
|
+
io.messages,
|
|
1361
|
+
io.state,
|
|
1362
|
+
ctx.config,
|
|
1363
|
+
protectedRefs,
|
|
1364
|
+
ctx.countTokens
|
|
1365
|
+
);
|
|
1366
|
+
const nothingToCompress = contextRanges.compressible.length === 0;
|
|
1367
|
+
const recommendation = {
|
|
1368
|
+
contextRanges,
|
|
1369
|
+
recommendedRanges: mergeRangesToThreshold(
|
|
1370
|
+
contextRanges.compressible,
|
|
1371
|
+
ctx.config.compress.minCompressRange
|
|
1372
|
+
),
|
|
1373
|
+
nothingToCompress
|
|
1374
|
+
};
|
|
1375
|
+
return { ...io, effects: { ...io.effects, recommendation } };
|
|
1376
|
+
}
|
|
1377
|
+
};
|
|
1378
|
+
var nudgeNode = {
|
|
1379
|
+
name: "nudge-inject",
|
|
1380
|
+
run(io, ctx) {
|
|
1381
|
+
const nudge = decideNudge({
|
|
1382
|
+
tokenCount: ctx.tokenCount,
|
|
1383
|
+
config: ctx.config,
|
|
1384
|
+
state: io.state,
|
|
1385
|
+
messages: io.messages,
|
|
1386
|
+
recommendation: io.effects.recommendation,
|
|
1387
|
+
countTokens: ctx.countTokens
|
|
1388
|
+
});
|
|
1389
|
+
const baseline = io.state.nudge.lastPerMessageNudgeTokens;
|
|
1390
|
+
const nudgeGrowthTokens = resolveAdaptiveGrowth(
|
|
1391
|
+
ctx.config.modelContextLimit,
|
|
1392
|
+
ctx.config.nudge
|
|
1393
|
+
);
|
|
1394
|
+
let stamped = { ...io.state.nudge };
|
|
1395
|
+
if (baseline > 0 && ctx.tokenCount < baseline - nudgeGrowthTokens) {
|
|
1396
|
+
stamped.lastPerMessageNudgeTokens = ctx.tokenCount;
|
|
1397
|
+
stamped.lastNudgeShownTokens = 0;
|
|
1398
|
+
stamped.lastShownByTier = {};
|
|
1399
|
+
}
|
|
1400
|
+
if (stamped.lastPerMessageNudgeTokens === 0) {
|
|
1401
|
+
stamped.lastPerMessageNudgeTokens = ctx.tokenCount;
|
|
1402
|
+
}
|
|
1403
|
+
if (nudge.shouldInject) {
|
|
1404
|
+
stamped.lastNudgeShownTokens = ctx.tokenCount;
|
|
1405
|
+
if (nudge.tier !== null) {
|
|
1406
|
+
stamped.lastShownByTier = { ...stamped.lastShownByTier, [nudge.tier]: ctx.tokenCount };
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
return {
|
|
1410
|
+
...io,
|
|
1411
|
+
state: { ...io.state, nudge: stamped },
|
|
1412
|
+
effects: { ...io.effects, nudge }
|
|
1413
|
+
};
|
|
1414
|
+
}
|
|
1415
|
+
};
|
|
1416
|
+
var emergencyTruncateNode = {
|
|
1417
|
+
name: "emergency-truncate",
|
|
1418
|
+
run(io, ctx) {
|
|
1419
|
+
const usage = ctx.config.modelContextLimit > 0 ? ctx.tokenCount / ctx.config.modelContextLimit : 0;
|
|
1420
|
+
if (usage < ctx.config.truncate.threshold) return io;
|
|
1421
|
+
const trunc = truncateLargeToolOutputs(
|
|
1422
|
+
io.messages,
|
|
1423
|
+
ctx.tokenCount,
|
|
1424
|
+
ctx.config,
|
|
1425
|
+
ctx.countTokens,
|
|
1426
|
+
{ protectRecentMessages: ctx.config.preserveRecentMessages }
|
|
1427
|
+
);
|
|
1428
|
+
return {
|
|
1429
|
+
...io,
|
|
1430
|
+
messages: trunc.messages,
|
|
1431
|
+
effects: { ...io.effects, truncatedCount: trunc.truncatedCount }
|
|
1432
|
+
};
|
|
1433
|
+
}
|
|
1434
|
+
};
|
|
1435
|
+
function applySingleRange(input) {
|
|
1436
|
+
const warnings = [];
|
|
1437
|
+
const resolved = resolveBoundaries({
|
|
1438
|
+
startRef: input.spec.startRef,
|
|
1439
|
+
endRef: input.spec.endRef,
|
|
1440
|
+
messages: input.messages,
|
|
1441
|
+
state: input.state
|
|
1442
|
+
});
|
|
1443
|
+
const rangeMessageIds = applyPairBoundaryAdjustments(
|
|
1444
|
+
resolved,
|
|
1445
|
+
input.messages
|
|
1446
|
+
);
|
|
1447
|
+
if (rangeMessageIds.length > resolved.messageIds.length) {
|
|
1448
|
+
const indexByRawId = /* @__PURE__ */ new Map();
|
|
1449
|
+
input.messages.forEach((m, i) => indexByRawId.set(m.id, i));
|
|
1450
|
+
const adjustedStart = indexByRawId.get(rangeMessageIds[0]) ?? resolved.startIndex;
|
|
1451
|
+
const adjustedEnd = indexByRawId.get(rangeMessageIds[rangeMessageIds.length - 1]) ?? resolved.endIndex;
|
|
1452
|
+
const nestedSeen = new Set(resolved.nestedBlockIds);
|
|
1453
|
+
for (const block2 of activeBlocks(input.state)) {
|
|
1454
|
+
if (nestedSeen.has(block2.blockId)) continue;
|
|
1455
|
+
const anchor = earliestIndexOfIds(block2.effectiveMessageIds, indexByRawId);
|
|
1456
|
+
if (anchor !== null && anchor >= adjustedStart && anchor <= adjustedEnd) {
|
|
1457
|
+
nestedSeen.add(block2.blockId);
|
|
1458
|
+
resolved.nestedBlockIds.push(block2.blockId);
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
const isBlockBoundary = resolved.boundaryKind === "block";
|
|
1463
|
+
const targetTier = resolveTargetTier(
|
|
1464
|
+
input.state,
|
|
1465
|
+
resolved.nestedBlockIds,
|
|
1466
|
+
isBlockBoundary
|
|
1467
|
+
);
|
|
1468
|
+
const outputTier = isBlockBoundary ? Math.min(3, targetTier + 1) : 1;
|
|
1469
|
+
const consumedBlockIds = resolved.nestedBlockIds.filter((id) => {
|
|
1470
|
+
const block2 = blockById(input.state, id);
|
|
1471
|
+
return block2?.active && block2.tier === targetTier;
|
|
1472
|
+
});
|
|
1473
|
+
const effectiveMessageIds = new Set(rangeMessageIds);
|
|
1474
|
+
for (const consumedId of consumedBlockIds) {
|
|
1475
|
+
const consumed = blockById(input.state, consumedId);
|
|
1476
|
+
if (consumed) {
|
|
1477
|
+
for (const id of consumed.effectiveMessageIds)
|
|
1478
|
+
effectiveMessageIds.add(id);
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
const directMessageIds = [...effectiveMessageIds].filter(
|
|
1482
|
+
(id) => !input.preExistingCoverage.has(id)
|
|
1483
|
+
);
|
|
1484
|
+
let filteredIds = filterProtectedToolMessages(
|
|
1485
|
+
directMessageIds,
|
|
1486
|
+
input.messages,
|
|
1487
|
+
input.config
|
|
1488
|
+
);
|
|
1489
|
+
if (filteredIds.length < directMessageIds.length) {
|
|
1490
|
+
const kept = new Set(filteredIds);
|
|
1491
|
+
for (const id of directMessageIds) {
|
|
1492
|
+
if (!kept.has(id)) effectiveMessageIds.delete(id);
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
const protectedRefs = input.protectedMessageIds;
|
|
1496
|
+
const hitProtectedRaw = protectedRefs ? filteredIds.filter((id) => {
|
|
1497
|
+
const ref = input.state.messageRefs.byRaw[id];
|
|
1498
|
+
return ref !== void 0 && protectedRefs.has(ref);
|
|
1499
|
+
}) : [];
|
|
1500
|
+
if (hitProtectedRaw.length > 0) {
|
|
1501
|
+
const protectedSet = new Set(hitProtectedRaw);
|
|
1502
|
+
filteredIds = filteredIds.filter((id) => !protectedSet.has(id));
|
|
1503
|
+
for (const id of hitProtectedRaw) effectiveMessageIds.delete(id);
|
|
1504
|
+
const hitRefs = hitProtectedRaw.map((id) => input.state.messageRefs.byRaw[id]).filter((v) => typeof v === "string");
|
|
1505
|
+
if (filteredIds.length === 0 && consumedBlockIds.length === 0) {
|
|
1506
|
+
const recentN = input.config.preserveRecentMessages;
|
|
1507
|
+
throw new Error(
|
|
1508
|
+
`Range is entirely within the protected zone (the last ${recentN} messages and/or the most recent user message): ${hitRefs.join(
|
|
1509
|
+
", "
|
|
1510
|
+
)}. Adjust startId/endId to older messages.`
|
|
1511
|
+
);
|
|
1512
|
+
}
|
|
1513
|
+
warnings.push(
|
|
1514
|
+
`Excluded ${hitProtectedRaw.length} protected message(s) ${hitRefs.join(
|
|
1515
|
+
", "
|
|
1516
|
+
)} from compression range (recent/last-user zone).`
|
|
1517
|
+
);
|
|
1518
|
+
}
|
|
1519
|
+
validateCompressionRange(input, filteredIds, consumedBlockIds.length);
|
|
1520
|
+
let compressedTokens = 0;
|
|
1521
|
+
for (const id of filteredIds) {
|
|
1522
|
+
const message = input.messages.find((entry) => entry.id === id);
|
|
1523
|
+
compressedTokens += input.countTokens(message?.text ?? "");
|
|
1524
|
+
}
|
|
1525
|
+
for (const consumedId of consumedBlockIds) {
|
|
1526
|
+
const consumed = blockById(input.state, consumedId);
|
|
1527
|
+
if (consumed) {
|
|
1528
|
+
compressedTokens += input.countTokens(consumed.summary);
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
const blockId = allocateBlockId(input.state);
|
|
1532
|
+
const block = {
|
|
1533
|
+
blockId,
|
|
1534
|
+
runId: input.runId,
|
|
1535
|
+
tier: outputTier,
|
|
1536
|
+
topic: input.spec.topic,
|
|
1537
|
+
summary: input.spec.summary,
|
|
1538
|
+
directMessageIds: filteredIds,
|
|
1539
|
+
effectiveMessageIds: [...effectiveMessageIds],
|
|
1540
|
+
directBlockIds: [...consumedBlockIds],
|
|
1541
|
+
compressedTokens,
|
|
1542
|
+
createdAt: Date.now(),
|
|
1543
|
+
survivedCount: 0,
|
|
1544
|
+
generation: "young",
|
|
1545
|
+
active: true,
|
|
1546
|
+
compressCallId: input.spec.compressCallId,
|
|
1547
|
+
startRef: input.spec.startRef,
|
|
1548
|
+
endRef: input.spec.endRef
|
|
1549
|
+
};
|
|
1550
|
+
input.state.blocks.push(block);
|
|
1551
|
+
for (const consumedId of consumedBlockIds) {
|
|
1552
|
+
const consumed = blockById(input.state, consumedId);
|
|
1553
|
+
if (consumed) consumed.active = false;
|
|
1554
|
+
}
|
|
1555
|
+
return { tokens: compressedTokens, warnings };
|
|
1556
|
+
}
|
|
1557
|
+
function applyPairBoundaryAdjustments(resolved, messages) {
|
|
1558
|
+
if (resolved.boundaryKind === "block") {
|
|
1559
|
+
return resolved.messageIds;
|
|
1560
|
+
}
|
|
1561
|
+
let startIndex = resolved.startIndex;
|
|
1562
|
+
let endIndex = resolved.endIndex;
|
|
1563
|
+
for (let pass = 0; pass < 2; pass++) {
|
|
1564
|
+
const reasoningAdjusted = adjustBoundariesForReasoningPairs(
|
|
1565
|
+
startIndex,
|
|
1566
|
+
endIndex,
|
|
1567
|
+
messages
|
|
1568
|
+
);
|
|
1569
|
+
const toolAdjusted = adjustBoundariesForToolPairs(
|
|
1570
|
+
reasoningAdjusted.startIndex,
|
|
1571
|
+
reasoningAdjusted.endIndex,
|
|
1572
|
+
messages
|
|
1573
|
+
);
|
|
1574
|
+
const changed = toolAdjusted.startIndex !== startIndex || toolAdjusted.endIndex !== endIndex;
|
|
1575
|
+
startIndex = toolAdjusted.startIndex;
|
|
1576
|
+
endIndex = toolAdjusted.endIndex;
|
|
1577
|
+
if (!changed) break;
|
|
1578
|
+
}
|
|
1579
|
+
if (startIndex === resolved.startIndex && endIndex === resolved.endIndex) {
|
|
1580
|
+
return resolved.messageIds;
|
|
1581
|
+
}
|
|
1582
|
+
const ids = [];
|
|
1583
|
+
for (let i = startIndex; i <= endIndex; i++) {
|
|
1584
|
+
const msg = messages[i];
|
|
1585
|
+
if (msg) ids.push(msg.id);
|
|
1586
|
+
}
|
|
1587
|
+
return ids;
|
|
1588
|
+
}
|
|
1589
|
+
function validateCompressionRange(input, directMessageIds, consumedBlockCount) {
|
|
1590
|
+
const cfg = input.config.compress;
|
|
1591
|
+
const summary = input.spec.summary?.trim() ?? "";
|
|
1592
|
+
if (summary.length === 0) {
|
|
1593
|
+
throw new Error(
|
|
1594
|
+
"Summary is empty \u2014 provide a meaningful summary of the compressed range."
|
|
1595
|
+
);
|
|
1596
|
+
}
|
|
1597
|
+
if (cfg.minSummaryLength > 0 && summary.length < cfg.minSummaryLength) {
|
|
1598
|
+
throw new Error(
|
|
1599
|
+
`Summary too short (${summary.length} chars, min ${cfg.minSummaryLength}). The summary must capture the compressed range's key information.`
|
|
1600
|
+
);
|
|
1601
|
+
}
|
|
1602
|
+
const effectiveMax = input.spec.summaryMaxChars ?? cfg.maxSummaryLength;
|
|
1603
|
+
if (effectiveMax > 0 && summary.length > effectiveMax) {
|
|
1604
|
+
throw new Error(
|
|
1605
|
+
`Summary too long (${summary.length} chars, max ${effectiveMax}). Strip noise \u2014 keep critical paths, decisions, errors, and code references. Or pass summaryMaxChars to increase the limit \u2014 don't lose critical info just to fit.`
|
|
1606
|
+
);
|
|
1607
|
+
}
|
|
1608
|
+
if (directMessageIds.length === 0 && consumedBlockCount === 0) {
|
|
1609
|
+
throw new Error(
|
|
1610
|
+
"Range contains no compressible messages \u2014 all are already covered by active blocks or protected."
|
|
1611
|
+
);
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
function filterProtectedToolMessages(directMessageIds, messages, config) {
|
|
1615
|
+
const protectedCallIds = /* @__PURE__ */ new Set();
|
|
1616
|
+
const removedIds = /* @__PURE__ */ new Set();
|
|
1617
|
+
for (const msg of messages) {
|
|
1618
|
+
if (isMessageProtected(msg, config) && msg.toolCallId) {
|
|
1619
|
+
protectedCallIds.add(msg.toolCallId);
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
for (const id of directMessageIds) {
|
|
1623
|
+
const msg = messages.find((m) => m.id === id);
|
|
1624
|
+
if (!msg) continue;
|
|
1625
|
+
if (isMessageProtected(msg, config)) {
|
|
1626
|
+
removedIds.add(id);
|
|
1627
|
+
if (msg.toolCallId) protectedCallIds.add(msg.toolCallId);
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
for (const id of directMessageIds) {
|
|
1631
|
+
if (removedIds.has(id)) continue;
|
|
1632
|
+
const msg = messages.find((m) => m.id === id);
|
|
1633
|
+
if (!msg) continue;
|
|
1634
|
+
if (msg.contentType === "tool-result" && msg.toolCallId && protectedCallIds.has(msg.toolCallId)) {
|
|
1635
|
+
removedIds.add(id);
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
return directMessageIds.filter((id) => !removedIds.has(id));
|
|
1639
|
+
}
|
|
1640
|
+
function resolveTargetTier(state, nestedBlockIds, isBlockBoundary) {
|
|
1641
|
+
if (!isBlockBoundary) return 1;
|
|
1642
|
+
if (nestedBlockIds.length === 0) return 1;
|
|
1643
|
+
let minTier = 3;
|
|
1644
|
+
for (const id of nestedBlockIds) {
|
|
1645
|
+
const block = blockById(state, id);
|
|
1646
|
+
if (block && block.tier < minTier) minTier = block.tier;
|
|
1647
|
+
}
|
|
1648
|
+
return minTier;
|
|
1649
|
+
}
|
|
1650
|
+
function collectCoverage(state) {
|
|
1651
|
+
const coverage = /* @__PURE__ */ new Set();
|
|
1652
|
+
for (const block of activeBlocks(state)) {
|
|
1653
|
+
for (const id of block.effectiveMessageIds) coverage.add(id);
|
|
1654
|
+
}
|
|
1655
|
+
return coverage;
|
|
1656
|
+
}
|
|
1657
|
+
function resolveAdaptiveGrowth(modelContextLimit, nudge) {
|
|
1658
|
+
if (!modelContextLimit || modelContextLimit <= 0) return nudge.growthFloor;
|
|
1659
|
+
return Math.min(
|
|
1660
|
+
nudge.growthCap,
|
|
1661
|
+
Math.max(
|
|
1662
|
+
nudge.growthFloor,
|
|
1663
|
+
Math.round(modelContextLimit * nudge.growthRatio)
|
|
1664
|
+
)
|
|
1665
|
+
);
|
|
1666
|
+
}
|
|
1667
|
+
function pendingByTier(state, recommendation, countTokens, minCompressRange) {
|
|
1668
|
+
const out = {};
|
|
1669
|
+
const merged = recommendation?.recommendedRanges ?? [];
|
|
1670
|
+
const effective = minCompressRange > 0 ? merged.filter((r) => (r.chars ?? r.tokens * 4) >= minCompressRange) : merged;
|
|
1671
|
+
out[1] = { pending: effective.reduce((s, r) => s + r.tokens, 0), targetBlocks: [] };
|
|
1672
|
+
const active = activeBlocks(state);
|
|
1673
|
+
const t1 = active.filter((b) => b.tier === 1);
|
|
1674
|
+
const t2 = active.filter((b) => b.tier === 2);
|
|
1675
|
+
out[2] = { pending: t1.reduce((s, b) => s + countTokens(b.summary), 0), targetBlocks: t1 };
|
|
1676
|
+
out[3] = { pending: t2.reduce((s, b) => s + countTokens(b.summary), 0), targetBlocks: t2 };
|
|
1677
|
+
return out;
|
|
1678
|
+
}
|
|
1679
|
+
function decideNudge(input) {
|
|
1680
|
+
const { config, state, tokenCount, recommendation, countTokens } = input;
|
|
1681
|
+
const limit = config.modelContextLimit;
|
|
1682
|
+
const usage = limit > 0 ? tokenCount / limit : 0;
|
|
1683
|
+
const nudgeGrowthTokens = resolveAdaptiveGrowth(limit, config.nudge);
|
|
1684
|
+
const overLimit = usage >= config.nudge.maxContextLimitPct;
|
|
1685
|
+
const emergencyOverride = usage >= config.nudge.emergencyThresholdPct;
|
|
1686
|
+
const pressure = overLimit || emergencyOverride;
|
|
1687
|
+
const baseline = state.nudge.lastPerMessageNudgeTokens;
|
|
1688
|
+
const hadPendingNudge = state.nudge.lastNudgeShownTokens > 0;
|
|
1689
|
+
const hasPendingNudge = hadPendingNudge;
|
|
1690
|
+
const effectiveThreshold = hasPendingNudge ? Math.floor(nudgeGrowthTokens / 2) : nudgeGrowthTokens;
|
|
1691
|
+
const growthReference = state.nudge.lastNudgeShownTokens > 0 ? state.nudge.lastNudgeShownTokens : baseline > 0 ? baseline : tokenCount;
|
|
1692
|
+
const growthFloor = Math.max(
|
|
1693
|
+
config.nudge.minGrowthFloor,
|
|
1694
|
+
config.nudge.minGrowthRatio * nudgeGrowthTokens
|
|
1695
|
+
);
|
|
1696
|
+
const growthSinceReference = tokenCount - growthReference;
|
|
1697
|
+
const rec = recommendation;
|
|
1698
|
+
const tiers = pendingByTier(
|
|
1699
|
+
state,
|
|
1700
|
+
rec,
|
|
1701
|
+
countTokens,
|
|
1702
|
+
config.compress.minCompressRange
|
|
1703
|
+
);
|
|
1704
|
+
const tier2Threshold = Math.round(
|
|
1705
|
+
nudgeGrowthTokens * (config.nudge.tier2GrowthMultiplier ?? 1.5)
|
|
1706
|
+
);
|
|
1707
|
+
let injectedTier = null;
|
|
1708
|
+
let injectedReason = "";
|
|
1709
|
+
const growthReady = growthSinceReference >= growthFloor;
|
|
1710
|
+
const t1Eff = tiers[1]?.pending ?? 0;
|
|
1711
|
+
const t2Pen = tiers[2]?.pending ?? 0;
|
|
1712
|
+
const t3Pen = tiers[3]?.pending ?? 0;
|
|
1713
|
+
if (pressure) {
|
|
1714
|
+
const candidates = [1];
|
|
1715
|
+
if (config.tiers.enabled) {
|
|
1716
|
+
candidates.push(2, 3);
|
|
1717
|
+
}
|
|
1718
|
+
let best = null;
|
|
1719
|
+
let bestPending = 0;
|
|
1720
|
+
for (const t of candidates) {
|
|
1721
|
+
const p = tiers[t]?.pending ?? 0;
|
|
1722
|
+
if (p > bestPending) {
|
|
1723
|
+
bestPending = p;
|
|
1724
|
+
best = t;
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
if (best !== null && bestPending > 0) {
|
|
1728
|
+
injectedTier = best;
|
|
1729
|
+
const label = emergencyOverride ? "EMERGENCY" : "OVER-LIMIT";
|
|
1730
|
+
injectedReason = best === 1 ? `${label} T1: max effective pending ${bestPending}, usage ${Math.round(usage * 100)}%` : `${label} T${best} distill: max pending ${bestPending} (T1 effective ${t1Eff}, T2 ${t2Pen}, T3 ${t3Pen}), usage ${Math.round(usage * 100)}%`;
|
|
1731
|
+
}
|
|
1732
|
+
} else if (growthReady) {
|
|
1733
|
+
if (t1Eff >= nudgeGrowthTokens) {
|
|
1734
|
+
injectedTier = 1;
|
|
1735
|
+
injectedReason = `T1 effective ${t1Eff} >= ${nudgeGrowthTokens}, growth ${growthSinceReference}, usage ${Math.round(usage * 100)}%`;
|
|
1736
|
+
} else if (config.tiers.enabled && t2Pen >= tier2Threshold && t2Pen > t1Eff) {
|
|
1737
|
+
const lastShown = state.nudge.lastShownByTier[2] ?? 0;
|
|
1738
|
+
const cadenceMet = lastShown === 0 || tokenCount - lastShown >= growthFloor;
|
|
1739
|
+
if (cadenceMet) {
|
|
1740
|
+
injectedTier = 2;
|
|
1741
|
+
injectedReason = `T2 distill ready: ${tiers[2].targetBlocks.length} tier-1 blocks (${t2Pen} tokens) >= ${tier2Threshold} (1.5x) and > T1 effective ${t1Eff}, usage ${Math.round(usage * 100)}%`;
|
|
1742
|
+
}
|
|
1743
|
+
} else if (config.tiers.enabled && t3Pen >= tier2Threshold && t3Pen > t2Pen && t3Pen > t1Eff) {
|
|
1744
|
+
const lastShown = state.nudge.lastShownByTier[3] ?? 0;
|
|
1745
|
+
const cadenceMet = lastShown === 0 || tokenCount - lastShown >= growthFloor;
|
|
1746
|
+
if (cadenceMet) {
|
|
1747
|
+
injectedTier = 3;
|
|
1748
|
+
injectedReason = `T3 condense ready: ${tiers[3].targetBlocks.length} tier-2 blocks (${t3Pen} tokens) >= ${tier2Threshold} (1.5x) and > T2 ${t2Pen} and > T1 effective ${t1Eff}, usage ${Math.round(usage * 100)}%`;
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
const shouldInject = injectedTier !== null;
|
|
1753
|
+
let reason;
|
|
1754
|
+
if (injectedTier !== null) {
|
|
1755
|
+
reason = injectedReason;
|
|
1756
|
+
} else if (pressure) {
|
|
1757
|
+
const label = emergencyOverride ? "EMERGENCY" : "OVER-LIMIT";
|
|
1758
|
+
reason = `${label}: usage ${Math.round(usage * 100)}% but no tier has effective compressible content (T1 effective ${t1Eff}, T2 ${t2Pen}, T3 ${t3Pen}) \u2014 nudge suppressed to avoid offering ranges below minCompressRange`;
|
|
1759
|
+
} else {
|
|
1760
|
+
const tiersList = [1, 2, 3];
|
|
1761
|
+
const eligible = tiersList.filter((t) => config.tiers.enabled || t === 1);
|
|
1762
|
+
const ready = eligible.filter((t) => (tiers[t]?.pending ?? 0) >= nudgeGrowthTokens).map((t) => `T${t} ${tiers[t].pending}`);
|
|
1763
|
+
const readyHint = ready.length > 0 ? `, ready: ${ready.join(", ")}` : "";
|
|
1764
|
+
const blocked = eligible.filter((t) => (tiers[t]?.pending ?? 0) >= nudgeGrowthTokens && (state.nudge.lastShownByTier[t] ?? 0) > 0 && tokenCount - (state.nudge.lastShownByTier[t] ?? 0) < growthFloor).map((t) => `T${t} (cadence)`);
|
|
1765
|
+
const blockedHint = blocked.length > 0 ? `, blocked: ${blocked.join(", ")}` : "";
|
|
1766
|
+
const maxPending = Math.max(0, ...Object.values(tiers).map((t) => t.pending));
|
|
1767
|
+
const pendingShort = maxPending < nudgeGrowthTokens;
|
|
1768
|
+
const growthShort = growthSinceReference < growthFloor;
|
|
1769
|
+
const parts = [];
|
|
1770
|
+
if (pendingShort) parts.push(`max compressible ${maxPending} < threshold ${nudgeGrowthTokens}`);
|
|
1771
|
+
if (growthShort) parts.push(`growth ${growthSinceReference} < floor ${growthFloor}`);
|
|
1772
|
+
if (parts.length === 0) parts.push(`max compressible ${maxPending}, growth ${growthSinceReference}`);
|
|
1773
|
+
reason = `${parts.join("; ")}${readyHint}${blockedHint}`;
|
|
1774
|
+
}
|
|
1775
|
+
const ctxBreakdown = computeContextBreakdown(input.messages, tokenCount, growthSinceReference, countTokens);
|
|
1776
|
+
return {
|
|
1777
|
+
shouldInject,
|
|
1778
|
+
reason,
|
|
1779
|
+
compressibleRanges: rec?.recommendedRanges ?? [],
|
|
1780
|
+
protectedRanges: rec?.contextRanges.protected ?? [],
|
|
1781
|
+
tierTargetBlocks: injectedTier ? tiers[injectedTier].targetBlocks : [],
|
|
1782
|
+
contextUsage: usage,
|
|
1783
|
+
tier: injectedTier,
|
|
1784
|
+
breakdown: {
|
|
1785
|
+
usage,
|
|
1786
|
+
growth: growthSinceReference,
|
|
1787
|
+
growthReference,
|
|
1788
|
+
effectiveThreshold,
|
|
1789
|
+
nudgeGrowthTokens,
|
|
1790
|
+
growthFloor,
|
|
1791
|
+
hasPendingNudge: hasPendingNudge ? 1 : 0,
|
|
1792
|
+
overLimit: overLimit ? 1 : 0,
|
|
1793
|
+
emergencyOverride: emergencyOverride ? 1 : 0,
|
|
1794
|
+
pendingT1: tiers[1].pending,
|
|
1795
|
+
pendingT2: tiers[2].pending,
|
|
1796
|
+
pendingT3: tiers[3].pending
|
|
1797
|
+
},
|
|
1798
|
+
contextBreakdown: ctxBreakdown
|
|
1799
|
+
};
|
|
1800
|
+
}
|
|
1801
|
+
function computeContextBreakdown(messages, total, growth, countTokens) {
|
|
1802
|
+
const count = countTokens ?? ((t) => Math.ceil(t.length / 4));
|
|
1803
|
+
let system = 0, tool = 0, summaries = 0, code = 0, text = 0;
|
|
1804
|
+
for (const msg of messages) {
|
|
1805
|
+
const tokens = count(msg.text ?? "");
|
|
1806
|
+
if (msg.text?.startsWith("[Compressed conversation section]")) {
|
|
1807
|
+
summaries += tokens;
|
|
1808
|
+
} else if (msg.contentType === "tool-call" || msg.contentType === "tool-result") {
|
|
1809
|
+
tool += tokens;
|
|
1810
|
+
} else if (msg.role === "system") {
|
|
1811
|
+
system += tokens;
|
|
1812
|
+
} else if (msg.text?.includes("```")) {
|
|
1813
|
+
code += tokens;
|
|
1814
|
+
} else {
|
|
1815
|
+
text += tokens;
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
return { system, tool, summaries, code, text, total, growth };
|
|
1819
|
+
}
|
|
1820
|
+
function cloneState(state) {
|
|
1821
|
+
return {
|
|
1822
|
+
blocks: state.blocks.map((block) => ({
|
|
1823
|
+
...block,
|
|
1824
|
+
directMessageIds: [...block.directMessageIds],
|
|
1825
|
+
effectiveMessageIds: [...block.effectiveMessageIds],
|
|
1826
|
+
directBlockIds: [...block.directBlockIds]
|
|
1827
|
+
})),
|
|
1828
|
+
messageRefs: {
|
|
1829
|
+
byRaw: { ...state.messageRefs.byRaw },
|
|
1830
|
+
byRef: { ...state.messageRefs.byRef }
|
|
1831
|
+
},
|
|
1832
|
+
tokenSnapshot: { ...state.tokenSnapshot ?? {} },
|
|
1833
|
+
nudge: { ...state.nudge, anchors: { ...state.nudge.anchors } },
|
|
1834
|
+
stats: { ...state.stats },
|
|
1835
|
+
nextBlockId: state.nextBlockId,
|
|
1836
|
+
nextRunId: state.nextRunId
|
|
1837
|
+
};
|
|
1838
|
+
}
|
|
1839
|
+
function scoreRelevance(block, terms) {
|
|
1840
|
+
const topic = (block.topic ?? "").toLowerCase();
|
|
1841
|
+
const summary = block.summary.toLowerCase();
|
|
1842
|
+
let score = 0;
|
|
1843
|
+
for (const term of terms) {
|
|
1844
|
+
const topicHits = countOccurrences(topic, term);
|
|
1845
|
+
if (topicHits > 0) score += Math.min(topicHits * 0.15, 0.45);
|
|
1846
|
+
const summaryHits = countOccurrences(summary, term);
|
|
1847
|
+
if (summaryHits > 0) score += Math.min(summaryHits * 0.04, 0.2);
|
|
1848
|
+
}
|
|
1849
|
+
return Math.min(score, 1);
|
|
1850
|
+
}
|
|
1851
|
+
function countOccurrences(haystack, needle) {
|
|
1852
|
+
if (!haystack || !needle) return 0;
|
|
1853
|
+
let count = 0;
|
|
1854
|
+
let position = 0;
|
|
1855
|
+
while ((position = haystack.indexOf(needle, position)) !== -1) {
|
|
1856
|
+
count++;
|
|
1857
|
+
position += needle.length;
|
|
1858
|
+
}
|
|
1859
|
+
return count;
|
|
1860
|
+
}
|
|
1861
|
+
var COMPRESS_PHILOSOPHY = `Compression Philosophy:
|
|
1862
|
+
- All compression serves the primary task, but be frugal.
|
|
1863
|
+
- Context capacity is precious. Save context by compressing consumed outputs, not by avoiding tools.
|
|
1864
|
+
- Compress by need, not by percentage.
|
|
1865
|
+
- Work from summaries, not raw tool outputs. All listed ranges (user prompts, tool outputs, code, logs, exploration, intermediate steps) should be compressed to summary format \u2014 the ONLY exceptions are protected content, content the current step is actively using, or critical content you cannot reconstruct.`;
|
|
1866
|
+
var HOW_TO_COMPRESS_RULES = `HOW TO COMPRESS
|
|
1867
|
+
|
|
1868
|
+
When you call \`compress\`, the summary you write becomes the only record of the replaced conversation. Make it self-contained and complete: every user request, experiment purpose, and work task in the range must be accurately captured. A later reader (or you, after decompressing) should be able to continue the task WITHOUT needing the original.
|
|
1869
|
+
|
|
1870
|
+
KEEP VERBATIM \u2014 never paraphrase or abbreviate these:
|
|
1871
|
+
- Full file paths with line numbers, directory prefix on every mention (\`lib/hooks.ts:347\`, \`src/index.ts:12-18\`, \`gatenet_v3/model.py:45\`). Never abbreviate to a bare filename (\`hooks.ts\`, \`model.py\`) \u2014 they are ambiguous and cannot be grepped or decompressed-to later.
|
|
1872
|
+
- Function, class, and type signatures (exact names, params, return types) AND critical code lines that encode logic \u2014 the line that IS the finding, not just the function name (e.g. \`kv_keys += define_gate * a_key[i](emb)\` is more useful than "see model_kvnet.py").
|
|
1873
|
+
- Error messages and stack traces (exact text \u2014 you need the literal string to grep for it later).
|
|
1874
|
+
- Key details from reports and analyses \u2014 not just the conclusion. Keep the comparison numbers and the mechanism, not "X is worse" alone (write "1.76\xD7 PPL gap because KV store is static", not "KVNet underperforms").
|
|
1875
|
+
- Decisions and their rationale ("chose X over Y because Z" \u2014 the "because" is load-bearing; without it the decision looks arbitrary).
|
|
1876
|
+
- Constraints discovered ("must support Node 22", "no new dependencies", "AGENTS.md forbids \`as any\`").
|
|
1877
|
+
- Exact values: versions, config keys, thresholds, magic numbers.
|
|
1878
|
+
- User intent \u2014 quote short user messages verbatim. When the message is too long to quote, preserve intent with extra care: do not change scope, constraints, priorities, acceptance criteria, or requested outcomes. Mark them clearly as past quotes (e.g., "User said: ..."), not as current directives. Losing these changes the task itself.
|
|
1879
|
+
- The user's overall goal and any changes to it \u2014 the big-picture objective plus how it evolved during the compressed range. Each summary must reflect the goal as it stood at the end of the range, including pivots (e.g., "initially: fix bug X \u2192 pivoted to: refactor module Y after discovering root cause"). Losing the goal or its evolution makes all subsequent work appear unmotivated.
|
|
1880
|
+
- Purpose behind each significant action \u2014 preserve not just what was done but why: the hypothesis behind each experiment, the question behind each exploration, the task goal behind each work action. Without purpose, the summary reads as disconnected technical steps with no through-line.
|
|
1881
|
+
- Open questions and unresolved TODOs \u2014 losing these changes what work appears to remain.
|
|
1882
|
+
- Message refs of key anchors (\`m00420\`, \`m00510\u2013m00520\`) \u2014 they let you or a later reader jump back via decompress to the exact original.
|
|
1883
|
+
|
|
1884
|
+
DROP \u2014 extract the signal, discard the vessel:
|
|
1885
|
+
- Verbose logs (build/test/\`npm\` output) once you have captured the error line or the result.
|
|
1886
|
+
- Duplicate file reads once the needed content is recorded.
|
|
1887
|
+
- Consumed exploration \u2014 search hits, agent return values, successful tool outputs \u2014 once you have extracted the facts you need (same rule as dead-ends, but nothing went wrong; the content is simply spent).
|
|
1888
|
+
- Dead-end exploration \u2014 but PRESERVE the lesson in one line: "tried X, failed because Y".
|
|
1889
|
+
- Back-and-forth discussion and self-corrections once the final position is captured (keep the outcome, drop the journey to it).
|
|
1890
|
+
- Repeated status checks (\`git status\`, \`ls\`) once state is known.
|
|
1891
|
+
|
|
1892
|
+
For each significant item you DROP (scripts, reports, large analyses, long tool outputs), add a one-line CONTENT description of what it covers \u2014 not where it lives. Bad: "probe script at /path/probe_kvnet.py". Good: "probe_kvnet.py: tests n-gram baseline, generation quality, long-range dependency, position sensitivity, op pipeline, QUERY attention." This lets a later decompress target the right block by relevance, not by guessing locations.
|
|
1893
|
+
|
|
1894
|
+
PRIORITY \u2014 when the summary must be compact, preserve in this order:
|
|
1895
|
+
1. User's overall goal, goal evolution, intent, and hard constraints (losing these changes the task).
|
|
1896
|
+
2. Decisions and rationale.
|
|
1897
|
+
3. Exact technical artifacts: paths, signatures, errors, values.
|
|
1898
|
+
4. Conclusions and key findings.
|
|
1899
|
+
5. Lessons learned: what failed and why.
|
|
1900
|
+
|
|
1901
|
+
Write dense, scannable bullets \u2014 not narrative prose. If the range spans distinct concerns (request \u2192 findings \u2192 decision), group bullets under short thematic headers so a reader can scan to the part they need. Every line must earn its place. Do not mimic the style of existing summaries in context; follow these rules.`;
|
|
1902
|
+
var TIER2_DISTILL_RULES = `TIER 2 COMPRESSION \u2014 DISTILLATION
|
|
1903
|
+
|
|
1904
|
+
You are compressing historical summaries (not raw conversation). These summaries have already captured the details. Your job is to DISTILL them: extract only what matters for future work, discard the process.
|
|
1905
|
+
|
|
1906
|
+
KEEP \u2014 these are the only things that survive distillation:
|
|
1907
|
+
- Decisions and their rationale ("chose X over Y because Z" \u2014 the "because" is load-bearing).
|
|
1908
|
+
- Final outcomes: version numbers shipped, PR numbers merged/closed, bugs fixed or deferred.
|
|
1909
|
+
- Key lessons: what failed and why ("tried X, failed because Y"). These prevent repeating mistakes.
|
|
1910
|
+
- Critical constraints discovered ("must support Node 22", "AGENTS.md forbids as any").
|
|
1911
|
+
- Design decisions with architectural impact ("chose compress-as-anchor over synthetic messages because prefix cache").
|
|
1912
|
+
- Whether content is OBSOLETE or SUPERSEDED \u2014 mark with one line: "[SUPERSEDED by PR #NNN]" or "[OBSOLETE: deleted in vX.Y.Z]". Do NOT keep the obsolete content's details \u2014 just the marker and reason.
|
|
1913
|
+
- Function/class/type names and module paths that are the SUBJECT of the work \u2014 e.g., "fixed filterCompressedRanges in prune.ts", "added SessionStateRegistry in state.ts". Not exact line numbers or full signatures \u2014 just enough to LOCATE the code without searching.
|
|
1914
|
+
- Exploration findings: if a block was exploratory with no decision, keep the CONCLUSION in one line ("explored X, not viable because Y"). Do not keep the exploration process.
|
|
1915
|
+
|
|
1916
|
+
DROP \u2014 these were useful during the work but are no longer needed:
|
|
1917
|
+
- Exact line numbers, diffs, verbose function signatures, full code listings.
|
|
1918
|
+
- Build/deploy process details, test execution steps.
|
|
1919
|
+
- Review process details (who reviewed, what rounds, test counts).
|
|
1920
|
+
- Verbose logs, command output, intermediate debugging steps.
|
|
1921
|
+
|
|
1922
|
+
FORMAT:
|
|
1923
|
+
- Start each distilled block with a source header line:
|
|
1924
|
+
\`Source: bN+bM+... (XK\u2192YK tok, Zx). [original topic]\`
|
|
1925
|
+
Example: \`Source: b5+b7 (56K+44K\u2192268 tok, 375x). [Tool-result recap + publish]\`
|
|
1926
|
+
- 3-5 bullet points per source block, each a self-contained fact.
|
|
1927
|
+
- Dense, scannable \u2014 no narrative prose.
|
|
1928
|
+
- Start with the outcome, not the process: "v1.13.0 shipped (7 PRs bundled)" not "implemented 7 PRs then reviewed then merged".
|
|
1929
|
+
- Cross-block synthesis: if multiple source blocks cover the same topic (same PR, same feature, same bug), MERGE them into a single group of bullets. Do not repeat the same fact from different blocks \u2014 keep it once under the most relevant source header.
|
|
1930
|
+
|
|
1931
|
+
SIZE TARGET: 50-150 tokens per source block (excluding the header). If you can't fit it in 150 tokens, you're keeping too much process. If a block has nothing worth keeping (pure noise), output just the header followed by "[no actionable content]."`;
|
|
1932
|
+
var TIER3_CONDENSE_RULES = `TIER 3 COMPRESSION \u2014 ULTRA-CONDENSATION
|
|
1933
|
+
|
|
1934
|
+
You are compressing distilled summaries (Tier 2) into ultra-condensed facts (Tier 3). The distilled summaries already contain only decisions and outcomes. Your job is to reduce them to bare factual references.
|
|
1935
|
+
|
|
1936
|
+
PRIORITY \u2014 when a source block has more facts than the size target allows, keep in this order:
|
|
1937
|
+
1. Shipped outcomes (versions released, PRs merged) \u2014 these are permanent record.
|
|
1938
|
+
2. Open work (PRs/issues still pending) \u2014 these may need follow-up.
|
|
1939
|
+
3. Key decisions with architectural impact ("chose X over Y because Z").
|
|
1940
|
+
4. Critical constraints ("must support Node 22").
|
|
1941
|
+
Drop everything else. Tier 3 is a lookup index, not a knowledge base.
|
|
1942
|
+
|
|
1943
|
+
FORMAT:
|
|
1944
|
+
- Start with a source header line:
|
|
1945
|
+
\`Source: bN+bM+... (XK\u2192YK tok, Zx). [original topic]\`
|
|
1946
|
+
- Output 1-3 facts per source block. Each fact is a single line: subject + outcome.
|
|
1947
|
+
- No explanations, no rationale, no process \u2014 just the fact.
|
|
1948
|
+
- Format: "[PR/Issue/Version] \u2014 [outcome in \u22648 words]"
|
|
1949
|
+
- Merge related facts from different source blocks if they concern the same topic.
|
|
1950
|
+
|
|
1951
|
+
EXAMPLES:
|
|
1952
|
+
- "v1.13.0 shipped \u2014 quality gate + GC fix (7 PRs)"
|
|
1953
|
+
- "PR #196 merged \u2014 preserve-first-user (supersedes #169)"
|
|
1954
|
+
- "Bug 1214 fixed \u2014 compress consumed all user messages"
|
|
1955
|
+
- "Chose compress-as-anchor \u2014 prefix cache benefit over synthetic injection"
|
|
1956
|
+
- "Constraint: AGENTS.md forbids as any \u2014 never suppress types"
|
|
1957
|
+
|
|
1958
|
+
DROP:
|
|
1959
|
+
- Multi-sentence context. If a fact needs >1 sentence, it's too detailed for Tier 3.
|
|
1960
|
+
- Lessons learned ("tried X, failed because Y") \u2014 drop UNLESS the failure is likely to recur and the block is <30 days old.
|
|
1961
|
+
- Design rationale details \u2014 keep the decision, drop the "because" unless it's a critical constraint.
|
|
1962
|
+
- Anything marked [OBSOLETE] or [SUPERSEDED] \u2014 drop entirely, note "[N blocks obsolete]" in the summary.
|
|
1963
|
+
|
|
1964
|
+
SIZE TARGET: 30-60 tokens per source block (including header). For a batch of N source blocks, total output \u2248 N \xD7 40 tokens. If a source block has only one trivial fact, output just the header + one line.`;
|
|
1965
|
+
var defaultPrompts = Object.freeze({
|
|
1966
|
+
compressPhilosophy: COMPRESS_PHILOSOPHY,
|
|
1967
|
+
howToCompressRules: HOW_TO_COMPRESS_RULES,
|
|
1968
|
+
tier2DistillRules: TIER2_DISTILL_RULES,
|
|
1969
|
+
tier3CondenseRules: TIER3_CONDENSE_RULES
|
|
1970
|
+
});
|
|
1971
|
+
function efficiencyNote(prompts) {
|
|
1972
|
+
return `This is an efficiency nudge to compress early and keep context lean \u2014 not an overflow warning. A separate, stronger alert will appear if the context is actually full.
|
|
1973
|
+
|
|
1974
|
+
${prompts.compressPhilosophy}`;
|
|
1975
|
+
}
|
|
1976
|
+
function emergencyHeader(prompts) {
|
|
1977
|
+
return `\u26A0\uFE0F Context limit reached \u2014 compress now. Prioritize consumed tool outputs.
|
|
1978
|
+
|
|
1979
|
+
${prompts.compressPhilosophy}`;
|
|
1980
|
+
}
|
|
1981
|
+
function formatK(n) {
|
|
1982
|
+
if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
|
|
1983
|
+
return `${n}`;
|
|
1984
|
+
}
|
|
1985
|
+
function formatBreakdown(bd) {
|
|
1986
|
+
if (!bd) return "";
|
|
1987
|
+
const parts = [];
|
|
1988
|
+
if (bd.system > 0) parts.push(`${formatK(bd.system)} system`);
|
|
1989
|
+
if (bd.tool > 0) parts.push(`${formatK(bd.tool)} tool`);
|
|
1990
|
+
if (bd.summaries > 0) parts.push(`${formatK(bd.summaries)} summaries`);
|
|
1991
|
+
if (bd.code > 0) parts.push(`${formatK(bd.code)} code`);
|
|
1992
|
+
if (bd.text > 0) parts.push(`${formatK(bd.text)} text`);
|
|
1993
|
+
const growth = bd.growth > 0 ? `
|
|
1994
|
+
+${formatK(bd.growth)} since last nudge` : "";
|
|
1995
|
+
return `Context breakdown: ${parts.join(" | ")}${growth}`;
|
|
1996
|
+
}
|
|
1997
|
+
function formatTierTargetBlocks(blocks) {
|
|
1998
|
+
if (blocks.length === 0) {
|
|
1999
|
+
return "Target blocks: (none \u2014 no tier blocks found)";
|
|
2000
|
+
}
|
|
2001
|
+
const lines = blocks.map((b) => {
|
|
2002
|
+
const summaryTokens = Math.ceil((b.summary ?? "").length / 4);
|
|
2003
|
+
const topic = b.topic ? ` "${b.topic}"` : "";
|
|
2004
|
+
return ` ${b.blockId} ${b.effectiveMessageIds.length} msgs ${formatK(b.compressedTokens)}\u2192${formatK(summaryTokens)}${topic}`;
|
|
2005
|
+
});
|
|
2006
|
+
return `Target ${blocks[0].tier === 1 ? "tier-1" : "tier-2"} blocks to distill (${blocks.length}):
|
|
2007
|
+
${lines.join("\n")}`;
|
|
2008
|
+
}
|
|
2009
|
+
function formatRanges(compressible, protectedRanges) {
|
|
2010
|
+
if (compressible.length === 0 && protectedRanges.length === 0) {
|
|
2011
|
+
return "[No specific ranges detected \u2014 compress any consumed content.]";
|
|
2012
|
+
}
|
|
2013
|
+
const refNum2 = (ref) => {
|
|
2014
|
+
const m = ref.match(/\d+/);
|
|
2015
|
+
return m ? parseInt(m[0], 10) : 0;
|
|
2016
|
+
};
|
|
2017
|
+
const entries = [];
|
|
2018
|
+
for (const r of compressible) {
|
|
2019
|
+
entries.push({
|
|
2020
|
+
startRef: r.startRef,
|
|
2021
|
+
endRef: r.endRef,
|
|
2022
|
+
startNum: refNum2(r.startRef),
|
|
2023
|
+
endNum: refNum2(r.endRef),
|
|
2024
|
+
count: r.count,
|
|
2025
|
+
tokens: r.tokens,
|
|
2026
|
+
toolPct: r.toolPct,
|
|
2027
|
+
textPct: r.textPct,
|
|
2028
|
+
compressibleTokens: r.tokens,
|
|
2029
|
+
compressibleCount: r.count,
|
|
2030
|
+
protectedTokens: 0,
|
|
2031
|
+
protectedCount: 0,
|
|
2032
|
+
protectedTools: [],
|
|
2033
|
+
dangerous: r.dangerous ?? false
|
|
2034
|
+
});
|
|
2035
|
+
}
|
|
2036
|
+
for (const r of protectedRanges) {
|
|
2037
|
+
entries.push({
|
|
2038
|
+
startRef: r.startRef,
|
|
2039
|
+
endRef: r.endRef,
|
|
2040
|
+
startNum: refNum2(r.startRef),
|
|
2041
|
+
endNum: refNum2(r.endRef),
|
|
2042
|
+
count: r.count,
|
|
2043
|
+
tokens: r.tokens,
|
|
2044
|
+
toolPct: 0,
|
|
2045
|
+
textPct: 0,
|
|
2046
|
+
compressibleTokens: 0,
|
|
2047
|
+
compressibleCount: 0,
|
|
2048
|
+
protectedTokens: r.tokens,
|
|
2049
|
+
protectedCount: r.count,
|
|
2050
|
+
protectedTools: [...r.tools],
|
|
2051
|
+
dangerous: false
|
|
2052
|
+
});
|
|
2053
|
+
}
|
|
2054
|
+
entries.sort((a, b) => a.startNum - b.startNum);
|
|
2055
|
+
const merged = [];
|
|
2056
|
+
for (const e of entries) {
|
|
2057
|
+
const last = merged[merged.length - 1];
|
|
2058
|
+
if (last && e.startNum <= last.endNum + 1) {
|
|
2059
|
+
last.endRef = e.endRef;
|
|
2060
|
+
last.endNum = Math.max(last.endNum, e.endNum);
|
|
2061
|
+
last.count += e.count;
|
|
2062
|
+
last.tokens += e.tokens;
|
|
2063
|
+
last.compressibleTokens += e.compressibleTokens;
|
|
2064
|
+
last.compressibleCount += e.compressibleCount;
|
|
2065
|
+
last.protectedTokens += e.protectedTokens;
|
|
2066
|
+
last.protectedCount += e.protectedCount;
|
|
2067
|
+
if (e.dangerous) last.dangerous = true;
|
|
2068
|
+
for (const t of e.protectedTools) {
|
|
2069
|
+
if (!last.protectedTools.includes(t)) last.protectedTools.push(t);
|
|
2070
|
+
}
|
|
2071
|
+
} else {
|
|
2072
|
+
merged.push({ ...e });
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
const lines = merged.map((e) => {
|
|
2076
|
+
const suffix = e.dangerous && e.compressibleTokens > 0 ? " \u26A0\uFE0F NOT recommended unless you are certain." : "";
|
|
2077
|
+
if (e.protectedTokens > 0 && e.compressibleTokens === 0) {
|
|
2078
|
+
return ` ${e.startRef}\u2013${e.endRef} ${e.count} msgs ${formatK(e.tokens)} [PROTECTED: ${e.protectedTools.join(", ")} \u2014 not compressible]${suffix}`;
|
|
2079
|
+
}
|
|
2080
|
+
if (e.protectedTokens > 0 && e.compressibleTokens > 0) {
|
|
2081
|
+
return ` ${e.startRef}\u2013${e.endRef} ${e.count} msgs ${formatK(e.tokens)} [${formatK(e.compressibleTokens)} compressible | ${formatK(e.protectedTokens)} protected: ${e.protectedTools.join(", ")}]${suffix}`;
|
|
2082
|
+
}
|
|
2083
|
+
return ` ${e.startRef}\u2013${e.endRef} ${e.count} msgs ${formatK(e.tokens)} [tool ${e.toolPct}% | text ${e.textPct}%]${suffix}`;
|
|
2084
|
+
});
|
|
2085
|
+
return `Compressible ranges (${merged.length}, oldest first):
|
|
2086
|
+
${lines.join("\n")}`;
|
|
2087
|
+
}
|
|
2088
|
+
function renderNudgeText(decision, prompts = defaultPrompts) {
|
|
2089
|
+
const breakdownStr = formatBreakdown(decision.contextBreakdown);
|
|
2090
|
+
const rangesStr = formatRanges(decision.compressibleRanges, decision.protectedRanges ?? []);
|
|
2091
|
+
const isEmergency = !!decision.breakdown?.emergencyOverride || !!decision.breakdown?.overLimit;
|
|
2092
|
+
if (decision.tier !== null && decision.tier >= 2) {
|
|
2093
|
+
const isT2 = decision.tier === 2;
|
|
2094
|
+
const targets = decision.tierTargetBlocks ?? [];
|
|
2095
|
+
const blockList = formatTierTargetBlocks(targets);
|
|
2096
|
+
const startId = targets[0]?.blockId ?? "b1";
|
|
2097
|
+
const endId = targets[targets.length - 1]?.blockId ?? "b5";
|
|
2098
|
+
const voice = isEmergency ? "emergency" : "gentle";
|
|
2099
|
+
const triggerLine = isEmergency ? `[EMERGENCY \u2014 TIER ${decision.tier} ${isT2 ? "DISTILLATION" : "CONDENSATION"}] Context limit reached \u2014 distill NOW into a denser summary to reclaim tokens.` : `[TIER ${decision.tier} ${isT2 ? "DISTILLATION" : "CONDENSATION"} TRIGGER]`;
|
|
2100
|
+
return {
|
|
2101
|
+
voice,
|
|
2102
|
+
text: [
|
|
2103
|
+
efficiencyNote(prompts),
|
|
2104
|
+
"",
|
|
2105
|
+
breakdownStr,
|
|
2106
|
+
"",
|
|
2107
|
+
triggerLine,
|
|
2108
|
+
isT2 ? `Your tier-1 compression summaries have accumulated. Distill them into a single denser tier-2 summary. Use block IDs as boundaries (startId and endId as bN). Any raw (uncompressed) messages sitting between the boundary blocks are absorbed into the tier-2 block as well \u2014 apply HOW TO COMPRESS to those raw messages and the TIER 2 distillation rules to the existing summaries, so the whole span is covered and nothing is lost.` : `Your tier-2 compression summaries have accumulated. Condense them further into a tier-3 ultra-condensed summary. Use block IDs as boundaries (startId and endId as bN). Any raw (uncompressed) messages sitting between the boundary blocks are absorbed into the tier-3 block as well \u2014 apply HOW TO COMPRESS to those raw messages and the TIER 3 condensation rules to the existing summaries, so the whole span is covered and nothing is lost.`,
|
|
2109
|
+
blockList,
|
|
2110
|
+
`Example: compress({ content: [{ startId: "${startId}", endId: "${endId}", summary: "..." }] })`,
|
|
2111
|
+
"",
|
|
2112
|
+
prompts.howToCompressRules,
|
|
2113
|
+
"",
|
|
2114
|
+
isT2 ? prompts.tier2DistillRules : prompts.tier3CondenseRules
|
|
2115
|
+
].join("\n")
|
|
2116
|
+
};
|
|
2117
|
+
}
|
|
2118
|
+
if (isEmergency) {
|
|
2119
|
+
return {
|
|
2120
|
+
voice: "emergency",
|
|
2121
|
+
text: [
|
|
2122
|
+
emergencyHeader(prompts),
|
|
2123
|
+
"",
|
|
2124
|
+
breakdownStr,
|
|
2125
|
+
"",
|
|
2126
|
+
prompts.howToCompressRules,
|
|
2127
|
+
"",
|
|
2128
|
+
`{ "topic": "...", "content": [{ "startId": "<ID>", "endId": "<ID>", "summary": "..." }] }`,
|
|
2129
|
+
"Only use IDs from visible messages above. Compress older work first.",
|
|
2130
|
+
"",
|
|
2131
|
+
rangesStr
|
|
2132
|
+
].join("\n")
|
|
2133
|
+
};
|
|
2134
|
+
}
|
|
2135
|
+
return {
|
|
2136
|
+
voice: "gentle",
|
|
2137
|
+
text: [
|
|
2138
|
+
efficiencyNote(prompts),
|
|
2139
|
+
"",
|
|
2140
|
+
breakdownStr,
|
|
2141
|
+
"",
|
|
2142
|
+
prompts.howToCompressRules,
|
|
2143
|
+
"",
|
|
2144
|
+
rangesStr,
|
|
2145
|
+
"",
|
|
2146
|
+
`\u{1F4A1} Compress all ranges in one call (pass multiple content entries: \`content: [{...}, {...}]\`).`
|
|
2147
|
+
].join("\n")
|
|
2148
|
+
};
|
|
2149
|
+
}
|
|
2150
|
+
function formatTokens2(n) {
|
|
2151
|
+
if (!Number.isFinite(n) || n <= 0) return "0";
|
|
2152
|
+
return n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : String(n);
|
|
2153
|
+
}
|
|
2154
|
+
function pct(n, total) {
|
|
2155
|
+
if (n <= 0 || total <= 0) return 0;
|
|
2156
|
+
return Math.max(1, Math.round(n / total * 100));
|
|
2157
|
+
}
|
|
2158
|
+
function numericPart2(blockId) {
|
|
2159
|
+
const match = /^b(\d+)$/.exec(blockId);
|
|
2160
|
+
return match && match[1] !== void 0 ? Number(match[1]) : 0;
|
|
2161
|
+
}
|
|
2162
|
+
function summaryTokensOf(block, countTokens) {
|
|
2163
|
+
return countTokens(block.summary);
|
|
2164
|
+
}
|
|
2165
|
+
function effectiveCompressedTokens(block, _state, _countTokens) {
|
|
2166
|
+
return block.compressedTokens;
|
|
2167
|
+
}
|
|
2168
|
+
function tierLabel(block) {
|
|
2169
|
+
return `T${block.tier}`;
|
|
2170
|
+
}
|
|
2171
|
+
function tierBreakdown(blocks, countTokens) {
|
|
2172
|
+
const tierTokens = {};
|
|
2173
|
+
for (const block of blocks) {
|
|
2174
|
+
tierTokens[block.tier] = (tierTokens[block.tier] ?? 0) + summaryTokensOf(block, countTokens);
|
|
2175
|
+
}
|
|
2176
|
+
const tiers = Object.keys(tierTokens).map(Number);
|
|
2177
|
+
if (tiers.length <= 1) return null;
|
|
2178
|
+
const parts = [];
|
|
2179
|
+
for (const tier of [1, 2, 3]) {
|
|
2180
|
+
if (tierTokens[tier]) parts.push(`T${tier}: ${formatTokens2(tierTokens[tier])}`);
|
|
2181
|
+
}
|
|
2182
|
+
return parts.join(" | ");
|
|
2183
|
+
}
|
|
2184
|
+
function collectVisible(messages, state, countTokens) {
|
|
2185
|
+
const coveredIds = /* @__PURE__ */ new Set();
|
|
2186
|
+
for (const block of state.blocks) {
|
|
2187
|
+
if (!block.active) continue;
|
|
2188
|
+
for (const id of block.effectiveMessageIds) coveredIds.add(id);
|
|
2189
|
+
}
|
|
2190
|
+
let summaryTokens = 0;
|
|
2191
|
+
for (const block of state.blocks) {
|
|
2192
|
+
if (block.active) summaryTokens += summaryTokensOf(block, countTokens);
|
|
2193
|
+
}
|
|
2194
|
+
const visible = [];
|
|
2195
|
+
messages.forEach((message, index) => {
|
|
2196
|
+
if (coveredIds.has(message.id)) return;
|
|
2197
|
+
const ref = refForRaw(state.messageRefs, message.id);
|
|
2198
|
+
if (!ref) return;
|
|
2199
|
+
const tokens = countTokens(message.text ?? "");
|
|
2200
|
+
const tool = message.toolName ?? "text";
|
|
2201
|
+
if (tokens > 0) visible.push({ ref, tokens, tool, index });
|
|
2202
|
+
});
|
|
2203
|
+
return { visible, summaryTokens };
|
|
2204
|
+
}
|
|
2205
|
+
function buildStatusReport(state, messages, countTokens, options = {}) {
|
|
2206
|
+
const scope = options.scope;
|
|
2207
|
+
const view = options.view ?? "ranges";
|
|
2208
|
+
const toolFilter = options.tool;
|
|
2209
|
+
const sort = options.sort ?? "size";
|
|
2210
|
+
const limit = options.limit ?? 30;
|
|
2211
|
+
const activeBlocks2 = state.blocks.filter((b) => b.active).sort((a, b) => numericPart2(a.blockId) - numericPart2(b.blockId));
|
|
2212
|
+
if (scope === "compressed") {
|
|
2213
|
+
return renderCompressedDrilldown(activeBlocks2, state, sort, limit, countTokens);
|
|
2214
|
+
}
|
|
2215
|
+
const { visible, summaryTokens } = collectVisible(messages, state, countTokens);
|
|
2216
|
+
if (scope === "uncompressed") {
|
|
2217
|
+
if (view === "messages") {
|
|
2218
|
+
return renderMessageDrilldown(visible, toolFilter, sort, limit);
|
|
2219
|
+
}
|
|
2220
|
+
return renderUncompressedRanges(visible);
|
|
2221
|
+
}
|
|
2222
|
+
return renderOverview(visible, summaryTokens, activeBlocks2, state, countTokens, limit);
|
|
2223
|
+
}
|
|
2224
|
+
function renderOverview(visible, summaryTokens, blocks, state, countTokens, limit) {
|
|
2225
|
+
const lines = [];
|
|
2226
|
+
const toolTypeMap = /* @__PURE__ */ new Map();
|
|
2227
|
+
for (const message of visible) {
|
|
2228
|
+
toolTypeMap.set(message.tool, (toolTypeMap.get(message.tool) ?? 0) + message.tokens);
|
|
2229
|
+
}
|
|
2230
|
+
const topTool = [...toolTypeMap.entries()].sort((a, b) => b[1] - a[1])[0]?.[0];
|
|
2231
|
+
const totalTool = visible.filter((m) => m.tool !== "text").reduce((sum, m) => sum + m.tokens, 0);
|
|
2232
|
+
const totalText = visible.filter((m) => m.tool === "text").reduce((sum, m) => sum + m.tokens, 0);
|
|
2233
|
+
const total = summaryTokens + totalTool + totalText;
|
|
2234
|
+
lines.push("CONTEXT BREAKDOWN");
|
|
2235
|
+
lines.push(
|
|
2236
|
+
` ${formatTokens2(totalTool)} tool (${pct(totalTool, total)}%) | ${formatTokens2(totalText)} text (${pct(totalText, total)}%) | ${formatTokens2(summaryTokens)} summaries (${pct(summaryTokens, total)}%)`
|
|
2237
|
+
);
|
|
2238
|
+
const topTypes = [...toolTypeMap.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3);
|
|
2239
|
+
if (topTypes.length > 0) {
|
|
2240
|
+
lines.push(` Top tools: ${topTypes.map(([t, n]) => `${t} (${pct(n, total)}%)`).join(", ")}`);
|
|
2241
|
+
}
|
|
2242
|
+
lines.push("");
|
|
2243
|
+
if (blocks.length === 0) {
|
|
2244
|
+
lines.push("COMPRESSED BLOCKS");
|
|
2245
|
+
lines.push(" No compressed blocks.");
|
|
2246
|
+
} else {
|
|
2247
|
+
const totalSummary = blocks.reduce((s, b) => s + summaryTokensOf(b, countTokens), 0);
|
|
2248
|
+
const totalEffective = blocks.reduce(
|
|
2249
|
+
(s, b) => s + effectiveCompressedTokens(b, state, countTokens),
|
|
2250
|
+
0
|
|
2251
|
+
);
|
|
2252
|
+
lines.push(
|
|
2253
|
+
`COMPRESSED BLOCKS \u2014 ${blocks.length} active (${formatTokens2(totalSummary)} summary, ${formatTokens2(totalEffective)} original)`
|
|
2254
|
+
);
|
|
2255
|
+
const breakdown = tierBreakdown(blocks, countTokens);
|
|
2256
|
+
if (breakdown) lines.push(` Tier usage: ${breakdown}`);
|
|
2257
|
+
lines.push("");
|
|
2258
|
+
const sorted = [...blocks].sort(
|
|
2259
|
+
(a, b) => effectiveCompressedTokens(b, state, countTokens) - effectiveCompressedTokens(a, state, countTokens) || b.createdAt - a.createdAt
|
|
2260
|
+
);
|
|
2261
|
+
for (const block of sorted.slice(0, limit)) {
|
|
2262
|
+
const topic = block.topic ?? "(no topic)";
|
|
2263
|
+
const eff = effectiveCompressedTokens(block, state, countTokens);
|
|
2264
|
+
lines.push(
|
|
2265
|
+
` ${block.blockId} (${tierLabel(block)}) ${formatTokens2(eff)}\u2192${formatTokens2(summaryTokensOf(block, countTokens))} ${block.effectiveMessageIds.length} msgs "${topic}"`
|
|
2266
|
+
);
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
lines.push("");
|
|
2270
|
+
lines.push(
|
|
2271
|
+
`Tip: buildStatusReport({scope:"uncompressed", view:"messages", tool:"${topTool ?? "bash"}"}) for per-message listing`
|
|
2272
|
+
);
|
|
2273
|
+
return lines.join("\n");
|
|
2274
|
+
}
|
|
2275
|
+
function renderUncompressedRanges(visible) {
|
|
2276
|
+
const lines = [];
|
|
2277
|
+
const totalTokens = visible.reduce((s, m) => s + m.tokens, 0);
|
|
2278
|
+
lines.push(`UNCOMPRESSED \u2014 ${formatTokens2(totalTokens)} | ${visible.length} visible messages`);
|
|
2279
|
+
lines.push("");
|
|
2280
|
+
if (visible.length === 0) {
|
|
2281
|
+
lines.push(" (no uncompressed messages)");
|
|
2282
|
+
return lines.join("\n");
|
|
2283
|
+
}
|
|
2284
|
+
const refNum2 = (ref) => {
|
|
2285
|
+
const m = ref.match(/\d+/);
|
|
2286
|
+
return m ? parseInt(m[0], 10) : 0;
|
|
2287
|
+
};
|
|
2288
|
+
const merged = [];
|
|
2289
|
+
for (const m of visible) {
|
|
2290
|
+
const num = refNum2(m.ref);
|
|
2291
|
+
const last = merged[merged.length - 1];
|
|
2292
|
+
if (last && num === last.startNum + last.count) {
|
|
2293
|
+
last.endRef = m.ref;
|
|
2294
|
+
last.count += 1;
|
|
2295
|
+
last.tokens += m.tokens;
|
|
2296
|
+
} else {
|
|
2297
|
+
merged.push({ startRef: m.ref, endRef: m.ref, startNum: num, count: 1, tokens: m.tokens, tool: m.tool });
|
|
2298
|
+
}
|
|
2299
|
+
}
|
|
2300
|
+
for (const r of merged.slice(0, 30)) {
|
|
2301
|
+
const range = r.count === 1 ? r.startRef : `${r.startRef}\u2013${r.endRef}`;
|
|
2302
|
+
lines.push(` ${range} (${r.count} msgs, ${formatTokens2(r.tokens)}${r.count > 1 ? ` (${Math.round(r.tokens / r.count)}/msg)` : ""}) ${r.tool}`);
|
|
2303
|
+
}
|
|
2304
|
+
if (merged.length > 30) {
|
|
2305
|
+
lines.push(` ... and ${merged.length - 30} more ranges`);
|
|
2306
|
+
}
|
|
2307
|
+
return lines.join("\n");
|
|
2308
|
+
}
|
|
2309
|
+
function renderMessageDrilldown(visible, toolFilter, sort, limit) {
|
|
2310
|
+
let filtered = visible;
|
|
2311
|
+
if (toolFilter) filtered = filtered.filter((m) => m.tool === toolFilter);
|
|
2312
|
+
if (sort === "time") filtered.sort((a, b) => a.index - b.index);
|
|
2313
|
+
else if (sort === "tool") filtered.sort((a, b) => a.tool.localeCompare(b.tool) || b.tokens - a.tokens);
|
|
2314
|
+
else filtered.sort((a, b) => b.tokens - a.tokens);
|
|
2315
|
+
const totalTokens = filtered.reduce((s, m) => s + m.tokens, 0);
|
|
2316
|
+
const allTokens = visible.reduce((s, m) => s + m.tokens, 0);
|
|
2317
|
+
const header = toolFilter ? `UNCOMPRESSED \u2014 ${toolFilter}: ${formatTokens2(totalTokens)} | ${filtered.length} msgs | ${pct(totalTokens, allTokens)}% of visible` : `UNCOMPRESSED \u2014 ${formatTokens2(totalTokens)} | ${filtered.length} msgs`;
|
|
2318
|
+
const lines = [header, `Sorted by ${sort}`, ""];
|
|
2319
|
+
const shown = filtered.slice(0, limit);
|
|
2320
|
+
for (const message of shown) {
|
|
2321
|
+
lines.push(` ${message.ref} (${formatTokens2(message.tokens)}) ${message.tool}`);
|
|
2322
|
+
}
|
|
2323
|
+
if (filtered.length > shown.length) {
|
|
2324
|
+
lines.push("");
|
|
2325
|
+
lines.push(`${shown.length} of ${filtered.length} shown.`);
|
|
2326
|
+
}
|
|
2327
|
+
return lines.join("\n");
|
|
2328
|
+
}
|
|
2329
|
+
function renderCompressedDrilldown(blocks, state, sort, limit, countTokens) {
|
|
2330
|
+
let sorted = [...blocks];
|
|
2331
|
+
if (sort === "time") sorted.sort((a, b) => a.createdAt - b.createdAt);
|
|
2332
|
+
else if (sort === "age") sorted.sort((a, b) => b.survivedCount - a.survivedCount);
|
|
2333
|
+
else
|
|
2334
|
+
sorted.sort(
|
|
2335
|
+
(a, b) => effectiveCompressedTokens(b, state, countTokens) - effectiveCompressedTokens(a, state, countTokens) || b.createdAt - a.createdAt
|
|
2336
|
+
);
|
|
2337
|
+
const totalSummary = sorted.reduce((s, b) => s + summaryTokensOf(b, countTokens), 0);
|
|
2338
|
+
const totalEffective = sorted.reduce(
|
|
2339
|
+
(s, b) => s + effectiveCompressedTokens(b, state, countTokens),
|
|
2340
|
+
0
|
|
2341
|
+
);
|
|
2342
|
+
const lines = [
|
|
2343
|
+
`COMPRESSED \u2014 ${sorted.length} blocks | ${formatTokens2(totalEffective)} original \u2192 ${formatTokens2(totalSummary)} summary`
|
|
2344
|
+
];
|
|
2345
|
+
const breakdown = tierBreakdown(sorted, countTokens);
|
|
2346
|
+
if (breakdown) lines.push(`Tier usage: ${breakdown}`);
|
|
2347
|
+
lines.push("");
|
|
2348
|
+
const shown = sorted.slice(0, limit);
|
|
2349
|
+
for (const block of shown) {
|
|
2350
|
+
const nested = block.directBlockIds.length > 0 ? ` nested=[${block.directBlockIds.join(",")}]` : "";
|
|
2351
|
+
const topic = block.topic ?? "(no topic)";
|
|
2352
|
+
const eff = effectiveCompressedTokens(block, state, countTokens);
|
|
2353
|
+
lines.push(
|
|
2354
|
+
` ${block.blockId} (${tierLabel(block)}) ${formatTokens2(eff)}\u2192${formatTokens2(summaryTokensOf(block, countTokens))} ${block.effectiveMessageIds.length} msgs age=${block.survivedCount} ${block.generation}${nested}`
|
|
2355
|
+
);
|
|
2356
|
+
lines.push(` "${topic}"`);
|
|
2357
|
+
}
|
|
2358
|
+
if (sorted.length > shown.length) {
|
|
2359
|
+
lines.push("");
|
|
2360
|
+
lines.push(`${shown.length} of ${sorted.length} shown.`);
|
|
2361
|
+
}
|
|
2362
|
+
return lines.join("\n");
|
|
2363
|
+
}
|
|
2364
|
+
function stem(word) {
|
|
2365
|
+
let w = word;
|
|
2366
|
+
if (w.length <= 3) return w;
|
|
2367
|
+
if (w.endsWith("ies")) w = w.slice(0, -3) + "y";
|
|
2368
|
+
else if (w.endsWith("ses") || w.endsWith("xes") || w.endsWith("zes")) w = w.slice(0, -2);
|
|
2369
|
+
else if (w.endsWith("ches") || w.endsWith("shes")) w = w.slice(0, -2);
|
|
2370
|
+
else if (w.endsWith("s") && !w.endsWith("ss")) w = w.slice(0, -1);
|
|
2371
|
+
if (w.endsWith("ing") && w.length > 5) w = w.slice(0, -3);
|
|
2372
|
+
if (w.endsWith("ed") && w.length > 4) w = w.slice(0, -2);
|
|
2373
|
+
if (w.endsWith("ation") && w.length > 6) w = w.slice(0, -3);
|
|
2374
|
+
else if (w.endsWith("tion") && w.length > 5) w = w.slice(0, -4) + "t";
|
|
2375
|
+
else if (w.endsWith("ion") && w.length > 4) w = w.slice(0, -3);
|
|
2376
|
+
if (w.endsWith("ment") && w.length > 6) w = w.slice(0, -4);
|
|
2377
|
+
if (w.endsWith("ness") && w.length > 6) w = w.slice(0, -4);
|
|
2378
|
+
if (w.endsWith("ly") && w.length > 4) w = w.slice(0, -2);
|
|
2379
|
+
return w;
|
|
2380
|
+
}
|
|
2381
|
+
var CJK = /[\u3400-\u9fff\uf900-\ufaff\u3040-\u30ff\uac00-\ud7af]/;
|
|
2382
|
+
var LATIN_WORD = /[a-z][a-z0-9_]*[a-z0-9]|[a-z0-9]/g;
|
|
2383
|
+
var cjkSegmenter = new Intl.Segmenter("zh", { granularity: "word" });
|
|
2384
|
+
function cjkRunTokens(segs) {
|
|
2385
|
+
const words = segs.filter((w) => w.length >= 2);
|
|
2386
|
+
if (words.length > 0) return words;
|
|
2387
|
+
const run = segs.join("");
|
|
2388
|
+
const out = [];
|
|
2389
|
+
for (let i = 0; i < run.length - 1; i++) out.push(run.slice(i, i + 2));
|
|
2390
|
+
for (const ch of run) out.push(ch);
|
|
2391
|
+
return out;
|
|
2392
|
+
}
|
|
2393
|
+
function tokenize(text, opts = {}) {
|
|
2394
|
+
const lower = text.toLowerCase();
|
|
2395
|
+
const tokens = [];
|
|
2396
|
+
const latin = lower.match(LATIN_WORD) ?? [];
|
|
2397
|
+
for (let w of latin) {
|
|
2398
|
+
if (w.length >= 2) {
|
|
2399
|
+
if (opts.stem) w = stem(w);
|
|
2400
|
+
tokens.push(w);
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
if (!CJK.test(lower)) return tokens;
|
|
2404
|
+
const runSegs = [];
|
|
2405
|
+
let cur = null;
|
|
2406
|
+
for (const s of cjkSegmenter.segment(lower)) {
|
|
2407
|
+
const t = s.segment;
|
|
2408
|
+
if (t.length === 0) continue;
|
|
2409
|
+
if (CJK.test(t)) {
|
|
2410
|
+
(cur ??= []).push(t);
|
|
2411
|
+
} else if (cur) {
|
|
2412
|
+
runSegs.push(cur);
|
|
2413
|
+
cur = null;
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
2416
|
+
if (cur) runSegs.push(cur);
|
|
2417
|
+
for (const segs of runSegs) {
|
|
2418
|
+
tokens.push(...cjkRunTokens(segs));
|
|
2419
|
+
}
|
|
2420
|
+
return tokens;
|
|
2421
|
+
}
|
|
2422
|
+
function charBigrams(text) {
|
|
2423
|
+
const grams = [];
|
|
2424
|
+
for (let i = 0; i < text.length - 1; i++) {
|
|
2425
|
+
const pair = text.slice(i, i + 2);
|
|
2426
|
+
if (pair.trim().length === pair.length) grams.push(pair);
|
|
2427
|
+
}
|
|
2428
|
+
return grams;
|
|
2429
|
+
}
|
|
2430
|
+
function tfMap(text, stem2) {
|
|
2431
|
+
const m = /* @__PURE__ */ new Map();
|
|
2432
|
+
for (const t of tokenize(text, { stem: stem2 })) m.set(t, (m.get(t) ?? 0) + 1);
|
|
2433
|
+
return m;
|
|
2434
|
+
}
|
|
2435
|
+
var DEFAULT_CAP_CHARS = 8 * 1024 * 1024;
|
|
2436
|
+
var capChars = DEFAULT_CAP_CHARS;
|
|
2437
|
+
var cache = /* @__PURE__ */ new Map();
|
|
2438
|
+
var cachedChars = 0;
|
|
2439
|
+
function build(text) {
|
|
2440
|
+
const tf = tfMap(text, true);
|
|
2441
|
+
let len = 0;
|
|
2442
|
+
for (const v of tf.values()) len += v;
|
|
2443
|
+
const lower = text.toLowerCase();
|
|
2444
|
+
return { tf, len, lower, grams: new Set(charBigrams(lower)) };
|
|
2445
|
+
}
|
|
2446
|
+
function docFeatures(text) {
|
|
2447
|
+
const hit = cache.get(text);
|
|
2448
|
+
if (hit) return hit;
|
|
2449
|
+
const f = build(text);
|
|
2450
|
+
if (text.length > 0 && text.length <= capChars) {
|
|
2451
|
+
while (cachedChars + text.length > capChars && cache.size > 0) {
|
|
2452
|
+
const k = cache.keys().next().value;
|
|
2453
|
+
cachedChars -= k.length;
|
|
2454
|
+
cache.delete(k);
|
|
2455
|
+
}
|
|
2456
|
+
cache.set(text, f);
|
|
2457
|
+
cachedChars += text.length;
|
|
2458
|
+
}
|
|
2459
|
+
return f;
|
|
2460
|
+
}
|
|
2461
|
+
var substringAlgorithm = {
|
|
2462
|
+
name: "substring",
|
|
2463
|
+
description: "Exact substring counting (original baseline). Predictable, no normalization.",
|
|
2464
|
+
score(docs, query) {
|
|
2465
|
+
const terms = query.toLowerCase().trim().split(/\s+/).filter((t) => t.length > 0);
|
|
2466
|
+
if (terms.length === 0) return docs.map((d) => ({ ref: d.ref, score: 0 }));
|
|
2467
|
+
return docs.map((d) => {
|
|
2468
|
+
const haystack = docFeatures(d.text).lower;
|
|
2469
|
+
let score = 0;
|
|
2470
|
+
for (const term of terms) score += countOccurrences2(haystack, term);
|
|
2471
|
+
return { ref: d.ref, score };
|
|
2472
|
+
});
|
|
2473
|
+
}
|
|
2474
|
+
};
|
|
2475
|
+
function countOccurrences2(haystack, needle) {
|
|
2476
|
+
if (!needle) return 0;
|
|
2477
|
+
return haystack.split(needle).length - 1;
|
|
2478
|
+
}
|
|
2479
|
+
var bm25Algorithm = {
|
|
2480
|
+
name: "bm25",
|
|
2481
|
+
description: "BM25 with stemming + CJK bigram tokenization. IR-standard relevance ranking.",
|
|
2482
|
+
score(docs, query) {
|
|
2483
|
+
const N = docs.length;
|
|
2484
|
+
const k1 = 1.2;
|
|
2485
|
+
const b = 0.75;
|
|
2486
|
+
const parsed = docs.map((d) => {
|
|
2487
|
+
const f = docFeatures(d.text);
|
|
2488
|
+
return { id: d.ref, tf: f.tf, len: f.len };
|
|
2489
|
+
});
|
|
2490
|
+
const avgdl = parsed.reduce((s, d) => s + d.len, 0) / (N || 1);
|
|
2491
|
+
const qTerms = tokenize(query, { stem: true });
|
|
2492
|
+
if (qTerms.length === 0) return docs.map((d) => ({ ref: d.ref, score: 0 }));
|
|
2493
|
+
const idf = /* @__PURE__ */ new Map();
|
|
2494
|
+
for (const t of new Set(qTerms)) {
|
|
2495
|
+
let df = 0;
|
|
2496
|
+
for (const d of parsed) if (d.tf.has(t)) df++;
|
|
2497
|
+
idf.set(t, Math.log(1 + (N - df + 0.5) / (df + 0.5)));
|
|
2498
|
+
}
|
|
2499
|
+
return parsed.map((d) => {
|
|
2500
|
+
let score = 0;
|
|
2501
|
+
for (const t of qTerms) {
|
|
2502
|
+
const f = d.tf.get(t) ?? 0;
|
|
2503
|
+
if (f === 0) continue;
|
|
2504
|
+
const idfT = idf.get(t) ?? 0;
|
|
2505
|
+
score += idfT * (f * (k1 + 1)) / (f + k1 * (1 - b + b * d.len / (avgdl || 1)));
|
|
2506
|
+
}
|
|
2507
|
+
return { ref: d.id, score };
|
|
2508
|
+
});
|
|
2509
|
+
}
|
|
2510
|
+
};
|
|
2511
|
+
var fuzzyAlgorithm = {
|
|
2512
|
+
name: "fuzzy",
|
|
2513
|
+
description: "Character bigram overlap. Typo-tolerant, script-agnostic, high recall.",
|
|
2514
|
+
score(docs, query) {
|
|
2515
|
+
const qTokens = query.toLowerCase().split(/[\s,]+/).filter((t) => t.length >= 4 || t.length >= 2 && CJK.test(t));
|
|
2516
|
+
if (qTokens.length === 0) return docs.map((d) => ({ ref: d.ref, score: 0 }));
|
|
2517
|
+
const qGrams = /* @__PURE__ */ new Set();
|
|
2518
|
+
for (const t of qTokens) for (const g of charBigrams(t)) qGrams.add(g);
|
|
2519
|
+
if (qGrams.size === 0) return docs.map((d) => ({ ref: d.ref, score: 0 }));
|
|
2520
|
+
return docs.map((d) => {
|
|
2521
|
+
const docGrams = docFeatures(d.text).grams;
|
|
2522
|
+
let hits = 0;
|
|
2523
|
+
for (const g of qGrams) if (docGrams.has(g)) hits++;
|
|
2524
|
+
return { ref: d.ref, score: hits / qGrams.size };
|
|
2525
|
+
});
|
|
2526
|
+
}
|
|
2527
|
+
};
|
|
2528
|
+
var W_BM25 = 0.7;
|
|
2529
|
+
var W_FUZZY = 0.3;
|
|
2530
|
+
var hybridAlgorithm = {
|
|
2531
|
+
name: "hybrid",
|
|
2532
|
+
description: "Weighted BM25(stem) + fuzzy n-gram. Default \u2014 best precision + recall.",
|
|
2533
|
+
score(docs, query) {
|
|
2534
|
+
const bm = bm25Algorithm.score(docs, query);
|
|
2535
|
+
const fz = fuzzyAlgorithm.score(docs, query);
|
|
2536
|
+
const maxBm = Math.max(...bm.map((r) => r.score), 1e-9);
|
|
2537
|
+
const maxFz = Math.max(...fz.map((r) => r.score), 1e-9);
|
|
2538
|
+
const bmMap = new Map(bm.map((r) => [r.ref, r.score / maxBm]));
|
|
2539
|
+
const fzMap = new Map(fz.map((r) => [r.ref, r.score / maxFz]));
|
|
2540
|
+
return docs.map((d) => ({
|
|
2541
|
+
ref: d.ref,
|
|
2542
|
+
score: W_BM25 * (bmMap.get(d.ref) ?? 0) + W_FUZZY * (fzMap.get(d.ref) ?? 0)
|
|
2543
|
+
}));
|
|
2544
|
+
}
|
|
2545
|
+
};
|
|
2546
|
+
var registry2 = /* @__PURE__ */ new Map();
|
|
2547
|
+
function registerSearchAlgorithm(algo) {
|
|
2548
|
+
registry2.set(algo.name, algo);
|
|
2549
|
+
}
|
|
2550
|
+
function getSearchAlgorithm(name) {
|
|
2551
|
+
return registry2.get(name);
|
|
2552
|
+
}
|
|
2553
|
+
registerSearchAlgorithm(substringAlgorithm);
|
|
2554
|
+
registerSearchAlgorithm(bm25Algorithm);
|
|
2555
|
+
registerSearchAlgorithm(fuzzyAlgorithm);
|
|
2556
|
+
registerSearchAlgorithm(hybridAlgorithm);
|
|
2557
|
+
var DEFAULT_ROLE_WEIGHTS = {
|
|
2558
|
+
user: 1.5,
|
|
2559
|
+
assistant: 1,
|
|
2560
|
+
tool: 0.6,
|
|
2561
|
+
block: 1
|
|
2562
|
+
};
|
|
2563
|
+
var DEFAULT_ALGORITHM = "hybrid";
|
|
2564
|
+
function applyRoleWeight(scored, docs, rw) {
|
|
2565
|
+
if (docs.length === 0) return scored;
|
|
2566
|
+
const docByRef = new Map(docs.map((d) => [d.ref, d]));
|
|
2567
|
+
return scored.map((s) => {
|
|
2568
|
+
const doc = docByRef.get(s.ref);
|
|
2569
|
+
if (!doc) return s;
|
|
2570
|
+
const w = doc.kind === "message" ? doc.role === "user" ? rw.user : doc.role === "assistant" ? rw.assistant : rw.tool : rw.block;
|
|
2571
|
+
return { ref: s.ref, score: s.score * w };
|
|
2572
|
+
});
|
|
2573
|
+
}
|
|
2574
|
+
function runSearch(docs, query, options) {
|
|
2575
|
+
const limit = options.limit ?? 10;
|
|
2576
|
+
const previewLength = options.previewLength ?? 200;
|
|
2577
|
+
const minScore = options.minScore ?? 0.01;
|
|
2578
|
+
const algoName = options.algorithm ?? DEFAULT_ALGORITHM;
|
|
2579
|
+
const rw = { ...DEFAULT_ROLE_WEIGHTS, ...options.roleWeights };
|
|
2580
|
+
const algo = getSearchAlgorithm(algoName);
|
|
2581
|
+
if (!algo) return [];
|
|
2582
|
+
if (docs.length === 0) return [];
|
|
2583
|
+
const scoredOrPromise = algo.score(docs, query);
|
|
2584
|
+
const buildResults = (weighted) => {
|
|
2585
|
+
const byRef = new Map(docs.map((d) => [d.ref, d]));
|
|
2586
|
+
return weighted.map((s) => {
|
|
2587
|
+
const doc = byRef.get(s.ref);
|
|
2588
|
+
if (!doc) return null;
|
|
2589
|
+
return {
|
|
2590
|
+
kind: doc.kind,
|
|
2591
|
+
ref: doc.ref,
|
|
2592
|
+
blockId: doc.blockId,
|
|
2593
|
+
tier: doc.tier ?? 1,
|
|
2594
|
+
score: s.score,
|
|
2595
|
+
title: doc.title,
|
|
2596
|
+
preview: makePreview(doc.text, query, previewLength),
|
|
2597
|
+
role: doc.role,
|
|
2598
|
+
tokens: doc.tokens
|
|
2599
|
+
};
|
|
2600
|
+
}).filter((r) => r !== null && r.score >= minScore).sort((a, b) => b.score - a.score).slice(0, limit);
|
|
2601
|
+
};
|
|
2602
|
+
if (scoredOrPromise instanceof Promise) {
|
|
2603
|
+
return scoredOrPromise.then((raw) => buildResults(applyRoleWeight(raw, docs, rw)));
|
|
2604
|
+
}
|
|
2605
|
+
return buildResults(applyRoleWeight(scoredOrPromise, docs, rw));
|
|
2606
|
+
}
|
|
2607
|
+
function searchBlocks(docs, query, options = {}) {
|
|
2608
|
+
const result = runSearch(docs, query, options);
|
|
2609
|
+
if (result instanceof Promise) {
|
|
2610
|
+
throw new Error(
|
|
2611
|
+
`searchBlocks: algorithm "${options.algorithm ?? DEFAULT_ALGORITHM}" is async (e.g. semantic). Use searchBlocksAsync() instead.`
|
|
2612
|
+
);
|
|
2613
|
+
}
|
|
2614
|
+
return result;
|
|
2615
|
+
}
|
|
2616
|
+
function makePreview(text, query, len) {
|
|
2617
|
+
if (!text) return "";
|
|
2618
|
+
const terms = query.toLowerCase().trim().split(/\s+/).filter((t) => t.length > 1);
|
|
2619
|
+
if (terms.length === 0) return text.slice(0, len);
|
|
2620
|
+
const lower = text.toLowerCase();
|
|
2621
|
+
let hitIdx = -1;
|
|
2622
|
+
for (const term of terms) {
|
|
2623
|
+
const idx = lower.indexOf(term);
|
|
2624
|
+
if (idx >= 0) {
|
|
2625
|
+
hitIdx = idx;
|
|
2626
|
+
break;
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
if (hitIdx < 0) return text.slice(0, len);
|
|
2630
|
+
const half = Math.max(0, Math.floor(len / 2) - 10);
|
|
2631
|
+
const start = Math.max(0, hitIdx - half);
|
|
2632
|
+
const end = Math.min(text.length, start + len);
|
|
2633
|
+
const prefix = start > 0 ? "\u2026" : "";
|
|
2634
|
+
const suffix = end < text.length ? "\u2026" : "";
|
|
2635
|
+
return prefix + text.slice(start, end).trim() + suffix;
|
|
2636
|
+
}
|
|
10
2637
|
|
|
11
2638
|
// src/region.ts
|
|
12
2639
|
import { randomUUID } from "crypto";
|
|
@@ -17,7 +2644,6 @@ import {
|
|
|
17
2644
|
toolPairingBalancedBefore
|
|
18
2645
|
} from "@deepseek-ai/dsh-compaction";
|
|
19
2646
|
import { createAssistantMessage, createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
20
|
-
import { defaultCountTokens } from "acp-kernel";
|
|
21
2647
|
|
|
22
2648
|
// src/messages.ts
|
|
23
2649
|
function extractText(content) {
|
|
@@ -307,6 +2933,7 @@ function runCompactionTransaction(session, input) {
|
|
|
307
2933
|
model: input.model,
|
|
308
2934
|
tier: input.tier ?? 1,
|
|
309
2935
|
...input.kernelBlockId === void 0 ? {} : { kernelBlockId: input.kernelBlockId },
|
|
2936
|
+
...input.topic === void 0 ? {} : { topic: input.topic },
|
|
310
2937
|
...input.parentBlockIds === void 0 || input.parentBlockIds.length === 0 ? {} : { parentBlockIds: [...input.parentBlockIds] },
|
|
311
2938
|
...input.directMessageIds === void 0 ? {} : { directMessageIds: [...input.directMessageIds] },
|
|
312
2939
|
...input.effectiveMessageIds === void 0 ? {} : { effectiveMessageIds: [...input.effectiveMessageIds] }
|
|
@@ -351,6 +2978,7 @@ function rebuildBlockLedger(events) {
|
|
|
351
2978
|
ledger.push({
|
|
352
2979
|
blockId: data.compactionId,
|
|
353
2980
|
summary: extractText(data.summary),
|
|
2981
|
+
...typeof data.topic === "string" ? { topic: data.topic } : {},
|
|
354
2982
|
shadowedSeqs: [...data.shadowedSeqs],
|
|
355
2983
|
shadowedTokenCount,
|
|
356
2984
|
start: data.shadowedRange.start,
|
|
@@ -366,6 +2994,12 @@ function rebuildBlockLedger(events) {
|
|
|
366
2994
|
}
|
|
367
2995
|
return ledger;
|
|
368
2996
|
}
|
|
2997
|
+
function isToolEvent(event) {
|
|
2998
|
+
if (event.type === "tool/result") return true;
|
|
2999
|
+
if (event.type !== "assistant/message") return false;
|
|
3000
|
+
const content = event.data.message?.content;
|
|
3001
|
+
return Array.isArray(content) && content.some((block) => block?.type === "tool-call");
|
|
3002
|
+
}
|
|
369
3003
|
function isCheckpointNode(event) {
|
|
370
3004
|
if (event.type !== "user/message") return false;
|
|
371
3005
|
const source = event.data.source;
|
|
@@ -559,7 +3193,9 @@ function buildCompressibleSeqRanges(session, opts = {}) {
|
|
|
559
3193
|
const nodes = session.surface.nodes;
|
|
560
3194
|
const preserve = opts.preserveRecent ?? 5;
|
|
561
3195
|
const protectedSeqs = /* @__PURE__ */ new Set();
|
|
562
|
-
|
|
3196
|
+
if (preserve > 0) {
|
|
3197
|
+
for (const seq of nodes.slice(-preserve)) protectedSeqs.add(seq);
|
|
3198
|
+
}
|
|
563
3199
|
for (let index = nodes.length - 1; index >= 0; index -= 1) {
|
|
564
3200
|
const event = session.events[nodes[index]];
|
|
565
3201
|
if (event?.type === "user/message" && !isCheckpointNode(event)) {
|
|
@@ -584,10 +3220,11 @@ function buildCompressibleSeqRanges(session, opts = {}) {
|
|
|
584
3220
|
cur = null;
|
|
585
3221
|
}
|
|
586
3222
|
const tokens = defaultCountTokens(extractEventText(event));
|
|
3223
|
+
const isTool = isToolEvent(event);
|
|
587
3224
|
if (cur === null) {
|
|
588
|
-
cur = { start: seq, end: seq, count: 1, tokens };
|
|
3225
|
+
cur = { start: seq, end: seq, count: 1, tokens, toolCount: isTool ? 1 : 0 };
|
|
589
3226
|
} else {
|
|
590
|
-
cur = { start: cur.start, end: seq, count: cur.count + 1, tokens: cur.tokens + tokens };
|
|
3227
|
+
cur = { start: cur.start, end: seq, count: cur.count + 1, tokens: cur.tokens + tokens, toolCount: cur.toolCount + (isTool ? 1 : 0) };
|
|
591
3228
|
}
|
|
592
3229
|
}
|
|
593
3230
|
flush();
|
|
@@ -596,11 +3233,17 @@ function buildCompressibleSeqRanges(session, opts = {}) {
|
|
|
596
3233
|
try {
|
|
597
3234
|
const { start, end } = resolveSurfaceRange(session, range.start, range.end);
|
|
598
3235
|
const count = range.count;
|
|
599
|
-
out.push({
|
|
3236
|
+
out.push({
|
|
3237
|
+
start,
|
|
3238
|
+
end,
|
|
3239
|
+
count,
|
|
3240
|
+
tokens: range.tokens,
|
|
3241
|
+
toolPct: count > 0 ? Math.round(range.toolCount / count * 100) : 0
|
|
3242
|
+
});
|
|
600
3243
|
} catch {
|
|
601
3244
|
}
|
|
602
3245
|
}
|
|
603
|
-
return out.sort((a, b) =>
|
|
3246
|
+
return out.sort((a, b) => a.start - b.start);
|
|
604
3247
|
}
|
|
605
3248
|
function surfaceSummary(session) {
|
|
606
3249
|
const nodes = session.surface.nodes;
|
|
@@ -735,6 +3378,7 @@ function rebuildKernelBlocks(events) {
|
|
|
735
3378
|
runId: `r${blocks.length + 1}`,
|
|
736
3379
|
tier: entry.tier,
|
|
737
3380
|
summary: entry.summary,
|
|
3381
|
+
...entry.topic === void 0 ? {} : { topic: entry.topic },
|
|
738
3382
|
directMessageIds: [...direct],
|
|
739
3383
|
effectiveMessageIds: [...effective],
|
|
740
3384
|
directBlockIds: parentKernelIds.get(entry.blockId) ?? [],
|
|
@@ -781,10 +3425,8 @@ var AcpStateStore = class {
|
|
|
781
3425
|
|
|
782
3426
|
// src/tools.ts
|
|
783
3427
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
784
|
-
import { buildStatusReport, defaultCountTokens as defaultCountTokens3 } from "acp-kernel";
|
|
785
3428
|
|
|
786
3429
|
// src/config.ts
|
|
787
|
-
import { defaultConfig } from "acp-kernel";
|
|
788
3430
|
function kernelConfigFor(input) {
|
|
789
3431
|
const nudgePatch = {};
|
|
790
3432
|
if (input.nudgeMinContextLimitPct !== void 0) nudgePatch.minContextLimitPct = input.nudgeMinContextLimitPct;
|
|
@@ -798,17 +3440,9 @@ function kernelConfigFor(input) {
|
|
|
798
3440
|
}
|
|
799
3441
|
|
|
800
3442
|
// src/nudge.ts
|
|
801
|
-
import {
|
|
802
|
-
COMPRESS_PHILOSOPHY as COMPRESS_PHILOSOPHY2,
|
|
803
|
-
TIER2_DISTILL_RULES as TIER2_DISTILL_RULES2,
|
|
804
|
-
TIER3_CONDENSE_RULES as TIER3_CONDENSE_RULES2,
|
|
805
|
-
defaultCountTokens as defaultCountTokens2,
|
|
806
|
-
renderNudgeText
|
|
807
|
-
} from "acp-kernel";
|
|
808
3443
|
import { createUserMessage as createUserMessage2 } from "@deepseek-ai/dsh-llm";
|
|
809
3444
|
|
|
810
3445
|
// src/prompts.ts
|
|
811
|
-
import { COMPRESS_PHILOSOPHY, HOW_TO_COMPRESS_RULES, TIER2_DISTILL_RULES, TIER3_CONDENSE_RULES } from "acp-kernel";
|
|
812
3446
|
var NUDGE_ALLOWED = {
|
|
813
3447
|
normal: /* @__PURE__ */ new Set(["pct", "philosophy"]),
|
|
814
3448
|
emergency: /* @__PURE__ */ new Set(["pct", "philosophy"]),
|
|
@@ -895,15 +3529,15 @@ var DEFAULT_PROMPTS = {
|
|
|
895
3529
|
},
|
|
896
3530
|
rangeTable: {
|
|
897
3531
|
header: "Surface: {surface}",
|
|
898
|
-
title: "Compressible ranges (
|
|
899
|
-
line: " - seq {start}..{end} \u2014 {count} messages, ~{tokens} tokens",
|
|
3532
|
+
title: "Compressible ranges ({count}, oldest first; exact surface seqs \u2014 usable as-is):",
|
|
3533
|
+
line: " - seq {start}..{end} \u2014 {count} messages, ~{tokens} tokens [tool {toolPct}% | text {textPct}%]",
|
|
900
3534
|
footer: "Compress with: compress({ content: [{ startSeq, endSeq, summary }] }) \u2014 content is an array: batch multiple unrelated segments in one call, each entry its own block. Keep ranges disjoint.\nSnapshot taken at nudge time: the seqs go stale once the surface moves (a later compress shadows them), so re-run acp_status for fresh refs before compressing."
|
|
901
3535
|
},
|
|
902
3536
|
tools: {
|
|
903
|
-
compress: "Replace older conversation ranges with dense summaries you write. Each message seq is a surface reference. Single range: compress({ content: [{ startSeq, endSeq, summary }] }). Batch multiple unrelated ranges in one call (each content entry becomes its own block); keep ranges disjoint. Never compress content the current step is actively using. Compress boundaries are SURFACE SEQS (acp_status Surface: row, latest nudge table) \u2014 NOT the block refs (bN, e.g. b1) that acp_status COMPRESSED BLOCKS shows, which are for decompress only. Seq refs must come from the CURRENT surface (acp_status or the latest nudge): a span whose edges were shadowed by an earlier compress is auto-remapped to its still-live content, a fully compressed span is reported as already compressed, and invented/other-session seqs fail with guidance.",
|
|
3537
|
+
compress: "Replace older conversation ranges with dense summaries you write. Each message seq is a surface reference. Single range: compress({ content: [{ startSeq, endSeq, summary }] }). Batch multiple unrelated ranges in one call (each content entry becomes its own block); keep ranges disjoint. Never compress content the current step is actively using. Compress boundaries are SURFACE SEQS (acp_status Surface: row, latest nudge table) \u2014 NOT the block refs (bN, e.g. b1) that acp_status COMPRESSED BLOCKS shows, which are for decompress only. Drilldown mN refs (e.g. m00306) are ALSO accepted as startSeq/endSeq \u2014 they are auto-mapped to the live surface seq; an unknown mN (never assigned on the current surface) fails with guidance. Seq refs must come from the CURRENT surface (acp_status or the latest nudge): a span whose edges were shadowed by an earlier compress is auto-remapped to its still-live content, a fully compressed span is reported as already compressed, and invented/other-session seqs fail with guidance.",
|
|
904
3538
|
decompress: "Recover the original content of a compressed block by its blockId \u2014 the kernel block ref `bN` shown by acp_status (e.g. b1), or a compaction id from search_context (read-only; does not unshadow the range).",
|
|
905
3539
|
searchContext: "Search inside compressed blocks (summaries and original content) for information the model no longer sees in context.",
|
|
906
|
-
acpStatus: 'Context status: overview of the current context \u2014 CONTEXT BREAKDOWN (tool/text/summaries token shares of the visible total), COMPRESSED BLOCKS ledger, and the nudge decision. No args = overview. Percentages are shares of the visible content, not the context window. Note: the block refs in COMPRESSED BLOCKS (bN, e.g. b1) are for decompress; compress uses the Surface: seq range, not bN. Drilldown: pass scope:"compressed" for a per-block list, or scope:"uncompressed" with view:"messages" (every visible message) / view:"ranges" (merged ranges); tool filters to one tool name, sort reorders (size/time/tool; age for compressed), limit caps rows (default 30). Drilldown row refs are kernel ids (mN)
|
|
3540
|
+
acpStatus: 'Context status: overview of the current context \u2014 CONTEXT BREAKDOWN (tool/text/summaries token shares of the visible total), COMPRESSED BLOCKS ledger, and the nudge decision. No args = overview. Percentages are shares of the visible content, not the context window. Note: the block refs in COMPRESSED BLOCKS (bN, e.g. b1) are for decompress; compress uses the Surface: seq range, not bN. Drilldown: pass scope:"compressed" for a per-block list, or scope:"uncompressed" with view:"messages" (every visible message) / view:"ranges" (merged ranges); tool filters to one tool name, sort reorders (size/time/tool; age for compressed), limit caps rows (default 30). Drilldown row refs are kernel ids (mN) \u2014 feed them straight to compress as startSeq/endSeq (auto-mapped to the live surface seq); bN is for decompress, Surface: seqs also work in compress.'
|
|
907
3541
|
},
|
|
908
3542
|
systemPromptTemplate: `Active Context Pruning \u2014 model-driven context management
|
|
909
3543
|
|
|
@@ -931,7 +3565,7 @@ Compression tools (refs are SURFACE SEQS, not ids):
|
|
|
931
3565
|
- compress: replace one or more seq ranges, each with your own dense summary. Single range: compress({ content: [{ startSeq, endSeq, summary }] }). Batch multiple unrelated segments in one call (each entry becomes its own block): compress({ content: [{ startSeq: 1, endSeq: 5, summary: '...' }, { startSeq: 12, endSeq: 18, summary: '...' }] }). Keep ranges disjoint \u2014 overlapping entries in one batch are skipped. Edges are auto-balanced to tool-call/result boundaries; a trailing #callId fragment in a seq is ignored. Seq refs must be on the current surface: seqs from older nudges or earlier compresses go stale as the surface moves, so a stale span is auto-remapped to its still-live remainder (the result reports the adjusted span), a fully compressed span is reported as already compressed, and invented/other-session seqs fail with guidance. The block refs (bN, e.g. b1) in acp_status COMPRESSED BLOCKS are for decompress, NOT compress boundaries.
|
|
932
3566
|
- decompress: recover a compressed block's original content, read-only. decompress({ blockId }) \u2014 accept the bN ref shown by acp_status (e.g. b1) or a compaction id.
|
|
933
3567
|
- search_context: find information inside compressed blocks BEFORE decompressing. search_context({ query }).
|
|
934
|
-
- acp_status: current context usage and the live compressible-range list. Run it right before compressing \u2014 the only seqs that never go stale are the ones you just read. Drilldown (scope/view/tool/sort/limit) lists per-message or per-block sizes; drilldown rows are kernel ids (mN)
|
|
3568
|
+
- acp_status: current context usage and the live compressible-range list. Run it right before compressing \u2014 the only seqs that never go stale are the ones you just read. Drilldown (scope/view/tool/sort/limit) lists per-message or per-block sizes; drilldown rows are kernel ids (mN) \u2014 compress accepts them directly (auto-mapped to the live surface seq).
|
|
935
3569
|
|
|
936
3570
|
Tiered compression: each compressed block appears on the surface as one summary node. Compressing that node again DISTILLS the block (tier 2): the parent summary folds into your new summary and the original messages are freed. Distilling a tier-2 block yields tier 3. Distill when a summary itself is consumed \u2014 decompress on the tier-2 block recovers the full originals.
|
|
937
3571
|
|
|
@@ -951,7 +3585,7 @@ function resolveTokenCount(agent, coreMessages) {
|
|
|
951
3585
|
const meter = agent.ctx?.get?.("tokenMeter");
|
|
952
3586
|
const surface = meter?.measure?.(agent.session)?.surfaceTokens;
|
|
953
3587
|
if (typeof surface === "number" && surface > 0) return surface;
|
|
954
|
-
return coreMessages.reduce((sum, message) => sum +
|
|
3588
|
+
return coreMessages.reduce((sum, message) => sum + defaultCountTokens(message.text ?? ""), 0);
|
|
955
3589
|
}
|
|
956
3590
|
function rangeTable(session, prompts = DEFAULT_RESOLVED) {
|
|
957
3591
|
const ranges = buildCompressibleSeqRanges(session).slice(0, 6);
|
|
@@ -961,7 +3595,9 @@ function rangeTable(session, prompts = DEFAULT_RESOLVED) {
|
|
|
961
3595
|
start: range.start,
|
|
962
3596
|
end: range.end,
|
|
963
3597
|
count: range.count,
|
|
964
|
-
tokens: range.tokens
|
|
3598
|
+
tokens: range.tokens,
|
|
3599
|
+
toolPct: range.toolPct,
|
|
3600
|
+
textPct: 100 - range.toolPct
|
|
965
3601
|
})
|
|
966
3602
|
);
|
|
967
3603
|
return [
|
|
@@ -1057,10 +3693,10 @@ function replaceEmergencyExample(text) {
|
|
|
1057
3693
|
return text.slice(0, start) + "\n\ncompress({ content: [{ startSeq, endSeq, summary }] }) \u2014 use the seqs from the range table above." + text.slice(end);
|
|
1058
3694
|
}
|
|
1059
3695
|
function renderNudgeFromTemplates(nudge, emergency, session, prompts) {
|
|
1060
|
-
const
|
|
3696
|
+
const pct2 = Math.round(Math.min(nudge.contextUsage, 1) * 100);
|
|
1061
3697
|
const frame = renderTemplate(
|
|
1062
3698
|
emergency ? prompts.nudge.emergency : prompts.nudge.normal,
|
|
1063
|
-
{ pct, philosophy:
|
|
3699
|
+
{ pct: pct2, philosophy: COMPRESS_PHILOSOPHY }
|
|
1064
3700
|
);
|
|
1065
3701
|
const parts = [frame];
|
|
1066
3702
|
if (nudge.contextBreakdown) {
|
|
@@ -1092,7 +3728,7 @@ function renderNudgeFromTemplates(nudge, emergency, session, prompts) {
|
|
|
1092
3728
|
seqs: summarySeqs.join(", ")
|
|
1093
3729
|
});
|
|
1094
3730
|
if (tierLine !== "") parts.push(tierLine);
|
|
1095
|
-
const tierRules = nudge.tier === 2 ?
|
|
3731
|
+
const tierRules = nudge.tier === 2 ? TIER2_DISTILL_RULES : TIER3_CONDENSE_RULES;
|
|
1096
3732
|
parts.push("", tierRules);
|
|
1097
3733
|
} else {
|
|
1098
3734
|
parts.push(rangeTable(session, prompts));
|
|
@@ -1165,6 +3801,32 @@ function parseSeq(value) {
|
|
|
1165
3801
|
}
|
|
1166
3802
|
return seq;
|
|
1167
3803
|
}
|
|
3804
|
+
var MN_RE = /^m0*(\d{1,5})(?:#.*)?$/i;
|
|
3805
|
+
function mnRefIndex(value) {
|
|
3806
|
+
const match = MN_RE.exec(value.trim());
|
|
3807
|
+
if (match === null) return null;
|
|
3808
|
+
const index = Number(match[1]);
|
|
3809
|
+
return index >= 1 && index <= 99999 ? index : null;
|
|
3810
|
+
}
|
|
3811
|
+
function parseBoundary2(value, byRef) {
|
|
3812
|
+
const text = String(value);
|
|
3813
|
+
const index = mnRefIndex(text);
|
|
3814
|
+
if (index === null) return parseSeq(value);
|
|
3815
|
+
const ref = `m${String(index).padStart(5, "0")}`;
|
|
3816
|
+
const raw = byRef[ref];
|
|
3817
|
+
if (raw === void 0) {
|
|
3818
|
+
throw new Error(
|
|
3819
|
+
`billion-context-dsh: mN "${text}" not found on the current surface \u2014 re-run acp_status for fresh refs (the surface may have moved)`
|
|
3820
|
+
);
|
|
3821
|
+
}
|
|
3822
|
+
const seq = Number(String(raw).split("#")[0]);
|
|
3823
|
+
if (!Number.isInteger(seq) || seq < 0) {
|
|
3824
|
+
throw new Error(
|
|
3825
|
+
`billion-context-dsh: mN "${text}" maps to a non-seq id "${raw}" \u2014 re-run acp_status`
|
|
3826
|
+
);
|
|
3827
|
+
}
|
|
3828
|
+
return seq;
|
|
3829
|
+
}
|
|
1168
3830
|
function unwrapCompressArgs(args) {
|
|
1169
3831
|
if (args.content !== void 0) return args;
|
|
1170
3832
|
if (args.arguments === void 0) return null;
|
|
@@ -1207,6 +3869,7 @@ async function handleCompress(env, args, exec) {
|
|
|
1207
3869
|
const turn = env.kernel.processTurn({ messages: coreMessages, state, config, tokenCount });
|
|
1208
3870
|
env.store.set(session, turn.state);
|
|
1209
3871
|
const byRaw = turn.state.messageRefs.byRaw;
|
|
3872
|
+
const byRef = turn.state.messageRefs.byRef;
|
|
1210
3873
|
const unwrapped = unwrapCompressArgs(args);
|
|
1211
3874
|
if (unwrapped === null) {
|
|
1212
3875
|
return {
|
|
@@ -1217,8 +3880,8 @@ async function handleCompress(env, args, exec) {
|
|
|
1217
3880
|
const ranges = [];
|
|
1218
3881
|
const alreadyCompressedNotes = [];
|
|
1219
3882
|
for (const range of args.content) {
|
|
1220
|
-
const startSeq =
|
|
1221
|
-
const endSeq =
|
|
3883
|
+
const startSeq = parseBoundary2(range.startSeq, byRef);
|
|
3884
|
+
const endSeq = parseBoundary2(range.endSeq, byRef);
|
|
1222
3885
|
let resolved;
|
|
1223
3886
|
try {
|
|
1224
3887
|
resolved = resolveSurfaceRange(session, startSeq, endSeq);
|
|
@@ -1308,7 +3971,7 @@ async function handleCompress(env, args, exec) {
|
|
|
1308
3971
|
let shadowedTokens = 0;
|
|
1309
3972
|
for (const seq of shadowed) {
|
|
1310
3973
|
const event = session.events[seq];
|
|
1311
|
-
if (event !== void 0) shadowedTokens +=
|
|
3974
|
+
if (event !== void 0) shadowedTokens += defaultCountTokens(extractEventText(event));
|
|
1312
3975
|
}
|
|
1313
3976
|
const tier = block.tier === 2 || block.tier === 3 ? block.tier : 1;
|
|
1314
3977
|
const parentBlockIds = compactionIdsOfKernelBlocks(session, block.directBlockIds);
|
|
@@ -1322,6 +3985,7 @@ async function handleCompress(env, args, exec) {
|
|
|
1322
3985
|
model: agent.options.model ?? "",
|
|
1323
3986
|
tier,
|
|
1324
3987
|
kernelBlockId: block.blockId,
|
|
3988
|
+
...range.topic === void 0 ? {} : { topic: range.topic },
|
|
1325
3989
|
...parentBlockIds.length === 0 ? {} : { parentBlockIds },
|
|
1326
3990
|
// Record the kernel block's raw coverage so a restarted engine
|
|
1327
3991
|
// rehydrates the SAME effective messages (a tier-2 block's coverage is
|
|
@@ -1330,10 +3994,10 @@ async function handleCompress(env, args, exec) {
|
|
|
1330
3994
|
effectiveMessageIds: block.effectiveMessageIds
|
|
1331
3995
|
});
|
|
1332
3996
|
const adjusted = start !== range.startSeq || end !== range.endSeq;
|
|
1333
|
-
const
|
|
3997
|
+
const tierLabel2 = tier === 1 ? "" : `, tier ${tier}`;
|
|
1334
3998
|
const note = range.recovered === true ? ` (seqs ${range.startSeq}..${range.endSeq} were already shadowed \u2014 compressed the live remainder ${start}..${end})` : adjusted ? ` (adjusted from ${range.startSeq}..${range.endSeq} to balanced edges)` : "";
|
|
1335
3999
|
lines.push(
|
|
1336
|
-
` block ${compactionId.slice(0, 8)}: seqs ${start}..${end}, ${shadowed.length} messages shadowed${
|
|
4000
|
+
` block ${compactionId.slice(0, 8)}: seqs ${start}..${end}, ${shadowed.length} messages shadowed${tierLabel2}${note}`
|
|
1337
4001
|
);
|
|
1338
4002
|
}
|
|
1339
4003
|
const summaryLine = `Compressed ${applied.result.blocksCreated} block(s), ~${applied.result.tokensCompressed} tokens reclaimed.`;
|
|
@@ -1383,26 +4047,70 @@ var searchParameters = {
|
|
|
1383
4047
|
query: { type: "string", required: true, description: "Search terms to find inside compressed blocks." },
|
|
1384
4048
|
limit: { type: "integer", description: "Maximum results (default 5)." }
|
|
1385
4049
|
};
|
|
4050
|
+
function roleOfEvent(event) {
|
|
4051
|
+
switch (event.type) {
|
|
4052
|
+
case "user/message":
|
|
4053
|
+
return "user";
|
|
4054
|
+
case "assistant/message":
|
|
4055
|
+
return "assistant";
|
|
4056
|
+
case "tool/result":
|
|
4057
|
+
return "tool";
|
|
4058
|
+
default:
|
|
4059
|
+
return null;
|
|
4060
|
+
}
|
|
4061
|
+
}
|
|
4062
|
+
function buildSearchDocs(session) {
|
|
4063
|
+
const ledger = rebuildBlockLedger(session.events);
|
|
4064
|
+
const docs = [];
|
|
4065
|
+
const claimed = /* @__PURE__ */ new Set();
|
|
4066
|
+
for (const block of ledger) {
|
|
4067
|
+
docs.push({
|
|
4068
|
+
kind: "block",
|
|
4069
|
+
ref: block.blockId,
|
|
4070
|
+
text: block.summary,
|
|
4071
|
+
title: block.summary.slice(0, 60) || block.blockId,
|
|
4072
|
+
blockId: block.blockId,
|
|
4073
|
+
tier: block.tier,
|
|
4074
|
+
tokens: defaultCountTokens(block.summary)
|
|
4075
|
+
});
|
|
4076
|
+
for (const seq of expandShadowedSeqs(session, block.blockId)) {
|
|
4077
|
+
if (claimed.has(seq)) continue;
|
|
4078
|
+
claimed.add(seq);
|
|
4079
|
+
const event = session.events[seq];
|
|
4080
|
+
if (event === void 0) continue;
|
|
4081
|
+
const role = roleOfEvent(event);
|
|
4082
|
+
const text = extractEventText(event);
|
|
4083
|
+
if (role === null || text.length === 0) continue;
|
|
4084
|
+
docs.push({
|
|
4085
|
+
kind: "message",
|
|
4086
|
+
ref: `seq ${seq}`,
|
|
4087
|
+
text,
|
|
4088
|
+
title: `${role}: ${text.slice(0, 60)}`,
|
|
4089
|
+
role,
|
|
4090
|
+
blockId: block.blockId,
|
|
4091
|
+
tier: block.tier,
|
|
4092
|
+
tokens: defaultCountTokens(text)
|
|
4093
|
+
});
|
|
4094
|
+
}
|
|
4095
|
+
}
|
|
4096
|
+
return docs;
|
|
4097
|
+
}
|
|
1386
4098
|
function handleSearch(_env, rawArgs, exec) {
|
|
1387
4099
|
const args = unwrapEnvelope(rawArgs);
|
|
1388
4100
|
const session = requireAgent(exec).session;
|
|
1389
|
-
|
|
1390
|
-
const
|
|
1391
|
-
const
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
const
|
|
1395
|
-
${
|
|
1396
|
-
|
|
1397
|
-
for (const term of terms) score += haystack.split(term).length - 1;
|
|
1398
|
-
if (score > 0) scored.push({ blockId: block.blockId, score, summary: block.summary });
|
|
1399
|
-
}
|
|
1400
|
-
scored.sort((a, b) => b.score - a.score);
|
|
1401
|
-
const top = scored.slice(0, args.limit ?? 5);
|
|
1402
|
-
if (top.length === 0) return { text: `search_context: no matches for "${args.query}"` };
|
|
4101
|
+
if (args.query.trim() === "") return { text: "search_context: empty query (no matches)" };
|
|
4102
|
+
const docs = buildSearchDocs(session);
|
|
4103
|
+
const results = searchBlocks(docs, args.query, { limit: args.limit ?? 5, previewLength: 160 });
|
|
4104
|
+
if (results.length === 0) return { text: `search_context: no matches for "${args.query}"` };
|
|
4105
|
+
const lines = results.map((r) => {
|
|
4106
|
+
const kind = r.kind === "block" ? `block ${r.ref}` : `message ${r.ref} (${r.role ?? "?"}, in block ${r.blockId ?? "?"})`;
|
|
4107
|
+
return ` - ${kind} (score ${r.score.toFixed(2)}): ${r.preview}`;
|
|
4108
|
+
});
|
|
1403
4109
|
return {
|
|
1404
4110
|
text: `Matches for "${args.query}":
|
|
1405
|
-
|
|
4111
|
+
${lines.join("\n")}
|
|
4112
|
+
|
|
4113
|
+
Decompress with: decompress({ blockId })`
|
|
1406
4114
|
};
|
|
1407
4115
|
}
|
|
1408
4116
|
var statusParameters = {
|
|
@@ -1451,7 +4159,7 @@ async function handleStatus(env, rawArgs, exec) {
|
|
|
1451
4159
|
surface.filter((event) => !isCheckpointEvent(event)),
|
|
1452
4160
|
toolNames
|
|
1453
4161
|
);
|
|
1454
|
-
const report = buildStatusReport(turn.state, statusMessages,
|
|
4162
|
+
const report = buildStatusReport(turn.state, statusMessages, defaultCountTokens, args);
|
|
1455
4163
|
const lines = [report];
|
|
1456
4164
|
if (args.scope === void 0) {
|
|
1457
4165
|
const nudge = turn.nudge;
|
|
@@ -1461,7 +4169,7 @@ async function handleStatus(env, rawArgs, exec) {
|
|
|
1461
4169
|
}
|
|
1462
4170
|
lines.push("", `Surface: ${surfaceSummary(session)}`);
|
|
1463
4171
|
if (args.scope === "uncompressed") {
|
|
1464
|
-
lines.push("", "Note: drilldown rows are kernel refs (mN)
|
|
4172
|
+
lines.push("", "Note: drilldown rows are kernel refs (mN) \u2014 feed them straight to compress (auto-mapped to the live surface seq); an unknown mN fails with guidance.");
|
|
1465
4173
|
}
|
|
1466
4174
|
return { text: lines.join("\n") };
|
|
1467
4175
|
}
|
|
@@ -1507,9 +4215,6 @@ function makeTools(env) {
|
|
|
1507
4215
|
];
|
|
1508
4216
|
}
|
|
1509
4217
|
|
|
1510
|
-
// src/commands.ts
|
|
1511
|
-
import { defaultCountTokens as defaultCountTokens4 } from "acp-kernel";
|
|
1512
|
-
|
|
1513
4218
|
// src/window.ts
|
|
1514
4219
|
var DEFAULT_CONTEXT_WINDOW = 128e3;
|
|
1515
4220
|
function windowSourceLabel(window) {
|
|
@@ -1573,7 +4278,7 @@ function compressText(env, agent, args) {
|
|
|
1573
4278
|
let shadowedTokens = 0;
|
|
1574
4279
|
for (const seq of shadowed) {
|
|
1575
4280
|
const event = session.events[seq];
|
|
1576
|
-
if (event !== void 0) shadowedTokens +=
|
|
4281
|
+
if (event !== void 0) shadowedTokens += defaultCountTokens(extractEventText(event));
|
|
1577
4282
|
}
|
|
1578
4283
|
const { compactionId } = runCompactionTransaction(session, {
|
|
1579
4284
|
start,
|
|
@@ -1680,10 +4385,10 @@ var AcpCompactionEngine = class extends CompactionEngine {
|
|
|
1680
4385
|
let done = false;
|
|
1681
4386
|
const registerTools = () => {
|
|
1682
4387
|
if (done) return;
|
|
1683
|
-
const
|
|
1684
|
-
if (
|
|
4388
|
+
const registry3 = ctx.get("tools");
|
|
4389
|
+
if (registry3 === void 0) return;
|
|
1685
4390
|
done = true;
|
|
1686
|
-
for (const tool of makeTools(env))
|
|
4391
|
+
for (const tool of makeTools(env)) registry3.register(tool);
|
|
1687
4392
|
};
|
|
1688
4393
|
ctx.on("internal/service", (name) => {
|
|
1689
4394
|
if (name === "tools") registerTools();
|
|
@@ -1696,10 +4401,10 @@ var AcpCompactionEngine = class extends CompactionEngine {
|
|
|
1696
4401
|
let done = false;
|
|
1697
4402
|
const registerCommand = () => {
|
|
1698
4403
|
if (done) return;
|
|
1699
|
-
const
|
|
1700
|
-
if (
|
|
4404
|
+
const registry3 = ctx.get("commands");
|
|
4405
|
+
if (registry3 === void 0) return;
|
|
1701
4406
|
done = true;
|
|
1702
|
-
|
|
4407
|
+
registry3.register(acpCommand(env));
|
|
1703
4408
|
};
|
|
1704
4409
|
ctx.on("internal/service", (name) => {
|
|
1705
4410
|
if (name === "commands") registerCommand();
|
|
@@ -1737,10 +4442,10 @@ var AcpCompactionEngine = class extends CompactionEngine {
|
|
|
1737
4442
|
let done = false;
|
|
1738
4443
|
const registerSystemPrompt = () => {
|
|
1739
4444
|
if (done) return;
|
|
1740
|
-
const
|
|
1741
|
-
if (
|
|
4445
|
+
const registry3 = ctx.get("systemPrompt");
|
|
4446
|
+
if (registry3 === void 0) return;
|
|
1742
4447
|
done = true;
|
|
1743
|
-
|
|
4448
|
+
registry3.section({
|
|
1744
4449
|
name: "billion-context-dsh",
|
|
1745
4450
|
order: ACP_SYSTEM_PROMPT_ORDER,
|
|
1746
4451
|
text: renderSystemPrompt(this.prompts)
|