@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 +2 -0
- package/dist/index.js +42 -0
- package/extensions/index.ts +46 -0
- package/package.json +2 -1
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)
|
package/extensions/index.ts
CHANGED
|
@@ -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.
|
|
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": {
|