@raquezha/noheadroom 0.3.0 → 0.3.1

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/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
  };
@@ -227,6 +233,9 @@ async function handleContextCompression(runtime, event, ctx) {
227
233
  if (runtime.state.lastGuardSkipCandidateFingerprint === candidateFingerprint) {
228
234
  return undefined;
229
235
  }
236
+ if (allCandidateContentSeen(runtime.state, payload)) {
237
+ return undefined;
238
+ }
230
239
  if (runtime.state.proxyOnline !== true) {
231
240
  void ensureProxyInBackground(runtime, ctx);
232
241
  return undefined;
@@ -265,6 +274,7 @@ async function handleContextCompression(runtime, event, ctx) {
265
274
  runtime.state.lastInputFingerprint = inputFingerprint;
266
275
  runtime.state.lastOutputFingerprint = generateFingerprint(applied.messages);
267
276
  runtime.state.lastGuardSkipCandidateFingerprint = null;
277
+ recordSeenCandidateContent(runtime.state, payload, result.messages);
268
278
  recordAppliedCompression(runtime.state.stats, result, applied.appliedMessages);
269
279
  emitNotraceTelemetry(runtime);
270
280
  announceAppliedCompression(runtime.pi, ctx, result, applied.appliedMessages, runtime.config.mode);
@@ -279,6 +289,38 @@ async function handleContextCompression(runtime, event, ctx) {
279
289
  runtime.state.processing = false;
280
290
  }
281
291
  }
292
+ function allCandidateContentSeen(state, payload) {
293
+ const seen = state.seenCandidateContentFingerprints;
294
+ if (!seen?.size)
295
+ return false;
296
+ const hashes = payload.mappings.filter((mapping) => mapping.applyTo).map((mapping) => stableHash(mapping.originalText));
297
+ return hashes.length > 0 && hashes.every((hash) => seen.has(hash));
298
+ }
299
+ function recordSeenCandidateContent(state, payload, compressedMessages) {
300
+ state.seenCandidateContentFingerprints ??= new Set();
301
+ state.seenCandidateContentOrder ??= [];
302
+ payload.mappings.forEach((mapping, index) => {
303
+ if (!mapping.applyTo)
304
+ return;
305
+ addSeenCandidateHash(state, stableHash(mapping.originalText));
306
+ const compressed = compressedMessages[index];
307
+ if (compressed)
308
+ addSeenCandidateHash(state, stableHash(extractOpenAIText(compressed)));
309
+ });
310
+ }
311
+ function addSeenCandidateHash(state, hash) {
312
+ state.seenCandidateContentFingerprints ??= new Set();
313
+ state.seenCandidateContentOrder ??= [];
314
+ if (state.seenCandidateContentFingerprints.has(hash))
315
+ return;
316
+ state.seenCandidateContentFingerprints.add(hash);
317
+ state.seenCandidateContentOrder.push(hash);
318
+ while (state.seenCandidateContentOrder.length > MAX_SEEN_CANDIDATE_CONTENT_FINGERPRINTS) {
319
+ const oldest = state.seenCandidateContentOrder.shift();
320
+ if (oldest)
321
+ state.seenCandidateContentFingerprints.delete(oldest);
322
+ }
323
+ }
282
324
  function generateCandidateFingerprint(messages, payload) {
283
325
  const units = payload.mappings
284
326
  .filter((mapping) => mapping.applyTo)
@@ -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
  };
@@ -291,6 +299,9 @@ async function handleContextCompression(
291
299
  if (runtime.state.lastGuardSkipCandidateFingerprint === candidateFingerprint) {
292
300
  return undefined;
293
301
  }
302
+ if (allCandidateContentSeen(runtime.state, payload)) {
303
+ return undefined;
304
+ }
294
305
  if (runtime.state.proxyOnline !== true) {
295
306
  void ensureProxyInBackground(runtime, ctx);
296
307
  return undefined;
@@ -333,6 +344,7 @@ async function handleContextCompression(
333
344
  runtime.state.lastInputFingerprint = inputFingerprint;
334
345
  runtime.state.lastOutputFingerprint = generateFingerprint(applied.messages);
335
346
  runtime.state.lastGuardSkipCandidateFingerprint = null;
347
+ recordSeenCandidateContent(runtime.state, payload, result.messages);
336
348
 
337
349
  recordAppliedCompression(runtime.state.stats, result, applied.appliedMessages);
338
350
  emitNotraceTelemetry(runtime);
@@ -347,6 +359,40 @@ async function handleContextCompression(
347
359
  }
348
360
  }
349
361
 
362
+ function allCandidateContentSeen(state: HeadroomRuntimeState, payload: CompressionPayload): boolean {
363
+ const seen = state.seenCandidateContentFingerprints;
364
+ if (!seen?.size) return false;
365
+ const hashes = payload.mappings.filter((mapping) => mapping.applyTo).map((mapping) => stableHash(mapping.originalText));
366
+ return hashes.length > 0 && hashes.every((hash) => seen.has(hash));
367
+ }
368
+
369
+ function recordSeenCandidateContent(
370
+ state: HeadroomRuntimeState,
371
+ payload: CompressionPayload,
372
+ compressedMessages: CompressionPayload["messages"],
373
+ ): void {
374
+ state.seenCandidateContentFingerprints ??= new Set();
375
+ state.seenCandidateContentOrder ??= [];
376
+ payload.mappings.forEach((mapping, index) => {
377
+ if (!mapping.applyTo) return;
378
+ addSeenCandidateHash(state, stableHash(mapping.originalText));
379
+ const compressed = compressedMessages[index];
380
+ if (compressed) addSeenCandidateHash(state, stableHash(extractOpenAIText(compressed)));
381
+ });
382
+ }
383
+
384
+ function addSeenCandidateHash(state: HeadroomRuntimeState, hash: string): void {
385
+ state.seenCandidateContentFingerprints ??= new Set();
386
+ state.seenCandidateContentOrder ??= [];
387
+ if (state.seenCandidateContentFingerprints.has(hash)) return;
388
+ state.seenCandidateContentFingerprints.add(hash);
389
+ state.seenCandidateContentOrder.push(hash);
390
+ while (state.seenCandidateContentOrder.length > MAX_SEEN_CANDIDATE_CONTENT_FINGERPRINTS) {
391
+ const oldest = state.seenCandidateContentOrder.shift();
392
+ if (oldest) state.seenCandidateContentFingerprints.delete(oldest);
393
+ }
394
+ }
395
+
350
396
  function generateCandidateFingerprint(messages: AgentMessage[], payload: CompressionPayload): string {
351
397
  const units = payload.mappings
352
398
  .filter((mapping) => mapping.applyTo)
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@raquezha/noheadroom",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
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": {