@raquezha/noheadroom 0.2.2 → 0.2.4

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/client.d.ts CHANGED
@@ -8,6 +8,7 @@ export declare class HeadroomHttpClient {
8
8
  private readonly timeoutMs;
9
9
  constructor(options: HeadroomClientOptions);
10
10
  health(signal?: AbortSignal): Promise<boolean>;
11
+ probe(signal?: AbortSignal): Promise<boolean>;
11
12
  stats(signal?: AbortSignal): Promise<unknown>;
12
13
  compress(messages: OpenAIMessage[], model: string | undefined, signal?: AbortSignal): Promise<CompressResult>;
13
14
  }
package/dist/client.js CHANGED
@@ -21,6 +21,20 @@ export class HeadroomHttpClient {
21
21
  return false;
22
22
  }
23
23
  }
24
+ async probe(signal) {
25
+ try {
26
+ const response = await fetch(`${this.baseUrl}/v1/compress`, {
27
+ method: "POST",
28
+ headers: { "Content-Type": "application/json" },
29
+ body: JSON.stringify({ messages: [{ role: "user", content: "ping" }], model: "gpt-4o" }),
30
+ signal: buildSignal(5_000, signal),
31
+ });
32
+ return response.ok;
33
+ }
34
+ catch {
35
+ return false;
36
+ }
37
+ }
24
38
  async stats(signal) {
25
39
  const response = await fetch(`${this.baseUrl}/stats`, {
26
40
  signal: buildSignal(this.timeoutMs, signal),
package/dist/index.js CHANGED
@@ -82,7 +82,12 @@ function createRuntime(pi) {
82
82
  async function updateHealthState(runtime, signal) {
83
83
  if (isRemoteBlocked(runtime.config))
84
84
  return false;
85
- runtime.state.proxyOnline = await runtime.client.health(signal);
85
+ if (await runtime.client.health(signal)) {
86
+ runtime.state.proxyOnline = true;
87
+ }
88
+ else {
89
+ runtime.state.proxyOnline = await runtime.client.probe(signal);
90
+ }
86
91
  return runtime.state.proxyOnline;
87
92
  }
88
93
  async function ensureProxy(runtime, ctx) {
@@ -293,8 +298,8 @@ function generateFingerprint(messages) {
293
298
  return messages
294
299
  .map((m) => {
295
300
  const converted = convertMessage(m);
296
- const len = converted ? extractOpenAIText(converted).length : 0;
297
- return `${m.role}:${len}`;
301
+ const text = converted ? extractOpenAIText(converted) : "";
302
+ return `${m.role}:${text.length}:${stableHash(text)}`;
298
303
  })
299
304
  .join(",");
300
305
  }
@@ -382,12 +387,16 @@ function announceAppliedCompression(pi, ctx, result, appliedMessages) {
382
387
  // Note: we no longer use pi.appendEntry or pi.sendMessage here because
383
388
  // modifying the session history inside the context event triggers an infinite loop.
384
389
  // Non-interactive `pi -p` does not always show footer/status UI, so print an explicit proof line.
385
- process.stderr.write(`🗜 ${line}\n`);
390
+ // In interactive Pi TUI, writing to stderr during the context hook can leave a stale "Working..." row.
391
+ if (!ctx.hasUI)
392
+ process.stderr.write(`🗜 ${line}\n`);
386
393
  }
387
394
  function announceGuardSkip(ctx, reason, result) {
388
395
  const line = `noheadroom: compression skipped by guard (${reason}); Headroom reported ${result.tokensSaved.toLocaleString()} tokens saved but Pi context was left unchanged`;
389
396
  ctx.ui.notify(line, "warning");
390
- process.stderr.write(`⚠ ${line}\n`);
397
+ // In interactive Pi TUI, writing to stderr during the context hook can leave a stale "Working..." row.
398
+ if (!ctx.hasUI)
399
+ process.stderr.write(`⚠ ${line}\n`);
391
400
  }
392
401
  function recordCompressionError(runtime, ctx, error) {
393
402
  runtime.state.stats.lastError = getErrorMessage(error);
@@ -479,7 +488,7 @@ function renderFooterStatus(ctx, config, state) {
479
488
  if (!state.stats.last)
480
489
  return paint("success", "✓") + paint("dim", " Headroom");
481
490
  const pct = Math.round((1 - state.stats.last.compressionRatio) * 100);
482
- return (paint("success", "✓") + paint("dim", ` Headroom -${pct}% (${state.stats.last.tokensSaved.toLocaleString()} saved)`));
491
+ return (paint("success", "✓") + paint("dim", ` Headroom -${pct}% (${state.stats.last.tokensSaved.toLocaleString()} sve)`));
483
492
  }
484
493
  async function showProxyStats(ctx, client, config) {
485
494
  if (isRemoteBlocked(config)) {
@@ -38,6 +38,20 @@ export class HeadroomHttpClient {
38
38
  }
39
39
  }
40
40
 
41
+ async probe(signal?: AbortSignal): Promise<boolean> {
42
+ try {
43
+ const response = await fetch(`${this.baseUrl}/v1/compress`, {
44
+ method: "POST",
45
+ headers: { "Content-Type": "application/json" },
46
+ body: JSON.stringify({ messages: [{ role: "user", content: "ping" }], model: "gpt-4o" }),
47
+ signal: buildSignal(5_000, signal),
48
+ });
49
+ return response.ok;
50
+ } catch {
51
+ return false;
52
+ }
53
+ }
54
+
41
55
  async stats(signal?: AbortSignal): Promise<unknown> {
42
56
  const response = await fetch(`${this.baseUrl}/stats`, {
43
57
  signal: buildSignal(this.timeoutMs, signal),
@@ -137,7 +137,11 @@ function createRuntime(pi: ExtensionAPI): HeadroomRuntime {
137
137
 
138
138
  async function updateHealthState(runtime: HeadroomRuntime, signal?: AbortSignal): Promise<boolean> {
139
139
  if (isRemoteBlocked(runtime.config)) return false;
140
- runtime.state.proxyOnline = await runtime.client.health(signal);
140
+ if (await runtime.client.health(signal)) {
141
+ runtime.state.proxyOnline = true;
142
+ } else {
143
+ runtime.state.proxyOnline = await runtime.client.probe(signal);
144
+ }
141
145
  return runtime.state.proxyOnline;
142
146
  }
143
147
 
@@ -368,8 +372,8 @@ function generateFingerprint(messages: AgentMessage[]): string {
368
372
  return messages
369
373
  .map((m) => {
370
374
  const converted = convertMessage(m);
371
- const len = converted ? extractOpenAIText(converted).length : 0;
372
- return `${m.role}:${len}`;
375
+ const text = converted ? extractOpenAIText(converted) : "";
376
+ return `${m.role}:${text.length}:${stableHash(text)}`;
373
377
  })
374
378
  .join(",");
375
379
  }
@@ -460,13 +464,15 @@ function announceAppliedCompression(
460
464
  // Note: we no longer use pi.appendEntry or pi.sendMessage here because
461
465
  // modifying the session history inside the context event triggers an infinite loop.
462
466
  // Non-interactive `pi -p` does not always show footer/status UI, so print an explicit proof line.
463
- process.stderr.write(`🗜 ${line}\n`);
467
+ // In interactive Pi TUI, writing to stderr during the context hook can leave a stale "Working..." row.
468
+ if (!ctx.hasUI) process.stderr.write(`🗜 ${line}\n`);
464
469
  }
465
470
 
466
471
  function announceGuardSkip(ctx: ExtensionContext, reason: string, result: CompressResult): void {
467
472
  const line = `noheadroom: compression skipped by guard (${reason}); Headroom reported ${result.tokensSaved.toLocaleString()} tokens saved but Pi context was left unchanged`;
468
473
  ctx.ui.notify(line, "warning");
469
- process.stderr.write(`⚠ ${line}\n`);
474
+ // In interactive Pi TUI, writing to stderr during the context hook can leave a stale "Working..." row.
475
+ if (!ctx.hasUI) process.stderr.write(`⚠ ${line}\n`);
470
476
  }
471
477
 
472
478
  function recordCompressionError(runtime: HeadroomRuntime, ctx: ExtensionContext, error: unknown): void {
@@ -576,7 +582,7 @@ function renderFooterStatus(ctx: ExtensionContext, config: HeadroomConfig, state
576
582
 
577
583
  const pct = Math.round((1 - state.stats.last.compressionRatio) * 100);
578
584
  return (
579
- paint("success", "✓") + paint("dim", ` Headroom -${pct}% (${state.stats.last.tokensSaved.toLocaleString()} saved)`)
585
+ paint("success", "✓") + paint("dim", ` Headroom -${pct}% (${state.stats.last.tokensSaved.toLocaleString()} sve)`)
580
586
  );
581
587
  }
582
588
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raquezha/noheadroom",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
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",