@raquezha/noheadroom 0.3.1 → 0.3.2
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/dist/bridge.js +21 -1
- package/dist/index.js +47 -29
- package/dist/types.d.ts +4 -0
- package/extensions/bridge.ts +22 -1
- package/extensions/index.ts +47 -31
- package/extensions/types.ts +9 -1
- package/package.json +1 -1
package/dist/bridge.js
CHANGED
|
@@ -32,6 +32,8 @@ export function applyCompressionResult(originalMessages, mappings, compressedMes
|
|
|
32
32
|
}
|
|
33
33
|
const nextMessages = structuredClone(originalMessages);
|
|
34
34
|
let appliedMessages = 0;
|
|
35
|
+
let appliedTokensBefore = 0;
|
|
36
|
+
let appliedTokensAfter = 0;
|
|
35
37
|
for (let index = 0; index < mappings.length; index++) {
|
|
36
38
|
const mapping = mappings[index];
|
|
37
39
|
const compressed = compressedMessages[index];
|
|
@@ -52,12 +54,26 @@ export function applyCompressionResult(originalMessages, mappings, compressedMes
|
|
|
52
54
|
if (!hasContent(target) || !replaceTextContent(target, nextText)) {
|
|
53
55
|
return { ok: false, reason: "target-content-unreplaceable" };
|
|
54
56
|
}
|
|
57
|
+
appliedTokensBefore += estimateTokens(mapping.originalText);
|
|
58
|
+
appliedTokensAfter += estimateTokens(nextText);
|
|
55
59
|
appliedMessages++;
|
|
56
60
|
}
|
|
57
61
|
if (appliedMessages === 0) {
|
|
58
62
|
return { ok: false, reason: "no-applicable-message-changed" };
|
|
59
63
|
}
|
|
60
|
-
|
|
64
|
+
const appliedTokensSaved = Math.max(0, appliedTokensBefore - appliedTokensAfter);
|
|
65
|
+
if (appliedTokensSaved === 0) {
|
|
66
|
+
return { ok: false, reason: "no-estimated-token-savings" };
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
ok: true,
|
|
70
|
+
messages: nextMessages,
|
|
71
|
+
appliedMessages,
|
|
72
|
+
appliedTokensBefore,
|
|
73
|
+
appliedTokensAfter,
|
|
74
|
+
appliedTokensSaved,
|
|
75
|
+
appliedCompressionRatio: appliedTokensAfter / appliedTokensBefore,
|
|
76
|
+
};
|
|
61
77
|
}
|
|
62
78
|
export function convertMessage(message) {
|
|
63
79
|
if (!message.role || !STANDARD_COMPRESSIBLE_ROLES.has(message.role))
|
|
@@ -221,6 +237,10 @@ function readStringProperty(value, key) {
|
|
|
221
237
|
function isRecord(value) {
|
|
222
238
|
return typeof value === "object" && value !== null;
|
|
223
239
|
}
|
|
240
|
+
function estimateTokens(text) {
|
|
241
|
+
// ponytail: cheap local estimate; exact tokenizer would add a heavy dependency for a footer number.
|
|
242
|
+
return text.length === 0 ? 0 : Math.max(1, Math.ceil(text.length / 4));
|
|
243
|
+
}
|
|
224
244
|
function naturalizeHeadroomMarkers(text) {
|
|
225
245
|
// Translate Headroom's "[X items compressed to Y. Retrieve more: hash=...]"
|
|
226
246
|
// into a Pi-native instruction to use offset/limit instead of hallucinating a retrieve command.
|
package/dist/index.js
CHANGED
|
@@ -207,10 +207,17 @@ function registerCompressionRenderer(pi) {
|
|
|
207
207
|
async function handleContextCompression(runtime, event, ctx) {
|
|
208
208
|
if (runtime.state.processing)
|
|
209
209
|
return undefined;
|
|
210
|
+
const bypassUncompressed = () => {
|
|
211
|
+
if (runtime.state.stats.last) {
|
|
212
|
+
runtime.state.stats.last = undefined;
|
|
213
|
+
runtime.refreshStatus(ctx);
|
|
214
|
+
}
|
|
215
|
+
return undefined;
|
|
216
|
+
};
|
|
210
217
|
// Throttle: max 1 compression attempt per 3 seconds to kill infinite loops
|
|
211
218
|
const now = Date.now();
|
|
212
219
|
if (now - runtime.state.lastCompressionTime < 3000) {
|
|
213
|
-
return
|
|
220
|
+
return bypassUncompressed();
|
|
214
221
|
}
|
|
215
222
|
// Content-based guards to prevent infinite recursion
|
|
216
223
|
const inputFingerprint = generateFingerprint(event.messages);
|
|
@@ -220,25 +227,26 @@ async function handleContextCompression(runtime, event, ctx) {
|
|
|
220
227
|
}
|
|
221
228
|
// 2. If this input matches our previous input, it didn't change. Stop.
|
|
222
229
|
if (runtime.state.lastInputFingerprint === inputFingerprint) {
|
|
223
|
-
return
|
|
230
|
+
return bypassUncompressed();
|
|
224
231
|
}
|
|
225
232
|
if (shouldSkipBeforePayload(runtime, ctx))
|
|
226
|
-
return
|
|
233
|
+
return bypassUncompressed();
|
|
227
234
|
const payload = buildCompressionPayload(event.messages, runtime.config.minMessageChars);
|
|
228
235
|
if (payload.candidateCount === 0)
|
|
229
|
-
return
|
|
236
|
+
return bypassUncompressed();
|
|
230
237
|
// 3. If eligible candidates haven't changed since the last skip/no-savings result,
|
|
231
238
|
// don't spend another proxy call just because surrounding conversation changed.
|
|
232
239
|
const candidateFingerprint = generateCandidateFingerprint(event.messages, payload);
|
|
233
240
|
if (runtime.state.lastGuardSkipCandidateFingerprint === candidateFingerprint) {
|
|
234
|
-
return
|
|
241
|
+
return bypassUncompressed();
|
|
235
242
|
}
|
|
236
243
|
if (allCandidateContentSeen(runtime.state, payload)) {
|
|
237
|
-
return
|
|
244
|
+
return bypassUncompressed();
|
|
238
245
|
}
|
|
246
|
+
ignoreSeenCandidateContent(runtime.state, payload);
|
|
239
247
|
if (runtime.state.proxyOnline !== true) {
|
|
240
248
|
void ensureProxyInBackground(runtime, ctx);
|
|
241
|
-
return
|
|
249
|
+
return bypassUncompressed();
|
|
242
250
|
}
|
|
243
251
|
runtime.state.processing = true;
|
|
244
252
|
runtime.state.lastCompressionTime = now;
|
|
@@ -251,10 +259,7 @@ async function handleContextCompression(runtime, event, ctx) {
|
|
|
251
259
|
// until the actual compressible candidate material changes.
|
|
252
260
|
runtime.state.lastInputFingerprint = inputFingerprint;
|
|
253
261
|
runtime.state.lastGuardSkipCandidateFingerprint = candidateFingerprint;
|
|
254
|
-
|
|
255
|
-
runtime.state.stats.last = undefined;
|
|
256
|
-
runtime.refreshStatus(ctx);
|
|
257
|
-
return undefined;
|
|
262
|
+
return bypassUncompressed();
|
|
258
263
|
}
|
|
259
264
|
const applied = applyCompressionResult(event.messages, payload.mappings, result.messages, {
|
|
260
265
|
minMessageChars: runtime.config.minMessageChars,
|
|
@@ -267,23 +272,29 @@ async function handleContextCompression(runtime, event, ctx) {
|
|
|
267
272
|
recordGuardSkip(runtime.state.stats, applied.reason);
|
|
268
273
|
emitNotraceTelemetry(runtime);
|
|
269
274
|
announceGuardSkip(ctx, applied.reason, result, runtime.config.mode);
|
|
270
|
-
|
|
271
|
-
return undefined;
|
|
275
|
+
return bypassUncompressed();
|
|
272
276
|
}
|
|
273
277
|
// Store fingerprints to break the feedback loop
|
|
274
278
|
runtime.state.lastInputFingerprint = inputFingerprint;
|
|
275
279
|
runtime.state.lastOutputFingerprint = generateFingerprint(applied.messages);
|
|
276
280
|
runtime.state.lastGuardSkipCandidateFingerprint = null;
|
|
277
|
-
recordSeenCandidateContent(runtime.state, payload,
|
|
278
|
-
|
|
281
|
+
recordSeenCandidateContent(runtime.state, payload, applied.messages);
|
|
282
|
+
const appliedResult = {
|
|
283
|
+
...result,
|
|
284
|
+
tokensBefore: applied.appliedTokensBefore,
|
|
285
|
+
tokensAfter: applied.appliedTokensAfter,
|
|
286
|
+
tokensSaved: applied.appliedTokensSaved,
|
|
287
|
+
compressionRatio: applied.appliedCompressionRatio,
|
|
288
|
+
};
|
|
289
|
+
recordAppliedCompression(runtime.state.stats, appliedResult, applied.appliedMessages);
|
|
279
290
|
emitNotraceTelemetry(runtime);
|
|
280
|
-
announceAppliedCompression(runtime.pi, ctx,
|
|
291
|
+
announceAppliedCompression(runtime.pi, ctx, appliedResult, applied.appliedMessages, runtime.config.mode);
|
|
281
292
|
runtime.refreshStatus(ctx);
|
|
282
293
|
return { messages: applied.messages };
|
|
283
294
|
}
|
|
284
295
|
catch (error) {
|
|
285
296
|
recordCompressionError(runtime, ctx, error);
|
|
286
|
-
return
|
|
297
|
+
return bypassUncompressed();
|
|
287
298
|
}
|
|
288
299
|
finally {
|
|
289
300
|
runtime.state.processing = false;
|
|
@@ -296,16 +307,25 @@ function allCandidateContentSeen(state, payload) {
|
|
|
296
307
|
const hashes = payload.mappings.filter((mapping) => mapping.applyTo).map((mapping) => stableHash(mapping.originalText));
|
|
297
308
|
return hashes.length > 0 && hashes.every((hash) => seen.has(hash));
|
|
298
309
|
}
|
|
299
|
-
function
|
|
310
|
+
function ignoreSeenCandidateContent(state, payload) {
|
|
311
|
+
const seen = state.seenCandidateContentFingerprints;
|
|
312
|
+
if (!seen?.size)
|
|
313
|
+
return;
|
|
314
|
+
for (const mapping of payload.mappings) {
|
|
315
|
+
if (mapping.applyTo && seen.has(stableHash(mapping.originalText)))
|
|
316
|
+
mapping.applyTo = null;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
function recordSeenCandidateContent(state, payload, appliedMessages) {
|
|
300
320
|
state.seenCandidateContentFingerprints ??= new Set();
|
|
301
321
|
state.seenCandidateContentOrder ??= [];
|
|
302
|
-
payload.mappings.forEach((mapping
|
|
322
|
+
payload.mappings.forEach((mapping) => {
|
|
303
323
|
if (!mapping.applyTo)
|
|
304
324
|
return;
|
|
305
325
|
addSeenCandidateHash(state, stableHash(mapping.originalText));
|
|
306
|
-
const
|
|
307
|
-
if (
|
|
308
|
-
addSeenCandidateHash(state, stableHash(extractOpenAIText(
|
|
326
|
+
const applied = convertMessage(appliedMessages[mapping.sourceIndex]);
|
|
327
|
+
if (applied)
|
|
328
|
+
addSeenCandidateHash(state, stableHash(extractOpenAIText(applied)));
|
|
309
329
|
});
|
|
310
330
|
}
|
|
311
331
|
function addSeenCandidateHash(state, hash) {
|
|
@@ -391,7 +411,7 @@ function recordAppliedCompression(stats, result, appliedMessages) {
|
|
|
391
411
|
function summarizeTelemetry(state) {
|
|
392
412
|
if (state.stats.last) {
|
|
393
413
|
const pct = Math.round((1 - state.stats.last.compressionRatio) * 100);
|
|
394
|
-
return `
|
|
414
|
+
return `last compression estimated about ${state.stats.last.tokensSaved.toLocaleString()} tokens saved (-${pct}%) across ${state.stats.last.appliedMessages} tool results; local session estimate about ${state.stats.tokensSaved.toLocaleString()} tokens saved`;
|
|
395
415
|
}
|
|
396
416
|
if (!state.enabled)
|
|
397
417
|
return "headroom loaded but disabled for this session";
|
|
@@ -443,7 +463,7 @@ function announceAppliedCompression(pi, ctx, result, appliedMessages, mode = "no
|
|
|
443
463
|
if (mode === "quiet" || mode === "silent")
|
|
444
464
|
return;
|
|
445
465
|
const pct = Math.round((1 - result.compressionRatio) * 100);
|
|
446
|
-
const summary = `compressed ${
|
|
466
|
+
const summary = `compressed ${appliedMessages} tool results; estimated ~${result.tokensSaved.toLocaleString()} tokens saved (-${pct}%)`;
|
|
447
467
|
const line = `noheadroom: ${summary}`;
|
|
448
468
|
ctx.ui.notify(line, "info");
|
|
449
469
|
// Note: we no longer use pi.appendEntry or pi.sendMessage here because
|
|
@@ -561,10 +581,7 @@ function renderFooterStatus(ctx, config, state) {
|
|
|
561
581
|
return paint("dim", "○ Headroom not running");
|
|
562
582
|
if (state.proxyOnline === null && !state.stats.last)
|
|
563
583
|
return paint("dim", "○ Headroom idle");
|
|
564
|
-
|
|
565
|
-
return paint("success", "✓") + paint("dim", " Headroom");
|
|
566
|
-
const pct = Math.round((1 - state.stats.last.compressionRatio) * 100);
|
|
567
|
-
return (paint("success", "✓") + paint("dim", ` Headroom -${pct}% (${state.stats.last.tokensSaved.toLocaleString()} saved)`));
|
|
584
|
+
return paint("success", "✓") + paint("dim", " Headroom active");
|
|
568
585
|
}
|
|
569
586
|
async function showProxyStats(ctx, client, config) {
|
|
570
587
|
if (isRemoteBlocked(config)) {
|
|
@@ -596,7 +613,8 @@ function renderStatus(config, state) {
|
|
|
596
613
|
` Attempts: ${stats.attempts}`,
|
|
597
614
|
` Applied: ${stats.applied}`,
|
|
598
615
|
` Guard skips: ${stats.guardSkips}`,
|
|
599
|
-
`
|
|
616
|
+
` Estimated tokens saved: ~${stats.tokensSaved.toLocaleString()}`,
|
|
617
|
+
` Note: local estimate; proxy /stats does not track Pi extension /v1/compress calls.`,
|
|
600
618
|
];
|
|
601
619
|
if (stats.last) {
|
|
602
620
|
const pct = Math.round((1 - stats.last.compressionRatio) * 100);
|
package/dist/types.d.ts
CHANGED
|
@@ -96,6 +96,10 @@ export type ApplyCompressionResult = {
|
|
|
96
96
|
ok: true;
|
|
97
97
|
messages: AgentMessage[];
|
|
98
98
|
appliedMessages: number;
|
|
99
|
+
appliedTokensBefore: number;
|
|
100
|
+
appliedTokensAfter: number;
|
|
101
|
+
appliedTokensSaved: number;
|
|
102
|
+
appliedCompressionRatio: number;
|
|
99
103
|
} | {
|
|
100
104
|
ok: false;
|
|
101
105
|
reason: string;
|
package/extensions/bridge.ts
CHANGED
|
@@ -61,6 +61,8 @@ export function applyCompressionResult(
|
|
|
61
61
|
|
|
62
62
|
const nextMessages = structuredClone(originalMessages) as AgentMessage[];
|
|
63
63
|
let appliedMessages = 0;
|
|
64
|
+
let appliedTokensBefore = 0;
|
|
65
|
+
let appliedTokensAfter = 0;
|
|
64
66
|
|
|
65
67
|
for (let index = 0; index < mappings.length; index++) {
|
|
66
68
|
const mapping = mappings[index];
|
|
@@ -84,14 +86,28 @@ export function applyCompressionResult(
|
|
|
84
86
|
if (!hasContent(target) || !replaceTextContent(target, nextText)) {
|
|
85
87
|
return { ok: false, reason: "target-content-unreplaceable" };
|
|
86
88
|
}
|
|
89
|
+
appliedTokensBefore += estimateTokens(mapping.originalText);
|
|
90
|
+
appliedTokensAfter += estimateTokens(nextText);
|
|
87
91
|
appliedMessages++;
|
|
88
92
|
}
|
|
89
93
|
|
|
90
94
|
if (appliedMessages === 0) {
|
|
91
95
|
return { ok: false, reason: "no-applicable-message-changed" };
|
|
92
96
|
}
|
|
97
|
+
const appliedTokensSaved = Math.max(0, appliedTokensBefore - appliedTokensAfter);
|
|
98
|
+
if (appliedTokensSaved === 0) {
|
|
99
|
+
return { ok: false, reason: "no-estimated-token-savings" };
|
|
100
|
+
}
|
|
93
101
|
|
|
94
|
-
return {
|
|
102
|
+
return {
|
|
103
|
+
ok: true,
|
|
104
|
+
messages: nextMessages,
|
|
105
|
+
appliedMessages,
|
|
106
|
+
appliedTokensBefore,
|
|
107
|
+
appliedTokensAfter,
|
|
108
|
+
appliedTokensSaved,
|
|
109
|
+
appliedCompressionRatio: appliedTokensAfter / appliedTokensBefore,
|
|
110
|
+
};
|
|
95
111
|
}
|
|
96
112
|
|
|
97
113
|
export function convertMessage(message: AnyMessage): OpenAIMessage | undefined {
|
|
@@ -267,6 +283,11 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
267
283
|
return typeof value === "object" && value !== null;
|
|
268
284
|
}
|
|
269
285
|
|
|
286
|
+
function estimateTokens(text: string): number {
|
|
287
|
+
// ponytail: cheap local estimate; exact tokenizer would add a heavy dependency for a footer number.
|
|
288
|
+
return text.length === 0 ? 0 : Math.max(1, Math.ceil(text.length / 4));
|
|
289
|
+
}
|
|
290
|
+
|
|
270
291
|
function naturalizeHeadroomMarkers(text: string): string {
|
|
271
292
|
// Translate Headroom's "[X items compressed to Y. Retrieve more: hash=...]"
|
|
272
293
|
// into a Pi-native instruction to use offset/limit instead of hallucinating a retrieve command.
|
package/extensions/index.ts
CHANGED
|
@@ -270,10 +270,18 @@ async function handleContextCompression(
|
|
|
270
270
|
): Promise<{ messages?: AgentMessage[] } | undefined> {
|
|
271
271
|
if (runtime.state.processing) return undefined;
|
|
272
272
|
|
|
273
|
+
const bypassUncompressed = () => {
|
|
274
|
+
if (runtime.state.stats.last) {
|
|
275
|
+
runtime.state.stats.last = undefined;
|
|
276
|
+
runtime.refreshStatus(ctx);
|
|
277
|
+
}
|
|
278
|
+
return undefined;
|
|
279
|
+
};
|
|
280
|
+
|
|
273
281
|
// Throttle: max 1 compression attempt per 3 seconds to kill infinite loops
|
|
274
282
|
const now = Date.now();
|
|
275
283
|
if (now - runtime.state.lastCompressionTime < 3000) {
|
|
276
|
-
return
|
|
284
|
+
return bypassUncompressed();
|
|
277
285
|
}
|
|
278
286
|
|
|
279
287
|
// Content-based guards to prevent infinite recursion
|
|
@@ -286,25 +294,26 @@ async function handleContextCompression(
|
|
|
286
294
|
|
|
287
295
|
// 2. If this input matches our previous input, it didn't change. Stop.
|
|
288
296
|
if (runtime.state.lastInputFingerprint === inputFingerprint) {
|
|
289
|
-
return
|
|
297
|
+
return bypassUncompressed();
|
|
290
298
|
}
|
|
291
299
|
|
|
292
|
-
if (shouldSkipBeforePayload(runtime, ctx)) return
|
|
300
|
+
if (shouldSkipBeforePayload(runtime, ctx)) return bypassUncompressed();
|
|
293
301
|
const payload = buildCompressionPayload(event.messages, runtime.config.minMessageChars);
|
|
294
|
-
if (payload.candidateCount === 0) return
|
|
302
|
+
if (payload.candidateCount === 0) return bypassUncompressed();
|
|
295
303
|
|
|
296
304
|
// 3. If eligible candidates haven't changed since the last skip/no-savings result,
|
|
297
305
|
// don't spend another proxy call just because surrounding conversation changed.
|
|
298
306
|
const candidateFingerprint = generateCandidateFingerprint(event.messages, payload);
|
|
299
307
|
if (runtime.state.lastGuardSkipCandidateFingerprint === candidateFingerprint) {
|
|
300
|
-
return
|
|
308
|
+
return bypassUncompressed();
|
|
301
309
|
}
|
|
302
310
|
if (allCandidateContentSeen(runtime.state, payload)) {
|
|
303
|
-
return
|
|
311
|
+
return bypassUncompressed();
|
|
304
312
|
}
|
|
313
|
+
ignoreSeenCandidateContent(runtime.state, payload);
|
|
305
314
|
if (runtime.state.proxyOnline !== true) {
|
|
306
315
|
void ensureProxyInBackground(runtime, ctx);
|
|
307
|
-
return
|
|
316
|
+
return bypassUncompressed();
|
|
308
317
|
}
|
|
309
318
|
|
|
310
319
|
runtime.state.processing = true;
|
|
@@ -318,10 +327,7 @@ async function handleContextCompression(
|
|
|
318
327
|
// until the actual compressible candidate material changes.
|
|
319
328
|
runtime.state.lastInputFingerprint = inputFingerprint;
|
|
320
329
|
runtime.state.lastGuardSkipCandidateFingerprint = candidateFingerprint;
|
|
321
|
-
|
|
322
|
-
runtime.state.stats.last = undefined;
|
|
323
|
-
runtime.refreshStatus(ctx);
|
|
324
|
-
return undefined;
|
|
330
|
+
return bypassUncompressed();
|
|
325
331
|
}
|
|
326
332
|
|
|
327
333
|
const applied = applyCompressionResult(event.messages, payload.mappings, result.messages, {
|
|
@@ -336,24 +342,30 @@ async function handleContextCompression(
|
|
|
336
342
|
recordGuardSkip(runtime.state.stats, applied.reason);
|
|
337
343
|
emitNotraceTelemetry(runtime);
|
|
338
344
|
announceGuardSkip(ctx, applied.reason, result, runtime.config.mode);
|
|
339
|
-
|
|
340
|
-
return undefined;
|
|
345
|
+
return bypassUncompressed();
|
|
341
346
|
}
|
|
342
347
|
|
|
343
348
|
// Store fingerprints to break the feedback loop
|
|
344
349
|
runtime.state.lastInputFingerprint = inputFingerprint;
|
|
345
350
|
runtime.state.lastOutputFingerprint = generateFingerprint(applied.messages);
|
|
346
351
|
runtime.state.lastGuardSkipCandidateFingerprint = null;
|
|
347
|
-
recordSeenCandidateContent(runtime.state, payload,
|
|
348
|
-
|
|
349
|
-
|
|
352
|
+
recordSeenCandidateContent(runtime.state, payload, applied.messages);
|
|
353
|
+
const appliedResult = {
|
|
354
|
+
...result,
|
|
355
|
+
tokensBefore: applied.appliedTokensBefore,
|
|
356
|
+
tokensAfter: applied.appliedTokensAfter,
|
|
357
|
+
tokensSaved: applied.appliedTokensSaved,
|
|
358
|
+
compressionRatio: applied.appliedCompressionRatio,
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
recordAppliedCompression(runtime.state.stats, appliedResult, applied.appliedMessages);
|
|
350
362
|
emitNotraceTelemetry(runtime);
|
|
351
|
-
announceAppliedCompression(runtime.pi, ctx,
|
|
363
|
+
announceAppliedCompression(runtime.pi, ctx, appliedResult, applied.appliedMessages, runtime.config.mode);
|
|
352
364
|
runtime.refreshStatus(ctx);
|
|
353
365
|
return { messages: applied.messages };
|
|
354
366
|
} catch (error) {
|
|
355
367
|
recordCompressionError(runtime, ctx, error);
|
|
356
|
-
return
|
|
368
|
+
return bypassUncompressed();
|
|
357
369
|
} finally {
|
|
358
370
|
runtime.state.processing = false;
|
|
359
371
|
}
|
|
@@ -366,18 +378,26 @@ function allCandidateContentSeen(state: HeadroomRuntimeState, payload: Compressi
|
|
|
366
378
|
return hashes.length > 0 && hashes.every((hash) => seen.has(hash));
|
|
367
379
|
}
|
|
368
380
|
|
|
381
|
+
function ignoreSeenCandidateContent(state: HeadroomRuntimeState, payload: CompressionPayload): void {
|
|
382
|
+
const seen = state.seenCandidateContentFingerprints;
|
|
383
|
+
if (!seen?.size) return;
|
|
384
|
+
for (const mapping of payload.mappings) {
|
|
385
|
+
if (mapping.applyTo && seen.has(stableHash(mapping.originalText))) mapping.applyTo = null;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
369
389
|
function recordSeenCandidateContent(
|
|
370
390
|
state: HeadroomRuntimeState,
|
|
371
391
|
payload: CompressionPayload,
|
|
372
|
-
|
|
392
|
+
appliedMessages: AgentMessage[],
|
|
373
393
|
): void {
|
|
374
394
|
state.seenCandidateContentFingerprints ??= new Set();
|
|
375
395
|
state.seenCandidateContentOrder ??= [];
|
|
376
|
-
payload.mappings.forEach((mapping
|
|
396
|
+
payload.mappings.forEach((mapping) => {
|
|
377
397
|
if (!mapping.applyTo) return;
|
|
378
398
|
addSeenCandidateHash(state, stableHash(mapping.originalText));
|
|
379
|
-
const
|
|
380
|
-
if (
|
|
399
|
+
const applied = convertMessage(appliedMessages[mapping.sourceIndex]);
|
|
400
|
+
if (applied) addSeenCandidateHash(state, stableHash(extractOpenAIText(applied)));
|
|
381
401
|
});
|
|
382
402
|
}
|
|
383
403
|
|
|
@@ -472,7 +492,7 @@ function recordAppliedCompression(stats: HeadroomStats, result: CompressResult,
|
|
|
472
492
|
function summarizeTelemetry(state: HeadroomRuntimeState): string | null {
|
|
473
493
|
if (state.stats.last) {
|
|
474
494
|
const pct = Math.round((1 - state.stats.last.compressionRatio) * 100);
|
|
475
|
-
return `
|
|
495
|
+
return `last compression estimated about ${state.stats.last.tokensSaved.toLocaleString()} tokens saved (-${pct}%) across ${state.stats.last.appliedMessages} tool results; local session estimate about ${state.stats.tokensSaved.toLocaleString()} tokens saved`;
|
|
476
496
|
}
|
|
477
497
|
if (!state.enabled) return "headroom loaded but disabled for this session";
|
|
478
498
|
if (state.stats.lastSkipReason) return `compression not applied; last guard skip: ${state.stats.lastSkipReason}`;
|
|
@@ -524,7 +544,7 @@ function announceAppliedCompression(
|
|
|
524
544
|
): void {
|
|
525
545
|
if (mode === "quiet" || mode === "silent") return;
|
|
526
546
|
const pct = Math.round((1 - result.compressionRatio) * 100);
|
|
527
|
-
const summary = `compressed ${
|
|
547
|
+
const summary = `compressed ${appliedMessages} tool results; estimated ~${result.tokensSaved.toLocaleString()} tokens saved (-${pct}%)`;
|
|
528
548
|
const line = `noheadroom: ${summary}`;
|
|
529
549
|
ctx.ui.notify(line, "info");
|
|
530
550
|
// Note: we no longer use pi.appendEntry or pi.sendMessage here because
|
|
@@ -661,12 +681,7 @@ function renderFooterStatus(ctx: ExtensionContext, config: HeadroomConfig, state
|
|
|
661
681
|
if (state.proxyStarting) return paint("dim", "⏳ Headroom starting");
|
|
662
682
|
if (state.proxyOnline === false) return paint("dim", "○ Headroom not running");
|
|
663
683
|
if (state.proxyOnline === null && !state.stats.last) return paint("dim", "○ Headroom idle");
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
const pct = Math.round((1 - state.stats.last.compressionRatio) * 100);
|
|
667
|
-
return (
|
|
668
|
-
paint("success", "✓") + paint("dim", ` Headroom -${pct}% (${state.stats.last.tokensSaved.toLocaleString()} saved)`)
|
|
669
|
-
);
|
|
684
|
+
return paint("success", "✓") + paint("dim", " Headroom active");
|
|
670
685
|
}
|
|
671
686
|
|
|
672
687
|
async function showProxyStats(
|
|
@@ -706,7 +721,8 @@ function renderStatus(config: HeadroomConfig, state: HeadroomRuntimeState): stri
|
|
|
706
721
|
` Attempts: ${stats.attempts}`,
|
|
707
722
|
` Applied: ${stats.applied}`,
|
|
708
723
|
` Guard skips: ${stats.guardSkips}`,
|
|
709
|
-
`
|
|
724
|
+
` Estimated tokens saved: ~${stats.tokensSaved.toLocaleString()}`,
|
|
725
|
+
` Note: local estimate; proxy /stats does not track Pi extension /v1/compress calls.`,
|
|
710
726
|
];
|
|
711
727
|
if (stats.last) {
|
|
712
728
|
const pct = Math.round((1 - stats.last.compressionRatio) * 100);
|
package/extensions/types.ts
CHANGED
|
@@ -108,5 +108,13 @@ export interface ApplyCompressionOptions {
|
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
export type ApplyCompressionResult =
|
|
111
|
-
| {
|
|
111
|
+
| {
|
|
112
|
+
ok: true;
|
|
113
|
+
messages: AgentMessage[];
|
|
114
|
+
appliedMessages: number;
|
|
115
|
+
appliedTokensBefore: number;
|
|
116
|
+
appliedTokensAfter: number;
|
|
117
|
+
appliedTokensSaved: number;
|
|
118
|
+
appliedCompressionRatio: number;
|
|
119
|
+
}
|
|
112
120
|
| { ok: false; reason: string };
|
package/package.json
CHANGED