@raquezha/noheadroom 0.3.0 → 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.d.ts CHANGED
@@ -12,6 +12,8 @@ interface HeadroomRuntimeState {
12
12
  lastInputFingerprint: string | null;
13
13
  lastOutputFingerprint: string | null;
14
14
  lastGuardSkipCandidateFingerprint: string | null;
15
+ seenCandidateContentFingerprints?: Set<string>;
16
+ seenCandidateContentOrder?: string[];
15
17
  lastCompressionTime: number;
16
18
  stats: HeadroomStats;
17
19
  }
package/dist/index.js CHANGED
@@ -8,6 +8,10 @@ const STATUS_KEY = "headroom";
8
8
  const SUBCOMMANDS = ["status", "on", "off", "health", "stats", "mode"];
9
9
  const MODES = ["normal", "quiet", "silent"];
10
10
  const NOTRACE_TELEMETRY_CHANNEL = "notrace.telemetry.extension";
11
+ // Bounded FIFO, not config: this is a safety valve for duplicate-compression loops.
12
+ // 512 hashes covers roughly 256 compressed candidates because we store original + compressed text hashes.
13
+ // That is far beyond the reread-loop case while keeping memory bounded for long sessions.
14
+ const MAX_SEEN_CANDIDATE_CONTENT_FINGERPRINTS = 512;
11
15
  const COMPRESSION_ENTRY_TYPE = "noheadroom.compression";
