@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 +21 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +82 -22
- package/dist/types.d.ts +4 -0
- package/extensions/bridge.ts +22 -1
- package/extensions/index.ts +87 -25
- package/extensions/types.ts +9 -1
- package/package.json +2 -1
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
|
-
|
|
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
|
|
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
|
|
230
|
+
return bypassUncompressed();
|
|
218
231
|
}
|
|
219
232
|
if (shouldSkipBeforePayload(runtime, ctx))
|
|
220
|
-
return
|
|
233
|
+
return bypassUncompressed();
|
|
221
234
|
const payload = buildCompressionPayload(event.messages, runtime.config.minMessageChars);
|
|
222
235
|
if (payload.candidateCount === 0)
|
|
223
|
-
return
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
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 `
|
|
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 ${
|
|
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
|
-
|
|
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
|
-
`
|
|
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;
|
package/extensions/bridge.ts
CHANGED
|
@@ -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 {
|
|
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.
|
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
|
};
|
|
@@ -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
|
|
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
|
|
297
|
+
return bypassUncompressed();
|
|
282
298
|
}
|
|
283
299
|
|
|
284
|
-
if (shouldSkipBeforePayload(runtime, ctx)) return
|
|
300
|
+
if (shouldSkipBeforePayload(runtime, ctx)) return bypassUncompressed();
|
|
285
301
|
const payload = buildCompressionPayload(event.messages, runtime.config.minMessageChars);
|
|
286
|
-
if (payload.candidateCount === 0) return
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
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 `
|
|
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 ${
|
|
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
|
-
|
|
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
|
-
`
|
|
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);
|
package/extensions/types.ts
CHANGED
|
@@ -108,5 +108,13 @@ export interface ApplyCompressionOptions {
|
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
export type ApplyCompressionResult =
|
|
111
|
-
| {
|
|
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.
|
|
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": {
|