@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 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
- return { ok: true, messages: nextMessages, appliedMessages };
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 undefined;
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 undefined;
230
+ return bypassUncompressed();
224
231
  }
225
232
  if (shouldSkipBeforePayload(runtime, ctx))
226
- return undefined;
233
+ return bypassUncompressed();
227
234
  const payload = buildCompressionPayload(event.messages, runtime.config.minMessageChars);
228
235
  if (payload.candidateCount === 0)
229
- return undefined;
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 undefined;
241
+ return bypassUncompressed();
235
242
  }
236
243
  if (allCandidateContentSeen(runtime.state, payload)) {
237
- return undefined;
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 undefined;
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
- // ponytail: clear stats.last so renderFooterStatus shows idle instead of stale savings
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
- runtime.refreshStatus(ctx);
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, result.messages);
278
- recordAppliedCompression(runtime.state.stats, result, applied.appliedMessages);
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, result, applied.appliedMessages, runtime.config.mode);
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 undefined;
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 recordSeenCandidateContent(state, payload, compressedMessages) {
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, index) => {
322
+ payload.mappings.forEach((mapping) => {
303
323
  if (!mapping.applyTo)
304
324
  return;
305
325
  addSeenCandidateHash(state, stableHash(mapping.originalText));
306
- const compressed = compressedMessages[index];
307
- if (compressed)
308
- addSeenCandidateHash(state, stableHash(extractOpenAIText(compressed)));
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 `compressed ${state.stats.last.tokensBefore.toLocaleString()} to ${state.stats.last.tokensAfter.toLocaleString()} tokens; saved ${state.stats.last.tokensSaved.toLocaleString()} tokens across ${state.stats.last.appliedMessages} tool results (-${pct}%)`;
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 ${result.tokensBefore.toLocaleString()} → ${result.tokensAfter.toLocaleString()} tokens (-${pct}%, saved ${result.tokensSaved.toLocaleString()}, messages ${appliedMessages})`;
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
- if (!state.stats.last)
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
- ` Tokens saved: ${stats.tokensSaved.toLocaleString()}`,
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;
@@ -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 { ok: true, messages: nextMessages, appliedMessages };
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.
@@ -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 undefined;
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 undefined;
297
+ return bypassUncompressed();
290
298
  }
291
299
 
292
- if (shouldSkipBeforePayload(runtime, ctx)) return undefined;
300
+ if (shouldSkipBeforePayload(runtime, ctx)) return bypassUncompressed();
293
301
  const payload = buildCompressionPayload(event.messages, runtime.config.minMessageChars);
294
- if (payload.candidateCount === 0) return undefined;
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 undefined;
308
+ return bypassUncompressed();
301
309
  }
302
310
  if (allCandidateContentSeen(runtime.state, payload)) {
303
- return undefined;
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 undefined;
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
- // ponytail: clear stats.last so renderFooterStatus shows idle instead of stale savings
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
- runtime.refreshStatus(ctx);
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, result.messages);
348
-
349
- recordAppliedCompression(runtime.state.stats, result, applied.appliedMessages);
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, result, applied.appliedMessages, runtime.config.mode);
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 undefined;
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
- compressedMessages: CompressionPayload["messages"],
392
+ appliedMessages: AgentMessage[],
373
393
  ): void {
374
394
  state.seenCandidateContentFingerprints ??= new Set();
375
395
  state.seenCandidateContentOrder ??= [];
376
- payload.mappings.forEach((mapping, index) => {
396
+ payload.mappings.forEach((mapping) => {
377
397
  if (!mapping.applyTo) return;
378
398
  addSeenCandidateHash(state, stableHash(mapping.originalText));
379
- const compressed = compressedMessages[index];
380
- if (compressed) addSeenCandidateHash(state, stableHash(extractOpenAIText(compressed)));
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 `compressed ${state.stats.last.tokensBefore.toLocaleString()} to ${state.stats.last.tokensAfter.toLocaleString()} tokens; saved ${state.stats.last.tokensSaved.toLocaleString()} tokens across ${state.stats.last.appliedMessages} tool results (-${pct}%)`;
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 ${result.tokensBefore.toLocaleString()} → ${result.tokensAfter.toLocaleString()} tokens (-${pct}%, saved ${result.tokensSaved.toLocaleString()}, messages ${appliedMessages})`;
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
- if (!state.stats.last) return paint("success", "✓") + paint("dim", " Headroom");
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
- ` Tokens saved: ${stats.tokensSaved.toLocaleString()}`,
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);
@@ -108,5 +108,13 @@ export interface ApplyCompressionOptions {
108
108
  }
109
109
 
110
110
  export type ApplyCompressionResult =
111
- | { ok: true; messages: AgentMessage[]; appliedMessages: number }
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raquezha/noheadroom",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "Personal Headroom compression bridge for Pi with Docker-first defaults and Pi tool-name adaptation",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",