@raquezha/noheadroom 0.0.2 → 0.2.0
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.d.ts +8 -0
- package/dist/bridge.js +7 -4
- package/dist/index.d.ts +22 -2
- package/dist/index.js +46 -16
- package/extensions/bridge.ts +7 -4
- package/extensions/index.ts +61 -17
- package/package.json +8 -1
- package/CHANGELOG.md +0 -7
- package/tsconfig.json +0 -15
package/dist/bridge.d.ts
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
1
|
import type { AgentMessage, ApplyCompressionOptions, ApplyCompressionResult, CompressionMapping, CompressionPayload, OpenAIMessage } from "./types.js";
|
|
2
|
+
type AnyMessage = AgentMessage & {
|
|
3
|
+
role?: string;
|
|
4
|
+
content?: unknown;
|
|
5
|
+
timestamp?: number;
|
|
6
|
+
};
|
|
2
7
|
export declare function buildCompressionPayload(messages: AgentMessage[], minMessageChars: number): CompressionPayload;
|
|
3
8
|
export declare function applyCompressionResult(originalMessages: AgentMessage[], mappings: CompressionMapping[], compressedMessages: OpenAIMessage[], _options: ApplyCompressionOptions): ApplyCompressionResult;
|
|
9
|
+
export declare function convertMessage(message: AnyMessage): OpenAIMessage | undefined;
|
|
10
|
+
export declare function extractOpenAIText(message: OpenAIMessage): string;
|
|
11
|
+
export {};
|
package/dist/bridge.js
CHANGED
|
@@ -8,8 +8,11 @@ export function buildCompressionPayload(messages, minMessageChars) {
|
|
|
8
8
|
if (!converted)
|
|
9
9
|
continue;
|
|
10
10
|
const originalText = extractOpenAIText(converted);
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
// Allow any toolResult to be a candidate for compression if Headroom decides to shrink it.
|
|
12
|
+
// The minMessageChars threshold is primarily to avoid overhead for tiny messages,
|
|
13
|
+
// but we shouldn't block Headroom if it finds savings in slightly smaller ones.
|
|
14
|
+
const applyTo = source.role === "toolResult" ? "toolResult" : null;
|
|
15
|
+
if (applyTo && originalText.length >= minMessageChars)
|
|
13
16
|
candidateCount++;
|
|
14
17
|
mappings.push({ sourceIndex, message: converted, applyTo, originalText });
|
|
15
18
|
}
|
|
@@ -51,7 +54,7 @@ export function applyCompressionResult(originalMessages, mappings, compressedMes
|
|
|
51
54
|
}
|
|
52
55
|
return { ok: true, messages: nextMessages, appliedMessages };
|
|
53
56
|
}
|
|
54
|
-
function convertMessage(message) {
|
|
57
|
+
export function convertMessage(message) {
|
|
55
58
|
if (!message.role || !STANDARD_COMPRESSIBLE_ROLES.has(message.role))
|
|
56
59
|
return undefined;
|
|
57
60
|
switch (message.role) {
|
|
@@ -142,7 +145,7 @@ function validateAlignedMessage(original, compressed) {
|
|
|
142
145
|
}
|
|
143
146
|
return { ok: true };
|
|
144
147
|
}
|
|
145
|
-
function extractOpenAIText(message) {
|
|
148
|
+
export function extractOpenAIText(message) {
|
|
146
149
|
if (message.role === "assistant")
|
|
147
150
|
return typeof message.content === "string" ? message.content : "";
|
|
148
151
|
if (message.role === "tool" || message.role === "system")
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import
|
|
1
|
+
import type { ContextEvent, ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { HeadroomHttpClient } from "./client.js";
|
|
3
|
+
import type { AgentMessage, HeadroomConfig, HeadroomStats } from "./types.js";
|
|
3
4
|
interface HeadroomRuntimeState {
|
|
4
5
|
enabled: boolean;
|
|
5
6
|
proxyOnline: boolean | null;
|
|
@@ -7,13 +8,32 @@ interface HeadroomRuntimeState {
|
|
|
7
8
|
proxyStartAttempted: boolean;
|
|
8
9
|
remoteWarningShown: boolean;
|
|
9
10
|
offlineWarningShown: boolean;
|
|
11
|
+
processing: boolean;
|
|
12
|
+
lastInputFingerprint: string | null;
|
|
13
|
+
lastOutputFingerprint: string | null;
|
|
14
|
+
lastCompressionTime: number;
|
|
10
15
|
stats: HeadroomStats;
|
|
11
16
|
}
|
|
17
|
+
interface HeadroomRuntime {
|
|
18
|
+
pi: ExtensionAPI;
|
|
19
|
+
config: HeadroomConfig;
|
|
20
|
+
client: HeadroomHttpClient;
|
|
21
|
+
state: HeadroomRuntimeState;
|
|
22
|
+
refreshStatus(ctx: ExtensionContext): void;
|
|
23
|
+
updateHealth(ctx: ExtensionContext): Promise<boolean>;
|
|
24
|
+
ensureProxy(ctx: ExtensionContext): Promise<boolean>;
|
|
25
|
+
}
|
|
12
26
|
export default function headroomExtension(pi: ExtensionAPI): void;
|
|
27
|
+
declare function handleContextCompression(runtime: HeadroomRuntime, event: ContextEvent, ctx: ExtensionContext): Promise<{
|
|
28
|
+
messages?: AgentMessage[];
|
|
29
|
+
} | undefined>;
|
|
30
|
+
declare function generateFingerprint(messages: AgentMessage[]): string;
|
|
13
31
|
declare function isAbortOrTimeoutError(error: unknown): boolean;
|
|
14
32
|
declare function renderFooterStatus(ctx: ExtensionContext, config: HeadroomConfig, state: HeadroomRuntimeState): string;
|
|
15
33
|
export declare const __test__: {
|
|
16
34
|
isAbortOrTimeoutError: typeof isAbortOrTimeoutError;
|
|
17
35
|
renderFooterStatus: typeof renderFooterStatus;
|
|
36
|
+
generateFingerprint: typeof generateFingerprint;
|
|
37
|
+
handleContextCompression: typeof handleContextCompression;
|
|
18
38
|
};
|
|
19
39
|
export {};
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Box, Text } from "@earendil-works/pi-tui";
|
|
2
|
-
import { applyCompressionResult, buildCompressionPayload } from "./bridge.js";
|
|
2
|
+
import { applyCompressionResult, buildCompressionPayload, convertMessage, extractOpenAIText, } from "./bridge.js";
|
|
3
3
|
import { HeadroomHttpClient } from "./client.js";
|
|
4
4
|
import { isRemoteBlocked, loadHeadroomConfig } from "./config.js";
|
|
5
5
|
import { startPersistentHeadroomProxy } from "./proxy-manager.js";
|
|
@@ -49,6 +49,10 @@ function createRuntime(pi) {
|
|
|
49
49
|
proxyStartAttempted: false,
|
|
50
50
|
remoteWarningShown: false,
|
|
51
51
|
offlineWarningShown: false,
|
|
52
|
+
processing: false,
|
|
53
|
+
lastInputFingerprint: null,
|
|
54
|
+
lastOutputFingerprint: null,
|
|
55
|
+
lastCompressionTime: 0,
|
|
52
56
|
stats: { attempts: 0, applied: 0, guardSkips: 0, tokensSaved: 0 },
|
|
53
57
|
};
|
|
54
58
|
const runtime = {
|
|
@@ -154,7 +158,7 @@ function sleep(ms) {
|
|
|
154
158
|
function registerCompressionRenderer(pi) {
|
|
155
159
|
pi.registerMessageRenderer(COMPRESSION_ENTRY_TYPE, (message, { expanded }, theme) => {
|
|
156
160
|
const details = message.details;
|
|
157
|
-
const content = typeof message.content === "string" ? message.content : "noheadroom compression applied";
|
|
161
|
+
const content = details?.summary ?? (typeof message.content === "string" ? message.content : "noheadroom compression applied");
|
|
158
162
|
const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
|
|
159
163
|
let text = `${theme.fg("success", "🗜 noheadroom")} ${content}`;
|
|
160
164
|
if (expanded && details) {
|
|
@@ -169,6 +173,23 @@ function registerCompressionRenderer(pi) {
|
|
|
169
173
|
});
|
|
170
174
|
}
|
|
171
175
|
async function handleContextCompression(runtime, event, ctx) {
|
|
176
|
+
if (runtime.state.processing)
|
|
177
|
+
return undefined;
|
|
178
|
+
// Throttle: max 1 compression attempt per 3 seconds to kill infinite loops
|
|
179
|
+
const now = Date.now();
|
|
180
|
+
if (now - runtime.state.lastCompressionTime < 3000) {
|
|
181
|
+
return undefined;
|
|
182
|
+
}
|
|
183
|
+
// Content-based guards to prevent infinite recursion
|
|
184
|
+
const inputFingerprint = generateFingerprint(event.messages);
|
|
185
|
+
// 1. If this input matches our previous output, we already compressed it. Stop.
|
|
186
|
+
if (runtime.state.lastOutputFingerprint === inputFingerprint) {
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
189
|
+
// 2. If this input matches our previous input, it didn't change. Stop.
|
|
190
|
+
if (runtime.state.lastInputFingerprint === inputFingerprint) {
|
|
191
|
+
return undefined;
|
|
192
|
+
}
|
|
172
193
|
if (shouldSkipBeforePayload(runtime, ctx))
|
|
173
194
|
return undefined;
|
|
174
195
|
const payload = buildCompressionPayload(event.messages, runtime.config.minMessageChars);
|
|
@@ -178,11 +199,15 @@ async function handleContextCompression(runtime, event, ctx) {
|
|
|
178
199
|
void ensureProxyInBackground(runtime, ctx);
|
|
179
200
|
return undefined;
|
|
180
201
|
}
|
|
202
|
+
runtime.state.processing = true;
|
|
203
|
+
runtime.state.lastCompressionTime = now;
|
|
181
204
|
runtime.state.stats.attempts++;
|
|
182
205
|
try {
|
|
183
206
|
const result = await runtime.client.compress(payload.messages, ctx.model?.id, ctx.signal);
|
|
184
207
|
runtime.state.proxyOnline = true;
|
|
185
208
|
if (!result.compressed || result.tokensSaved <= 0) {
|
|
209
|
+
// Even if no tokens saved, record the input so we don't keep trying
|
|
210
|
+
runtime.state.lastInputFingerprint = inputFingerprint;
|
|
186
211
|
runtime.refreshStatus(ctx);
|
|
187
212
|
return undefined;
|
|
188
213
|
}
|
|
@@ -195,6 +220,9 @@ async function handleContextCompression(runtime, event, ctx) {
|
|
|
195
220
|
runtime.refreshStatus(ctx);
|
|
196
221
|
return undefined;
|
|
197
222
|
}
|
|
223
|
+
// Store fingerprints to break the feedback loop
|
|
224
|
+
runtime.state.lastInputFingerprint = inputFingerprint;
|
|
225
|
+
runtime.state.lastOutputFingerprint = generateFingerprint(applied.messages);
|
|
198
226
|
recordAppliedCompression(runtime.state.stats, result, applied.appliedMessages);
|
|
199
227
|
announceAppliedCompression(runtime.pi, ctx, result, applied.appliedMessages);
|
|
200
228
|
runtime.refreshStatus(ctx);
|
|
@@ -204,6 +232,18 @@ async function handleContextCompression(runtime, event, ctx) {
|
|
|
204
232
|
recordCompressionError(runtime, ctx, error);
|
|
205
233
|
return undefined;
|
|
206
234
|
}
|
|
235
|
+
finally {
|
|
236
|
+
runtime.state.processing = false;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
function generateFingerprint(messages) {
|
|
240
|
+
return messages
|
|
241
|
+
.map((m) => {
|
|
242
|
+
const converted = convertMessage(m);
|
|
243
|
+
const len = converted ? extractOpenAIText(converted).length : 0;
|
|
244
|
+
return `${m.role}:${len}`;
|
|
245
|
+
})
|
|
246
|
+
.join(",");
|
|
207
247
|
}
|
|
208
248
|
function shouldSkipBeforePayload(runtime, ctx) {
|
|
209
249
|
if (!runtime.state.enabled)
|
|
@@ -235,20 +275,8 @@ function announceAppliedCompression(pi, ctx, result, appliedMessages) {
|
|
|
235
275
|
const summary = `compressed ${result.tokensBefore.toLocaleString()} → ${result.tokensAfter.toLocaleString()} tokens (-${pct}%, saved ${result.tokensSaved.toLocaleString()}, messages ${appliedMessages})`;
|
|
236
276
|
const line = `noheadroom: ${summary}`;
|
|
237
277
|
ctx.ui.notify(line, "info");
|
|
238
|
-
pi.sendMessage
|
|
239
|
-
|
|
240
|
-
content: summary,
|
|
241
|
-
display: true,
|
|
242
|
-
details: {
|
|
243
|
-
tokensBefore: result.tokensBefore,
|
|
244
|
-
tokensAfter: result.tokensAfter,
|
|
245
|
-
tokensSaved: result.tokensSaved,
|
|
246
|
-
compressionRatio: result.compressionRatio,
|
|
247
|
-
appliedMessages,
|
|
248
|
-
transformsApplied: result.transformsApplied,
|
|
249
|
-
timestamp: Date.now(),
|
|
250
|
-
},
|
|
251
|
-
});
|
|
278
|
+
// Note: we no longer use pi.appendEntry or pi.sendMessage here because
|
|
279
|
+
// modifying the session history inside the context event triggers an infinite loop.
|
|
252
280
|
// Non-interactive `pi -p` does not always show footer/status UI, so print an explicit proof line.
|
|
253
281
|
process.stderr.write(`🗜 ${line}\n`);
|
|
254
282
|
}
|
|
@@ -428,4 +456,6 @@ function parseSubcommand(args) {
|
|
|
428
456
|
export const __test__ = {
|
|
429
457
|
isAbortOrTimeoutError,
|
|
430
458
|
renderFooterStatus,
|
|
459
|
+
generateFingerprint,
|
|
460
|
+
handleContextCompression,
|
|
431
461
|
};
|
package/extensions/bridge.ts
CHANGED
|
@@ -28,8 +28,11 @@ export function buildCompressionPayload(messages: AgentMessage[], minMessageChar
|
|
|
28
28
|
if (!converted) continue;
|
|
29
29
|
|
|
30
30
|
const originalText = extractOpenAIText(converted);
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
// Allow any toolResult to be a candidate for compression if Headroom decides to shrink it.
|
|
32
|
+
// The minMessageChars threshold is primarily to avoid overhead for tiny messages,
|
|
33
|
+
// but we shouldn't block Headroom if it finds savings in slightly smaller ones.
|
|
34
|
+
const applyTo = source.role === "toolResult" ? "toolResult" : null;
|
|
35
|
+
if (applyTo && originalText.length >= minMessageChars) candidateCount++;
|
|
33
36
|
mappings.push({ sourceIndex, message: converted, applyTo, originalText });
|
|
34
37
|
}
|
|
35
38
|
|
|
@@ -84,7 +87,7 @@ export function applyCompressionResult(
|
|
|
84
87
|
return { ok: true, messages: nextMessages, appliedMessages };
|
|
85
88
|
}
|
|
86
89
|
|
|
87
|
-
function convertMessage(message: AnyMessage): OpenAIMessage | undefined {
|
|
90
|
+
export function convertMessage(message: AnyMessage): OpenAIMessage | undefined {
|
|
88
91
|
if (!message.role || !STANDARD_COMPRESSIBLE_ROLES.has(message.role)) return undefined;
|
|
89
92
|
switch (message.role) {
|
|
90
93
|
case "user":
|
|
@@ -176,7 +179,7 @@ function validateAlignedMessage(
|
|
|
176
179
|
return { ok: true };
|
|
177
180
|
}
|
|
178
181
|
|
|
179
|
-
function extractOpenAIText(message: OpenAIMessage): string {
|
|
182
|
+
export function extractOpenAIText(message: OpenAIMessage): string {
|
|
180
183
|
if (message.role === "assistant") return typeof message.content === "string" ? message.content : "";
|
|
181
184
|
if (message.role === "tool" || message.role === "system") return message.content;
|
|
182
185
|
if (typeof message.content === "string") return message.content;
|
package/extensions/index.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import type { ContextEvent, ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Box, Text } from "@earendil-works/pi-tui";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
applyCompressionResult,
|
|
5
|
+
buildCompressionPayload,
|
|
6
|
+
convertMessage,
|
|
7
|
+
extractOpenAIText,
|
|
8
|
+
} from "./bridge.js";
|
|
4
9
|
import { HeadroomHttpClient } from "./client.js";
|
|
5
10
|
import { isRemoteBlocked, loadHeadroomConfig } from "./config.js";
|
|
6
11
|
import { startPersistentHeadroomProxy } from "./proxy-manager.js";
|
|
@@ -18,6 +23,10 @@ interface HeadroomRuntimeState {
|
|
|
18
23
|
proxyStartAttempted: boolean;
|
|
19
24
|
remoteWarningShown: boolean;
|
|
20
25
|
offlineWarningShown: boolean;
|
|
26
|
+
processing: boolean;
|
|
27
|
+
lastInputFingerprint: string | null;
|
|
28
|
+
lastOutputFingerprint: string | null;
|
|
29
|
+
lastCompressionTime: number;
|
|
21
30
|
stats: HeadroomStats;
|
|
22
31
|
}
|
|
23
32
|
|
|
@@ -93,6 +102,10 @@ function createRuntime(pi: ExtensionAPI): HeadroomRuntime {
|
|
|
93
102
|
proxyStartAttempted: false,
|
|
94
103
|
remoteWarningShown: false,
|
|
95
104
|
offlineWarningShown: false,
|
|
105
|
+
processing: false,
|
|
106
|
+
lastInputFingerprint: null,
|
|
107
|
+
lastOutputFingerprint: null,
|
|
108
|
+
lastCompressionTime: 0,
|
|
96
109
|
stats: { attempts: 0, applied: 0, guardSkips: 0, tokensSaved: 0 },
|
|
97
110
|
};
|
|
98
111
|
|
|
@@ -200,8 +213,8 @@ function sleep(ms: number): Promise<void> {
|
|
|
200
213
|
|
|
201
214
|
function registerCompressionRenderer(pi: ExtensionAPI): void {
|
|
202
215
|
pi.registerMessageRenderer(COMPRESSION_ENTRY_TYPE, (message, { expanded }, theme) => {
|
|
203
|
-
const details = message.details as CompressionEntryDetails | undefined;
|
|
204
|
-
const content = typeof message.content === "string" ? message.content : "noheadroom compression applied";
|
|
216
|
+
const details = message.details as (CompressionEntryDetails & { summary?: string }) | undefined;
|
|
217
|
+
const content = details?.summary ?? (typeof message.content === "string" ? message.content : "noheadroom compression applied");
|
|
205
218
|
const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
|
|
206
219
|
let text = `${theme.fg("success", "🗜 noheadroom")} ${content}`;
|
|
207
220
|
if (expanded && details) {
|
|
@@ -221,6 +234,27 @@ async function handleContextCompression(
|
|
|
221
234
|
event: ContextEvent,
|
|
222
235
|
ctx: ExtensionContext,
|
|
223
236
|
): Promise<{ messages?: AgentMessage[] } | undefined> {
|
|
237
|
+
if (runtime.state.processing) return undefined;
|
|
238
|
+
|
|
239
|
+
// Throttle: max 1 compression attempt per 3 seconds to kill infinite loops
|
|
240
|
+
const now = Date.now();
|
|
241
|
+
if (now - runtime.state.lastCompressionTime < 3000) {
|
|
242
|
+
return undefined;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Content-based guards to prevent infinite recursion
|
|
246
|
+
const inputFingerprint = generateFingerprint(event.messages);
|
|
247
|
+
|
|
248
|
+
// 1. If this input matches our previous output, we already compressed it. Stop.
|
|
249
|
+
if (runtime.state.lastOutputFingerprint === inputFingerprint) {
|
|
250
|
+
return undefined;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// 2. If this input matches our previous input, it didn't change. Stop.
|
|
254
|
+
if (runtime.state.lastInputFingerprint === inputFingerprint) {
|
|
255
|
+
return undefined;
|
|
256
|
+
}
|
|
257
|
+
|
|
224
258
|
if (shouldSkipBeforePayload(runtime, ctx)) return undefined;
|
|
225
259
|
const payload = buildCompressionPayload(event.messages, runtime.config.minMessageChars);
|
|
226
260
|
if (payload.candidateCount === 0) return undefined;
|
|
@@ -229,11 +263,15 @@ async function handleContextCompression(
|
|
|
229
263
|
return undefined;
|
|
230
264
|
}
|
|
231
265
|
|
|
266
|
+
runtime.state.processing = true;
|
|
267
|
+
runtime.state.lastCompressionTime = now;
|
|
232
268
|
runtime.state.stats.attempts++;
|
|
233
269
|
try {
|
|
234
270
|
const result = await runtime.client.compress(payload.messages, ctx.model?.id, ctx.signal);
|
|
235
271
|
runtime.state.proxyOnline = true;
|
|
236
272
|
if (!result.compressed || result.tokensSaved <= 0) {
|
|
273
|
+
// Even if no tokens saved, record the input so we don't keep trying
|
|
274
|
+
runtime.state.lastInputFingerprint = inputFingerprint;
|
|
237
275
|
runtime.refreshStatus(ctx);
|
|
238
276
|
return undefined;
|
|
239
277
|
}
|
|
@@ -248,6 +286,10 @@ async function handleContextCompression(
|
|
|
248
286
|
return undefined;
|
|
249
287
|
}
|
|
250
288
|
|
|
289
|
+
// Store fingerprints to break the feedback loop
|
|
290
|
+
runtime.state.lastInputFingerprint = inputFingerprint;
|
|
291
|
+
runtime.state.lastOutputFingerprint = generateFingerprint(applied.messages);
|
|
292
|
+
|
|
251
293
|
recordAppliedCompression(runtime.state.stats, result, applied.appliedMessages);
|
|
252
294
|
announceAppliedCompression(runtime.pi, ctx, result, applied.appliedMessages);
|
|
253
295
|
runtime.refreshStatus(ctx);
|
|
@@ -255,9 +297,21 @@ async function handleContextCompression(
|
|
|
255
297
|
} catch (error) {
|
|
256
298
|
recordCompressionError(runtime, ctx, error);
|
|
257
299
|
return undefined;
|
|
300
|
+
} finally {
|
|
301
|
+
runtime.state.processing = false;
|
|
258
302
|
}
|
|
259
303
|
}
|
|
260
304
|
|
|
305
|
+
function generateFingerprint(messages: AgentMessage[]): string {
|
|
306
|
+
return messages
|
|
307
|
+
.map((m) => {
|
|
308
|
+
const converted = convertMessage(m);
|
|
309
|
+
const len = converted ? extractOpenAIText(converted).length : 0;
|
|
310
|
+
return `${m.role}:${len}`;
|
|
311
|
+
})
|
|
312
|
+
.join(",");
|
|
313
|
+
}
|
|
314
|
+
|
|
261
315
|
function shouldSkipBeforePayload(runtime: HeadroomRuntime, ctx: ExtensionContext): boolean {
|
|
262
316
|
if (!runtime.state.enabled) return true;
|
|
263
317
|
if (isRemoteBlocked(runtime.config)) {
|
|
@@ -295,20 +349,8 @@ function announceAppliedCompression(
|
|
|
295
349
|
const summary = `compressed ${result.tokensBefore.toLocaleString()} → ${result.tokensAfter.toLocaleString()} tokens (-${pct}%, saved ${result.tokensSaved.toLocaleString()}, messages ${appliedMessages})`;
|
|
296
350
|
const line = `noheadroom: ${summary}`;
|
|
297
351
|
ctx.ui.notify(line, "info");
|
|
298
|
-
pi.sendMessage
|
|
299
|
-
|
|
300
|
-
content: summary,
|
|
301
|
-
display: true,
|
|
302
|
-
details: {
|
|
303
|
-
tokensBefore: result.tokensBefore,
|
|
304
|
-
tokensAfter: result.tokensAfter,
|
|
305
|
-
tokensSaved: result.tokensSaved,
|
|
306
|
-
compressionRatio: result.compressionRatio,
|
|
307
|
-
appliedMessages,
|
|
308
|
-
transformsApplied: result.transformsApplied,
|
|
309
|
-
timestamp: Date.now(),
|
|
310
|
-
} satisfies CompressionEntryDetails,
|
|
311
|
-
});
|
|
352
|
+
// Note: we no longer use pi.appendEntry or pi.sendMessage here because
|
|
353
|
+
// modifying the session history inside the context event triggers an infinite loop.
|
|
312
354
|
// Non-interactive `pi -p` does not always show footer/status UI, so print an explicit proof line.
|
|
313
355
|
process.stderr.write(`🗜 ${line}\n`);
|
|
314
356
|
}
|
|
@@ -524,4 +566,6 @@ function parseSubcommand(args: string): Subcommand {
|
|
|
524
566
|
export const __test__ = {
|
|
525
567
|
isAbortOrTimeoutError,
|
|
526
568
|
renderFooterStatus,
|
|
569
|
+
generateFingerprint,
|
|
570
|
+
handleContextCompression,
|
|
527
571
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@raquezha/noheadroom",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.2.0",
|
|
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",
|
|
@@ -34,6 +34,13 @@
|
|
|
34
34
|
"access": "public"
|
|
35
35
|
},
|
|
36
36
|
"types": "dist/index.d.ts",
|
|
37
|
+
"files": [
|
|
38
|
+
"dist",
|
|
39
|
+
"extensions",
|
|
40
|
+
"README.md",
|
|
41
|
+
"LICENSE",
|
|
42
|
+
"UPSTREAM-README.md"
|
|
43
|
+
],
|
|
37
44
|
"pi": {
|
|
38
45
|
"extensions": [
|
|
39
46
|
"extensions"
|
package/CHANGELOG.md
DELETED
package/tsconfig.json
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2022",
|
|
4
|
-
"module": "NodeNext",
|
|
5
|
-
"moduleResolution": "NodeNext",
|
|
6
|
-
"declaration": true,
|
|
7
|
-
"outDir": "./dist",
|
|
8
|
-
"rootDir": "extensions",
|
|
9
|
-
"strict": true,
|
|
10
|
-
"esModuleInterop": true,
|
|
11
|
-
"skipLibCheck": true,
|
|
12
|
-
"forceConsistentCasingInFileNames": true
|
|
13
|
-
},
|
|
14
|
-
"include": ["extensions/**/*.ts"]
|
|
15
|
-
}
|