12
16
  export default function headroomExtension(pi) {
13
17
  registerCompressionRenderer(pi);
@@ -73,6 +77,8 @@ function createRuntime(pi) {
73
77
  lastInputFingerprint: null,
74
78
  lastOutputFingerprint: null,
75
79
  lastGuardSkipCandidateFingerprint: null,
80
+ seenCandidateContentFingerprints: new Set(),
81
+ seenCandidateContentOrder: [],
76
82
  lastCompressionTime: 0,
77
83
  stats: { attempts: 0, applied: 0, guardSkips: 0, tokensSaved: 0 },
78
84
  };
@@ -201,10 +207,17 @@ function registerCompressionRenderer(pi) {
201
207
  async function handleContextCompression(runtime, event, ctx) {
202
208
  if (runtime.state.processing)
203
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
+ };
204
217
  // Throttle: max 1 compression attempt per 3 seconds to kill infinite loops
205
218
  const now = Date.now();
206
219
  if (now - runtime.state.lastCompressionTime < 3000) {
207
- return undefined;
220
+ return bypassUncompressed();
208
221
  }
209
222
  // Content-based guards to prevent infinite recursion
210
223
  const inputFingerprint = generateFingerprint(event.messages);
@@ -214,22 +227,26 @@ async function handleContextCompression(runtime, event, ctx) {
214
227
  }
215
228
  // 2. If this input matches our previous input, it didn't change. Stop.
216
229
  if (runtime.state.lastInputFingerprint === inputFingerprint) {
217
- return undefined;
230
+ return bypassUncompressed();
218
231
  }
219
232
  if (shouldSkipBeforePayload(runtime, ctx))
220
- return undefined;
233
+ return bypassUncompressed();
221
234
  const payload = buildCompressionPayload(event.messages, runtime.config.minMessageChars);
222
235
  if (payload.candidateCount === 0)
223
- return undefined;
236
+ return bypassUncompressed();
224
237
  // 3. If eligible candidates haven't changed since the last skip/no-savings result,
225
238
  // don't spend another proxy call just because surrounding conversation changed.
226
239
  const candidateFingerprint = generateCandidateFingerprint(event.messages, payload);
227
240
  if (runtime.state.lastGuardSkipCandidateFingerprint === candidateFingerprint) {
228
- return undefined;
241
+ return bypassUncompressed();
242
+ }
243
+ if (allCandidateContentSeen(runtime.state, payload)) {
244
+ return bypassUncompressed();
229
245
  }
246
+ ignoreSeenCandidateContent(runtime.state, payload);
230
247
  if (runtime.state.proxyOnline !== true) {
231
248
  void ensureProxyInBackground(runtime, ctx);
232
- return undefined;
249
+ return bypassUncompressed();
233
250
  }
234
251
  runtime.state.processing = true;
235
252
  runtime.state.lastCompressionTime = now;
@@ -242,10 +259,7 @@ async function handleContextCompression(runtime, event, ctx) {
242
259
  // until the actual compressible candidate material changes.
243
260
  runtime.state.lastInputFingerprint = inputFingerprint;
244
261
  runtime.state.lastGuardSkipCandidateFingerprint = candidateFingerprint;
245
- // ponytail: clear stats.last so renderFooterStatus shows idle instead of stale savings
246
- runtime.state.stats.last = undefined;
247
- runtime.refreshStatus(ctx);
248
- return undefined;
262
+ return bypassUncompressed();
249
263
  }
250
264
  const applied = applyCompressionResult(event.messages, payload.mappings, result.messages, {
251
265
  minMessageChars: runtime.config.minMessageChars,
@@ -258,27 +272,75 @@ async function handleContextCompression(runtime, event, ctx) {
258
272
  recordGuardSkip(runtime.state.stats, applied.reason);
259
273
  emitNotraceTelemetry(runtime);
260
274
  announceGuardSkip(ctx, applied.reason, result, runtime.config.mode);
261
- runtime.refreshStatus(ctx);
262
- return undefined;
275
+ return bypassUncompressed();
263
276
  }
264
277
  // Store fingerprints to break the feedback loop
265
278
  runtime.state.lastInputFingerprint = inputFingerprint;
266
279
  runtime.state.lastOutputFingerprint = generateFingerprint(applied.messages);
267
280
  runtime.state.lastGuardSkipCandidateFingerprint = null;
268
- 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);
269
290
  emitNotraceTelemetry(runtime);
270
- announceAppliedCompression(runtime.pi, ctx, result, applied.appliedMessages, runtime.config.mode);
291
+ announceAppliedCompression(runtime.pi, ctx, appliedResult, applied.appliedMessages, runtime.config.mode);
271
292
  runtime.refreshStatus(ctx);
272
293
  return { messages: applied.messages };
273
294
  }
274
295
  catch (error) {
275
296
  recordCompressionError(runtime, ctx, error);
276
- return undefined;
297
+ return bypassUncompressed();
277
298
  }
278
299
  finally {
279
300
  runtime.state.processing = false;
280
301
  }
281
302
  }
303
+ function allCandidateContentSeen(state, payload) {
304
+ const seen = state.seenCandidateContentFingerprints;
305
+ if (!seen?.size)
306
+ return false;
307
+ const hashes = payload.mappings.filter((mapping) => mapping.applyTo).map((mapping) => stableHash(mapping.originalText));
308
+ return hashes.length > 0 && hashes.every((hash) => seen.has(hash));
309
+ }
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) {
320
+ state.seenCandidateContentFingerprints ??= new Set();
321
+ state.seenCandidateContentOrder ??= [];
322
+ payload.mappings.forEach((mapping) => {
323
+ if (!mapping.applyTo)
324
+ return;
325
+ addSeenCandidateHash(state, stableHash(mapping.originalText));
326
+ const applied = convertMessage(appliedMessages[mapping.sourceIndex]);
327
+ if (applied)
328
+ addSeenCandidateHash(state, stableHash(extractOpenAIText(applied)));
329
+ });
330
+ }
331
+ function addSeenCandidateHash(state, hash) {
332
+ state.seenCandidateContentFingerprints ??= new Set();
333
+ state.seenCandidateContentOrder ??= [];
334
+ if (state.seenCandidateContentFingerprints.has(hash))
335
+ return;
336
+ state.seenCandidateContentFingerprints.add(hash);
337
+ state.seenCandidateContentOrder.push(hash);
338
+ while (state.seenCandidateContentOrder.length > MAX_SEEN_CANDIDATE_CONTENT_FINGERPRINTS) {
339
+ const oldest = state.seenCandidateContentOrder.shift();
340
+ if (oldest)
341
+ state.seenCandidateContentFingerprints.delete(oldest);
342
+ }
343
+ }
282
344
  function generateCandidateFingerprint(messages, payload) {
283
345
  const units = payload.mappings
284
346
  .filter((mapping) => mapping.applyTo)
@@ -349,7 +411,7 @@ function recordAppliedCompression(stats, result, appliedMessages) {
349
411
  function summarizeTelemetry(state) {
350
412
  if (state.stats.last) {
351
413
  const pct = Math.round((1 - state.stats.last.compressionRatio) * 100);
352
- 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`;
353
415
  }
354
416
  if (!state.enabled)
355
417
  return "headroom loaded but disabled for this session";
@@ -401,7 +463,7 @@ function announceAppliedCompression(pi, ctx, result, appliedMessages, mode = "no
401
463
  if (mode === "quiet" || mode === "silent")
402
464
  return;
403
465
  const pct = Math.round((1 - result.compressionRatio) * 100);
404
- 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}%)`;
405
467
  const line = `noheadroom: ${summary}`;
406
468
  ctx.ui.notify(line, "info");
407
469
  // Note: we no longer use pi.appendEntry or pi.sendMessage here because
@@ -519,10 +581,7 @@ function renderFooterStatus(ctx, config, state) {
519
581
  return paint("dim", "○ Headroom not running");
520
582
  if (state.proxyOnline === null && !state.stats.last)
521
583
  return paint("dim", "○ Headroom idle");
522
- if (!state.stats.last)
523
- return paint("success", "✓") + paint("dim", " Headroom");
524
- const pct = Math.round((1 - state.stats.last.compressionRatio) * 100);
525
- return (paint("success", "✓") + paint("dim", ` Headroom -${pct}% (${state.stats.last.tokensSaved.toLocaleString()} saved)`));
584
+ return paint("success", "✓") + paint("dim", " Headroom active");
526
585
  }
527
586
  async function showProxyStats(ctx, client, config) {
528
587
  if (isRemoteBlocked(config)) {
@@ -554,7 +613,8 @@ function renderStatus(config, state) {
554
613
  ` Attempts: ${stats.attempts}`,
555
614
  ` Applied: ${stats.applied}`,
556
615
  ` Guard skips: ${stats.guardSkips}`,
557
- ` 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.`,
558
618
  ];
559
619
  if (stats.last) {
560
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.
@@ -16,6 +16,10 @@ const STATUS_KEY = "headroom";
16
16
  const SUBCOMMANDS = ["status", "on", "off", "health", "stats", "mode"] as const;
17
17
  const MODES = ["normal", "quiet", "silent"] as const;
18
18
  const NOTRACE_TELEMETRY_CHANNEL = "notrace.telemetry.extension";
19
+ // Bounded FIFO, not config: this is a safety valve for duplicate-compression loops.
20
+ // 512 hashes covers roughly 256 compressed candidates because we store original + compressed text hashes.
21
+ // That is far beyond the reread-loop case while keeping memory bounded for long sessions.
22
+ const MAX_SEEN_CANDIDATE_CONTENT_FINGERPRINTS = 512;
19
23
 
20
24
  type Subcommand = (typeof SUBCOMMANDS)[number];
21
25
 
@@ -30,6 +34,8 @@ interface HeadroomRuntimeState {
30
34
  lastInputFingerprint: string | null;
31
35
  lastOutputFingerprint: string | null;
32
36
  lastGuardSkipCandidateFingerprint: string | null;
37
+ seenCandidateContentFingerprints?: Set<string>;
38
+ seenCandidateContentOrder?: string[];
33
39
  lastCompressionTime: number;
34
40
  stats: HeadroomStats;
35
41
  }
@@ -127,6 +133,8 @@ function createRuntime(pi: ExtensionAPI): HeadroomRuntime {
127
133
  lastInputFingerprint: null,
128
134
  lastOutputFingerprint: null,
129
135
  lastGuardSkipCandidateFingerprint: null,
136
+ seenCandidateContentFingerprints: new Set(),
137
+ seenCandidateContentOrder: [],
130
138
  lastCompressionTime: 0,
131
139
  stats: { attempts: 0, applied: 0, guardSkips: 0, tokensSaved: 0 },
132
140
  };
@@ -262,10 +270,18 @@ async function handleContextCompression(
262
270
  ): Promise<{ messages?: AgentMessage[] } | undefined> {
263
271
  if (runtime.state.processing) return undefined;
264
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
+
265
281
  // Throttle: max 1 compression attempt per 3 seconds to kill infinite loops
266
282
  const now = Date.now();
267
283
  if (now - runtime.state.lastCompressionTime < 3000) {
268
- return undefined;
284
+ return bypassUncompressed();
269
285
  }
270
286
 
271
287
  // Content-based guards to prevent infinite recursion
@@ -278,22 +294,26 @@ async function handleContextCompression(
278
294
 
279
295
  // 2. If this input matches our previous input, it didn't change. Stop.
280
296
  if (runtime.state.lastInputFingerprint === inputFingerprint) {
281
- return undefined;
297
+ return bypassUncompressed();
282
298
  }
283
299
 
284
- if (shouldSkipBeforePayload(runtime, ctx)) return undefined;
300
+ if (shouldSkipBeforePayload(runtime, ctx)) return bypassUncompressed();
285
301
  const payload = buildCompressionPayload(event.messages, runtime.config.minMessageChars);
286
- if (payload.candidateCount === 0) return undefined;
302
+ if (payload.candidateCount === 0) return bypassUncompressed();
287
303
 
288
304
  // 3. If eligible candidates haven't changed since the last skip/no-savings result,
289
305
  // don't spend another proxy call just because surrounding conversation changed.
290
306
  const candidateFingerprint = generateCandidateFingerprint(event.messages, payload);
291
307
  if (runtime.state.lastGuardSkipCandidateFingerprint === candidateFingerprint) {
292
- return undefined;
308
+ return bypassUncompressed();
309
+ }
310
+ if (allCandidateContentSeen(runtime.state, payload)) {
311
+ return bypassUncompressed();
293
312
  }
313
+ ignoreSeenCandidateContent(runtime.state, payload);
294
314
  if (runtime.state.proxyOnline !== true) {
295
315
  void ensureProxyInBackground(runtime, ctx);
296
- return undefined;
316
+ return bypassUncompressed();
297
317
  }
298
318
 
299
319
  runtime.state.processing = true;
@@ -307,10 +327,7 @@ async function handleContextCompression(
307
327
  // until the actual compressible candidate material changes.
308
328
  runtime.state.lastInputFingerprint = inputFingerprint;
309
329
  runtime.state.lastGuardSkipCandidateFingerprint = candidateFingerprint;
310
- // ponytail: clear stats.last so renderFooterStatus shows idle instead of stale savings
311
- runtime.state.stats.last = undefined;
312
- runtime.refreshStatus(ctx);
313
- return undefined;
330
+ return bypassUncompressed();
314
331
  }
315
332
 
316
333
  const applied = applyCompressionResult(event.messages, payload.mappings, result.messages, {
@@ -325,28 +342,77 @@ async function handleContextCompression(
325
342
  recordGuardSkip(runtime.state.stats, applied.reason);
326
343
  emitNotraceTelemetry(runtime);
327
344
  announceGuardSkip(ctx, applied.reason, result, runtime.config.mode);
328
- runtime.refreshStatus(ctx);
329
- return undefined;
345
+ return bypassUncompressed();
330
346
  }
331
347
 
332
348
  // Store fingerprints to break the feedback loop
333
349
  runtime.state.lastInputFingerprint = inputFingerprint;
334
350
  runtime.state.lastOutputFingerprint = generateFingerprint(applied.messages);
335
351
  runtime.state.lastGuardSkipCandidateFingerprint = null;
336
-
337
- 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);
338
362
  emitNotraceTelemetry(runtime);
339
- announceAppliedCompression(runtime.pi, ctx, result, applied.appliedMessages, runtime.config.mode);
363
+ announceAppliedCompression(runtime.pi, ctx, appliedResult, applied.appliedMessages, runtime.config.mode);
340
364
  runtime.refreshStatus(ctx);
341
365
  return { messages: applied.messages };
342
366
  } catch (error) {
343
367
  recordCompressionError(runtime, ctx, error);
344
- return undefined;
368
+ return bypassUncompressed();
345
369
  } finally {
346
370
  runtime.state.processing = false;
347
371
  }
348
372
  }
349
373
 
374
+ function allCandidateContentSeen(state: HeadroomRuntimeState, payload: CompressionPayload): boolean {
375
+ const seen = state.seenCandidateContentFingerprints;
376
+ if (!seen?.size) return false;
377
+ const hashes = payload.mappings.filter((mapping) => mapping.applyTo).map((mapping) => stableHash(mapping.originalText));
378
+ return hashes.length > 0 && hashes.every((hash) => seen.has(hash));
379
+ }
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
+
389
+ function recordSeenCandidateContent(
390
+ state: HeadroomRuntimeState,
391
+ payload: CompressionPayload,
392
+ appliedMessages: AgentMessage[],
393
+ ): void {
394
+ state.seenCandidateContentFingerprints ??= new Set();
395
+ state.seenCandidateContentOrder ??= [];
396
+ payload.mappings.forEach((mapping) => {
397
+ if (!mapping.applyTo) return;
398
+ addSeenCandidateHash(state, stableHash(mapping.originalText));
399
+ const applied = convertMessage(appliedMessages[mapping.sourceIndex]);
400
+ if (applied) addSeenCandidateHash(state, stableHash(extractOpenAIText(applied)));
401
+ });
402
+ }
403
+
404
+ function addSeenCandidateHash(state: HeadroomRuntimeState, hash: string): void {
405
+ state.seenCandidateContentFingerprints ??= new Set();
406
+ state.seenCandidateContentOrder ??= [];
407
+ if (state.seenCandidateContentFingerprints.has(hash)) return;
408
+ state.seenCandidateContentFingerprints.add(hash);
409
+ state.seenCandidateContentOrder.push(hash);
410
+ while (state.seenCandidateContentOrder.length > MAX_SEEN_CANDIDATE_CONTENT_FINGERPRINTS) {
411
+ const oldest = state.seenCandidateContentOrder.shift();
412
+ if (oldest) state.seenCandidateContentFingerprints.delete(oldest);
413
+ }
414
+ }
415
+
350
416
  function generateCandidateFingerprint(messages: AgentMessage[], payload: CompressionPayload): string {
351
417
  const units = payload.mappings
352
418
  .filter((mapping) => mapping.applyTo)
@@ -426,7 +492,7 @@ function recordAppliedCompression(stats: HeadroomStats, result: CompressResult,
426
492
  function summarizeTelemetry(state: HeadroomRuntimeState): string | null {
427
493
  if (state.stats.last) {
428
494
  const pct = Math.round((1 - state.stats.last.compressionRatio) * 100);
429
- 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`;
430
496
  }
431
497
  if (!state.enabled) return "headroom loaded but disabled for this session";
432
498
  if (state.stats.lastSkipReason) return `compression not applied; last guard skip: ${state.stats.lastSkipReason}`;
@@ -478,7 +544,7 @@ function announceAppliedCompression(
478
544
  ): void {
479
545
  if (mode === "quiet" || mode === "silent") return;
480
546
  const pct = Math.round((1 - result.compressionRatio) * 100);
481
- 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}%)`;
482
548
  const line = `noheadroom: ${summary}`;
483
549
  ctx.ui.notify(line, "info");
484
550
  // Note: we no longer use pi.appendEntry or pi.sendMessage here because
@@ -615,12 +681,7 @@ function renderFooterStatus(ctx: ExtensionContext, config: HeadroomConfig, state
615
681
  if (state.proxyStarting) return paint("dim", "⏳ Headroom starting");
616
682
  if (state.proxyOnline === false) return paint("dim", "○ Headroom not running");
617
683
  if (state.proxyOnline === null && !state.stats.last) return paint("dim", "○ Headroom idle");
618
- if (!state.stats.last) return paint("success", "✓") + paint("dim", " Headroom");
619
-
620
- const pct = Math.round((1 - state.stats.last.compressionRatio) * 100);
621
- return (
622
- paint("success", "✓") + paint("dim", ` Headroom -${pct}% (${state.stats.last.tokensSaved.toLocaleString()} saved)`)
623
- );
684
+ return paint("success", "✓") + paint("dim", " Headroom active");
624
685
  }
625
686
 
626
687
  async function showProxyStats(
@@ -660,7 +721,8 @@ function renderStatus(config: HeadroomConfig, state: HeadroomRuntimeState): stri
660
721
  ` Attempts: ${stats.attempts}`,
661
722
  ` Applied: ${stats.applied}`,
662
723
  ` Guard skips: ${stats.guardSkips}`,
663
- ` 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.`,
664
726
  ];
665
727
  if (stats.last) {
666
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,11 +1,12 @@
1
1
  {
2
2
  "name": "@raquezha/noheadroom",
3
- "version": "0.3.0",
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",
7
7
  "scripts": {
8
8
  "build": "tsc",
9
+ "bench:loop": "node bench-loop-hardening.mjs",
9
10
  "prepare": "npm run build"
10
11
  },
11
12
  "peerDependencies": {