@raquezha/noheadroom 0.0.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/CHANGELOG.md +7 -0
- package/LICENSE +24 -0
- package/README.md +91 -0
- package/UPSTREAM-README.md +68 -0
- package/dist/bridge.d.ts +3 -0
- package/dist/bridge.js +202 -0
- package/dist/client.d.ts +14 -0
- package/dist/client.js +95 -0
- package/dist/config.d.ts +18 -0
- package/dist/config.js +78 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +431 -0
- package/dist/proxy-manager.d.ts +13 -0
- package/dist/proxy-manager.js +50 -0
- package/dist/types.d.ts +100 -0
- package/dist/types.js +1 -0
- package/extensions/bridge.ts +241 -0
- package/extensions/client.ts +112 -0
- package/extensions/config.ts +101 -0
- package/extensions/index.ts +527 -0
- package/extensions/proxy-manager.ts +58 -0
- package/extensions/types.ts +109 -0
- package/package.json +42 -0
- package/tsconfig.json +15 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
import { Box, Text } from "@earendil-works/pi-tui";
|
|
2
|
+
import { applyCompressionResult, buildCompressionPayload } from "./bridge.js";
|
|
3
|
+
import { HeadroomHttpClient } from "./client.js";
|
|
4
|
+
import { isRemoteBlocked, loadHeadroomConfig } from "./config.js";
|
|
5
|
+
import { startPersistentHeadroomProxy } from "./proxy-manager.js";
|
|
6
|
+
const STATUS_KEY = "headroom";
|
|
7
|
+
const SUBCOMMANDS = ["status", "on", "off", "health", "stats"];
|
|
8
|
+
const COMPRESSION_ENTRY_TYPE = "noheadroom.compression";
|
|
9
|
+
export default function headroomExtension(pi) {
|
|
10
|
+
registerCompressionRenderer(pi);
|
|
11
|
+
const runtime = createRuntime(pi);
|
|
12
|
+
pi.on("session_start", (_event, ctx) => {
|
|
13
|
+
if (isRemoteBlocked(runtime.config)) {
|
|
14
|
+
runtime.refreshStatus(ctx);
|
|
15
|
+
ctx.ui.notify(`Headroom remote URL is blocked by default: ${runtime.config.baseUrl}\nSet PI_HEADROOM_ALLOW_REMOTE=1 only if you trust that proxy with full context.`, "warning");
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
runtime.refreshStatus(ctx);
|
|
19
|
+
if (!runtime.state.enabled)
|
|
20
|
+
return;
|
|
21
|
+
void ensureProxyInBackground(runtime, ctx);
|
|
22
|
+
});
|
|
23
|
+
pi.on("context", (event, ctx) => handleContextCompression(runtime, event, ctx));
|
|
24
|
+
pi.registerCommand("headroom", {
|
|
25
|
+
description: "Headroom token compression. Usage: /headroom [on|off|status|health|stats]",
|
|
26
|
+
getArgumentCompletions(argumentPrefix) {
|
|
27
|
+
const prefix = argumentPrefix.trim().toLowerCase();
|
|
28
|
+
return SUBCOMMANDS.filter((command) => command.startsWith(prefix)).map((command) => ({
|
|
29
|
+
value: command,
|
|
30
|
+
label: command,
|
|
31
|
+
}));
|
|
32
|
+
},
|
|
33
|
+
handler: async (args, ctx) => handleCommand(runtime, parseSubcommand(args), ctx),
|
|
34
|
+
});
|
|
35
|
+
pi.registerCommand("headroom-health", {
|
|
36
|
+
description: "Check Headroom proxy health",
|
|
37
|
+
handler: async (_args, ctx) => {
|
|
38
|
+
await handleCommand(runtime, "health", ctx);
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
function createRuntime(pi) {
|
|
43
|
+
const config = loadHeadroomConfig();
|
|
44
|
+
const client = new HeadroomHttpClient({ baseUrl: config.baseUrl, timeoutMs: config.timeoutMs });
|
|
45
|
+
const state = {
|
|
46
|
+
enabled: config.enabled,
|
|
47
|
+
proxyOnline: null,
|
|
48
|
+
proxyStarting: false,
|
|
49
|
+
proxyStartAttempted: false,
|
|
50
|
+
remoteWarningShown: false,
|
|
51
|
+
offlineWarningShown: false,
|
|
52
|
+
stats: { attempts: 0, applied: 0, guardSkips: 0, tokensSaved: 0 },
|
|
53
|
+
};
|
|
54
|
+
const runtime = {
|
|
55
|
+
pi,
|
|
56
|
+
config,
|
|
57
|
+
client,
|
|
58
|
+
state,
|
|
59
|
+
refreshStatus(ctx) {
|
|
60
|
+
refreshStatus(ctx, runtime.config, runtime.state);
|
|
61
|
+
},
|
|
62
|
+
async updateHealth(ctx) {
|
|
63
|
+
const online = await updateHealthState(runtime, ctx.signal);
|
|
64
|
+
runtime.refreshStatus(ctx);
|
|
65
|
+
return online;
|
|
66
|
+
},
|
|
67
|
+
async ensureProxy(ctx) {
|
|
68
|
+
return ensureProxy(runtime, ctx);
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
return runtime;
|
|
72
|
+
}
|
|
73
|
+
async function updateHealthState(runtime, signal) {
|
|
74
|
+
if (isRemoteBlocked(runtime.config))
|
|
75
|
+
return false;
|
|
76
|
+
runtime.state.proxyOnline = await runtime.client.health(signal);
|
|
77
|
+
return runtime.state.proxyOnline;
|
|
78
|
+
}
|
|
79
|
+
async function ensureProxy(runtime, ctx) {
|
|
80
|
+
if (await runtime.updateHealth(ctx))
|
|
81
|
+
return true;
|
|
82
|
+
if (!runtime.config.autoStart || runtime.state.proxyStartAttempted)
|
|
83
|
+
return false;
|
|
84
|
+
runtime.state.proxyStartAttempted = true;
|
|
85
|
+
runtime.state.proxyStarting = true;
|
|
86
|
+
runtime.refreshStatus(ctx);
|
|
87
|
+
const started = await startPersistentHeadroomProxy(runtime.config);
|
|
88
|
+
if (!started.ok) {
|
|
89
|
+
runtime.state.stats.lastError = started.reason;
|
|
90
|
+
runtime.state.proxyStarting = false;
|
|
91
|
+
runtime.state.proxyOnline = false;
|
|
92
|
+
runtime.refreshStatus(ctx);
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
const online = await waitForProxyHealth(runtime, ctx.signal);
|
|
96
|
+
runtime.state.proxyStarting = false;
|
|
97
|
+
runtime.state.proxyOnline = online;
|
|
98
|
+
runtime.refreshStatus(ctx);
|
|
99
|
+
return online;
|
|
100
|
+
}
|
|
101
|
+
async function ensureProxyInBackground(runtime, ctx) {
|
|
102
|
+
try {
|
|
103
|
+
if (await updateHealthState(runtime)) {
|
|
104
|
+
safeRefreshStatus(runtime, ctx);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (!runtime.config.autoStart || runtime.state.proxyStartAttempted) {
|
|
108
|
+
safeRefreshStatus(runtime, ctx);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
runtime.state.proxyStartAttempted = true;
|
|
112
|
+
runtime.state.proxyStarting = true;
|
|
113
|
+
safeRefreshStatus(runtime, ctx);
|
|
114
|
+
const started = await startPersistentHeadroomProxy(runtime.config);
|
|
115
|
+
if (!started.ok) {
|
|
116
|
+
runtime.state.stats.lastError = started.reason;
|
|
117
|
+
runtime.state.proxyStarting = false;
|
|
118
|
+
runtime.state.proxyOnline = false;
|
|
119
|
+
safeRefreshStatus(runtime, ctx);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
runtime.state.proxyOnline = await waitForProxyHealth(runtime);
|
|
123
|
+
runtime.state.proxyStarting = false;
|
|
124
|
+
safeRefreshStatus(runtime, ctx);
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
runtime.state.proxyStarting = false;
|
|
128
|
+
runtime.state.proxyOnline = false;
|
|
129
|
+
runtime.state.stats.lastError = error instanceof Error ? error.message : String(error);
|
|
130
|
+
safeRefreshStatus(runtime, ctx);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function safeRefreshStatus(runtime, ctx) {
|
|
134
|
+
if (!ctx)
|
|
135
|
+
return;
|
|
136
|
+
try {
|
|
137
|
+
runtime.refreshStatus(ctx);
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
// The session may have been reloaded/replaced while background health was in flight.
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
async function waitForProxyHealth(runtime, signal) {
|
|
144
|
+
for (const delay of [300, 500, 800, 1200, 2000]) {
|
|
145
|
+
await sleep(delay);
|
|
146
|
+
if (await updateHealthState(runtime, signal))
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
function sleep(ms) {
|
|
152
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
153
|
+
}
|
|
154
|
+
function registerCompressionRenderer(pi) {
|
|
155
|
+
pi.registerMessageRenderer(COMPRESSION_ENTRY_TYPE, (message, { expanded }, theme) => {
|
|
156
|
+
const details = message.details;
|
|
157
|
+
const content = typeof message.content === "string" ? message.content : "noheadroom compression applied";
|
|
158
|
+
const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
|
|
159
|
+
let text = `${theme.fg("success", "🗜 noheadroom")} ${content}`;
|
|
160
|
+
if (expanded && details) {
|
|
161
|
+
text += `\n${theme.fg("dim", ` before: ${details.tokensBefore.toLocaleString()} tokens`)}`;
|
|
162
|
+
text += `\n${theme.fg("dim", ` after: ${details.tokensAfter.toLocaleString()} tokens`)}`;
|
|
163
|
+
text += `\n${theme.fg("dim", ` saved: ${details.tokensSaved.toLocaleString()} tokens`)}`;
|
|
164
|
+
text += `\n${theme.fg("dim", ` messages: ${details.appliedMessages}`)}`;
|
|
165
|
+
text += `\n${theme.fg("dim", ` transforms: ${details.transformsApplied.join(", ") || "none"}`)}`;
|
|
166
|
+
}
|
|
167
|
+
box.addChild(new Text(text, 0, 0));
|
|
168
|
+
return box;
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
async function handleContextCompression(runtime, event, ctx) {
|
|
172
|
+
if (shouldSkipBeforePayload(runtime, ctx))
|
|
173
|
+
return undefined;
|
|
174
|
+
const payload = buildCompressionPayload(event.messages, runtime.config.minMessageChars);
|
|
175
|
+
if (payload.candidateCount === 0)
|
|
176
|
+
return undefined;
|
|
177
|
+
if (runtime.state.proxyOnline !== true) {
|
|
178
|
+
void ensureProxyInBackground(runtime, ctx);
|
|
179
|
+
return undefined;
|
|
180
|
+
}
|
|
181
|
+
runtime.state.stats.attempts++;
|
|
182
|
+
try {
|
|
183
|
+
const result = await runtime.client.compress(payload.messages, ctx.model?.id, ctx.signal);
|
|
184
|
+
runtime.state.proxyOnline = true;
|
|
185
|
+
if (!result.compressed || result.tokensSaved <= 0) {
|
|
186
|
+
runtime.refreshStatus(ctx);
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
189
|
+
const applied = applyCompressionResult(event.messages, payload.mappings, result.messages, {
|
|
190
|
+
minMessageChars: runtime.config.minMessageChars,
|
|
191
|
+
});
|
|
192
|
+
if (!applied.ok) {
|
|
193
|
+
recordGuardSkip(runtime.state.stats, applied.reason);
|
|
194
|
+
announceGuardSkip(ctx, applied.reason, result);
|
|
195
|
+
runtime.refreshStatus(ctx);
|
|
196
|
+
return undefined;
|
|
197
|
+
}
|
|
198
|
+
recordAppliedCompression(runtime.state.stats, result, applied.appliedMessages);
|
|
199
|
+
announceAppliedCompression(runtime.pi, ctx, result, applied.appliedMessages);
|
|
200
|
+
runtime.refreshStatus(ctx);
|
|
201
|
+
return { messages: applied.messages };
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
recordCompressionError(runtime, ctx, error);
|
|
205
|
+
return undefined;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
function shouldSkipBeforePayload(runtime, ctx) {
|
|
209
|
+
if (!runtime.state.enabled)
|
|
210
|
+
return true;
|
|
211
|
+
if (isRemoteBlocked(runtime.config)) {
|
|
212
|
+
if (!runtime.state.remoteWarningShown) {
|
|
213
|
+
runtime.state.remoteWarningShown = true;
|
|
214
|
+
ctx.ui.notify("Headroom compression skipped because remote proxy is blocked.", "warning");
|
|
215
|
+
}
|
|
216
|
+
runtime.refreshStatus(ctx);
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
const usage = ctx.getContextUsage();
|
|
220
|
+
return usage?.tokens !== null && usage?.tokens !== undefined && usage.tokens < runtime.config.minContextTokens;
|
|
221
|
+
}
|
|
222
|
+
function recordGuardSkip(stats, reason) {
|
|
223
|
+
stats.guardSkips++;
|
|
224
|
+
stats.lastSkipReason = reason;
|
|
225
|
+
}
|
|
226
|
+
function recordAppliedCompression(stats, result, appliedMessages) {
|
|
227
|
+
stats.applied++;
|
|
228
|
+
stats.tokensSaved += result.tokensSaved;
|
|
229
|
+
stats.lastError = undefined;
|
|
230
|
+
stats.lastSkipReason = undefined;
|
|
231
|
+
stats.last = { ...result, appliedMessages };
|
|
232
|
+
}
|
|
233
|
+
function announceAppliedCompression(pi, ctx, result, appliedMessages) {
|
|
234
|
+
const pct = Math.round((1 - result.compressionRatio) * 100);
|
|
235
|
+
const summary = `compressed ${result.tokensBefore.toLocaleString()} → ${result.tokensAfter.toLocaleString()} tokens (-${pct}%, saved ${result.tokensSaved.toLocaleString()}, messages ${appliedMessages})`;
|
|
236
|
+
const line = `noheadroom: ${summary}`;
|
|
237
|
+
ctx.ui.notify(line, "info");
|
|
238
|
+
pi.sendMessage({
|
|
239
|
+
customType: COMPRESSION_ENTRY_TYPE,
|
|
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
|
+
});
|
|
252
|
+
// Non-interactive `pi -p` does not always show footer/status UI, so print an explicit proof line.
|
|
253
|
+
process.stderr.write(`🗜 ${line}\n`);
|
|
254
|
+
}
|
|
255
|
+
function announceGuardSkip(ctx, reason, result) {
|
|
256
|
+
const line = `noheadroom: compression skipped by guard (${reason}); Headroom reported ${result.tokensSaved.toLocaleString()} tokens saved but Pi context was left unchanged`;
|
|
257
|
+
ctx.ui.notify(line, "warning");
|
|
258
|
+
process.stderr.write(`⚠ ${line}\n`);
|
|
259
|
+
}
|
|
260
|
+
function recordCompressionError(runtime, ctx, error) {
|
|
261
|
+
runtime.state.stats.lastError = getErrorMessage(error);
|
|
262
|
+
if (isAbortOrTimeoutError(error)) {
|
|
263
|
+
runtime.refreshStatus(ctx);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
runtime.state.proxyOnline = false;
|
|
267
|
+
if (!runtime.state.offlineWarningShown) {
|
|
268
|
+
runtime.state.offlineWarningShown = true;
|
|
269
|
+
ctx.ui.notify(`Headroom proxy unavailable. Compression disabled until /headroom health succeeds.\n${runtime.state.stats.lastError}`, "warning");
|
|
270
|
+
}
|
|
271
|
+
runtime.refreshStatus(ctx);
|
|
272
|
+
}
|
|
273
|
+
function getErrorMessage(error) {
|
|
274
|
+
return error instanceof Error ? error.message : String(error);
|
|
275
|
+
}
|
|
276
|
+
function isAbortOrTimeoutError(error) {
|
|
277
|
+
if (!error || typeof error !== "object")
|
|
278
|
+
return false;
|
|
279
|
+
const candidate = error;
|
|
280
|
+
if (candidate.name === "TimeoutError" || candidate.name === "AbortError")
|
|
281
|
+
return true;
|
|
282
|
+
if (typeof candidate.message === "string" &&
|
|
283
|
+
/aborted due to timeout|operation was aborted/i.test(candidate.message)) {
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
return candidate.cause !== undefined && candidate.cause !== error && isAbortOrTimeoutError(candidate.cause);
|
|
287
|
+
}
|
|
288
|
+
async function handleCommand(runtime, command, ctx) {
|
|
289
|
+
if (command === "on") {
|
|
290
|
+
runtime.state.enabled = true;
|
|
291
|
+
runtime.state.offlineWarningShown = false;
|
|
292
|
+
runtime.state.proxyStartAttempted = false;
|
|
293
|
+
const healthy = await runtime.ensureProxy(ctx);
|
|
294
|
+
ctx.ui.notify(healthy
|
|
295
|
+
? "Headroom compression enabled. Proxy will keep running after Pi exits."
|
|
296
|
+
: proxyStartHint(runtime.config), healthy ? "info" : "warning");
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (command === "off") {
|
|
300
|
+
runtime.state.enabled = false;
|
|
301
|
+
runtime.refreshStatus(ctx);
|
|
302
|
+
ctx.ui.notify("Headroom compression disabled for this Pi session. The proxy process is left running.", "info");
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
if (command === "health") {
|
|
306
|
+
runtime.state.proxyStartAttempted = false;
|
|
307
|
+
const healthy = await runtime.ensureProxy(ctx);
|
|
308
|
+
ctx.ui.notify(healthy ? `Headroom proxy online: ${runtime.config.baseUrl}` : proxyStartHint(runtime.config), healthy ? "info" : "warning");
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
if (command === "stats") {
|
|
312
|
+
await showProxyStats(ctx, runtime.client, runtime.config);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
ctx.ui.notify(renderStatus(runtime.config, runtime.state), "info");
|
|
316
|
+
}
|
|
317
|
+
function refreshStatus(ctx, config, state) {
|
|
318
|
+
if (!ctx.hasUI)
|
|
319
|
+
return;
|
|
320
|
+
ctx.ui.setStatus(STATUS_KEY, renderFooterStatus(ctx, config, state));
|
|
321
|
+
}
|
|
322
|
+
function isHeadroomStatusTheme(theme) {
|
|
323
|
+
return typeof theme?.fg === "function";
|
|
324
|
+
}
|
|
325
|
+
function createStatusPainter(theme) {
|
|
326
|
+
if (isHeadroomStatusTheme(theme))
|
|
327
|
+
return (color, text) => theme.fg(color, text);
|
|
328
|
+
return (_color, text) => text;
|
|
329
|
+
}
|
|
330
|
+
function renderFooterStatus(ctx, config, state) {
|
|
331
|
+
const paint = createStatusPainter(ctx.ui.theme);
|
|
332
|
+
if (!state.enabled)
|
|
333
|
+
return paint("dim", "○ Headroom off");
|
|
334
|
+
if (isRemoteBlocked(config))
|
|
335
|
+
return paint("warning", "⚠") + paint("dim", " Headroom remote blocked");
|
|
336
|
+
if (state.proxyStarting)
|
|
337
|
+
return paint("dim", "⏳ Headroom starting");
|
|
338
|
+
if (state.proxyOnline === false)
|
|
339
|
+
return paint("dim", "○ Headroom not running");
|
|
340
|
+
if (state.proxyOnline === null && !state.stats.last)
|
|
341
|
+
return paint("dim", "○ Headroom idle");
|
|
342
|
+
if (!state.stats.last)
|
|
343
|
+
return paint("success", "✓") + paint("dim", " Headroom");
|
|
344
|
+
const pct = Math.round((1 - state.stats.last.compressionRatio) * 100);
|
|
345
|
+
return (paint("success", "✓") + paint("dim", ` Headroom -${pct}% (${state.stats.last.tokensSaved.toLocaleString()} saved)`));
|
|
346
|
+
}
|
|
347
|
+
async function showProxyStats(ctx, client, config) {
|
|
348
|
+
if (isRemoteBlocked(config)) {
|
|
349
|
+
ctx.ui.notify(renderRemoteBlocked(config), "warning");
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
try {
|
|
353
|
+
const stats = await client.stats(ctx.signal);
|
|
354
|
+
ctx.ui.notify(`Headroom proxy stats (${config.baseUrl}):\n${JSON.stringify(stats, null, 2).slice(0, 4000)}`, "info");
|
|
355
|
+
}
|
|
356
|
+
catch (error) {
|
|
357
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
358
|
+
ctx.ui.notify(`Could not read Headroom stats: ${message}`, "warning");
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
function renderStatus(config, state) {
|
|
362
|
+
const stats = state.stats;
|
|
363
|
+
const lines = [
|
|
364
|
+
"Headroom token compression",
|
|
365
|
+
` Enabled: ${state.enabled ? "yes" : "no"}`,
|
|
366
|
+
` Proxy: ${config.baseUrl} (${state.proxyOnline === true ? "online" : state.proxyStarting ? "starting" : state.proxyOnline === false ? "not running" : "unknown"})`,
|
|
367
|
+
` Auto-start: ${config.autoStart ? `yes (${config.command})` : "no"}`,
|
|
368
|
+
` Shutdown: proxy is left running after Pi exits`,
|
|
369
|
+
` Remote: ${isRemoteBlocked(config) ? "blocked" : config.allowRemote ? "allowed" : "local-only"}`,
|
|
370
|
+
` Thresholds: context >= ${config.minContextTokens.toLocaleString()} tokens, toolResult >= ${config.minMessageChars.toLocaleString()} chars`,
|
|
371
|
+
"",
|
|
372
|
+
"Session stats:",
|
|
373
|
+
` Attempts: ${stats.attempts}`,
|
|
374
|
+
` Applied: ${stats.applied}`,
|
|
375
|
+
` Guard skips: ${stats.guardSkips}`,
|
|
376
|
+
` Tokens saved: ${stats.tokensSaved.toLocaleString()}`,
|
|
377
|
+
];
|
|
378
|
+
if (stats.last) {
|
|
379
|
+
const pct = Math.round((1 - stats.last.compressionRatio) * 100);
|
|
380
|
+
lines.push("", "Last applied compression:", ` ${stats.last.tokensBefore.toLocaleString()} → ${stats.last.tokensAfter.toLocaleString()} tokens (-${pct}%)`, ` Applied messages: ${stats.last.appliedMessages}`, ` Transforms: ${stats.last.transformsApplied.join(", ") || "none"}`, ` CCR hashes: ${stats.last.ccrHashes.length}`);
|
|
381
|
+
}
|
|
382
|
+
if (stats.lastSkipReason)
|
|
383
|
+
lines.push("", `Last guard skip: ${stats.lastSkipReason}`);
|
|
384
|
+
if (stats.lastError)
|
|
385
|
+
lines.push("", `Last error: ${stats.lastError}`);
|
|
386
|
+
return lines.join("\n");
|
|
387
|
+
}
|
|
388
|
+
function proxyStartHint(config) {
|
|
389
|
+
if (isRemoteBlocked(config))
|
|
390
|
+
return renderRemoteBlocked(config);
|
|
391
|
+
if (!config.autoStart) {
|
|
392
|
+
return [
|
|
393
|
+
`Headroom proxy is not running: ${config.baseUrl}`,
|
|
394
|
+
"Auto-start is disabled. Start it manually:",
|
|
395
|
+
` HEADROOM_TELEMETRY=off ${renderManualProxyCommand(config)}`,
|
|
396
|
+
].join("\n");
|
|
397
|
+
}
|
|
398
|
+
return [
|
|
399
|
+
`Headroom proxy is not running: ${config.baseUrl}`,
|
|
400
|
+
`Tried to start persistent proxy with command: ${config.command}`,
|
|
401
|
+
"Install Headroom or set PI_HEADROOM_COMMAND if needed:",
|
|
402
|
+
' pip install "headroom-ai[proxy]"',
|
|
403
|
+
" # then run /headroom on",
|
|
404
|
+
].join("\n");
|
|
405
|
+
}
|
|
406
|
+
function renderManualProxyCommand(config) {
|
|
407
|
+
try {
|
|
408
|
+
const url = new URL(config.baseUrl);
|
|
409
|
+
const host = url.hostname === "localhost" ? "127.0.0.1" : url.hostname.replace(/^\[(.*)]$/, "$1");
|
|
410
|
+
const port = url.port || "8788";
|
|
411
|
+
return `${config.command} proxy --host ${host} --port ${port} --mode token --no-cache`;
|
|
412
|
+
}
|
|
413
|
+
catch {
|
|
414
|
+
return `${config.command} proxy --mode token --no-cache`;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
function renderRemoteBlocked(config) {
|
|
418
|
+
return [
|
|
419
|
+
`Headroom remote URL is blocked: ${config.baseUrl}`,
|
|
420
|
+
"Compression sends conversation context to the proxy.",
|
|
421
|
+
"Set PI_HEADROOM_ALLOW_REMOTE=1 only for a trusted proxy.",
|
|
422
|
+
].join("\n");
|
|
423
|
+
}
|
|
424
|
+
function parseSubcommand(args) {
|
|
425
|
+
const normalized = args.trim().toLowerCase();
|
|
426
|
+
return SUBCOMMANDS.includes(normalized) ? normalized : "status";
|
|
427
|
+
}
|
|
428
|
+
export const __test__ = {
|
|
429
|
+
isAbortOrTimeoutError,
|
|
430
|
+
renderFooterStatus,
|
|
431
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { HeadroomConfig } from "./types.js";
|
|
2
|
+
export interface ProxyEndpoint {
|
|
3
|
+
host: string;
|
|
4
|
+
port: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function startPersistentHeadroomProxy(config: HeadroomConfig): Promise<{
|
|
7
|
+
ok: true;
|
|
8
|
+
} | {
|
|
9
|
+
ok: false;
|
|
10
|
+
reason: string;
|
|
11
|
+
}>;
|
|
12
|
+
export declare function buildProxyArgs(endpoint: ProxyEndpoint): string[];
|
|
13
|
+
export declare function parseLocalEndpoint(baseUrl: string): ProxyEndpoint | undefined;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { execFile, spawn } from "node:child_process";
|
|
2
|
+
export async function startPersistentHeadroomProxy(config) {
|
|
3
|
+
const endpoint = parseLocalEndpoint(config.baseUrl);
|
|
4
|
+
if (!endpoint)
|
|
5
|
+
return { ok: false, reason: "unsupported-local-url" };
|
|
6
|
+
const available = await isCommandAvailable(config.command);
|
|
7
|
+
if (!available)
|
|
8
|
+
return { ok: false, reason: `command-not-found:${config.command}` };
|
|
9
|
+
try {
|
|
10
|
+
const child = spawn(config.command, buildProxyArgs(endpoint), {
|
|
11
|
+
detached: true,
|
|
12
|
+
stdio: "ignore",
|
|
13
|
+
env: {
|
|
14
|
+
...process.env,
|
|
15
|
+
HEADROOM_TELEMETRY: process.env.HEADROOM_TELEMETRY || "off",
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
child.unref();
|
|
19
|
+
return { ok: true };
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
return { ok: false, reason: error instanceof Error ? error.message : String(error) };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export function buildProxyArgs(endpoint) {
|
|
26
|
+
return ["proxy", "--host", endpoint.host, "--port", endpoint.port, "--mode", "token", "--no-cache"];
|
|
27
|
+
}
|
|
28
|
+
export function parseLocalEndpoint(baseUrl) {
|
|
29
|
+
try {
|
|
30
|
+
const url = new URL(baseUrl);
|
|
31
|
+
if (!["http:", "https:"].includes(url.protocol))
|
|
32
|
+
return undefined;
|
|
33
|
+
if (!["localhost", "127.0.0.1", "::1", "[::1]"].includes(url.hostname))
|
|
34
|
+
return undefined;
|
|
35
|
+
const host = url.hostname === "localhost" ? "127.0.0.1" : url.hostname.replace(/^\[(.*)]$/, "$1");
|
|
36
|
+
const port = url.port || "8787";
|
|
37
|
+
return { host, port };
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function isCommandAvailable(command) {
|
|
44
|
+
return new Promise((resolve) => {
|
|
45
|
+
const child = execFile(command, ["--help"], { timeout: 1500 }, (error) => {
|
|
46
|
+
resolve(!error);
|
|
47
|
+
});
|
|
48
|
+
child.on("error", () => resolve(false));
|
|
49
|
+
});
|
|
50
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { ContextEvent } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
export type AgentMessage = ContextEvent["messages"][number];
|
|
3
|
+
export interface TextContentPart {
|
|
4
|
+
type: "text";
|
|
5
|
+
text: string;
|
|
6
|
+
}
|
|
7
|
+
export interface ImageContentPart {
|
|
8
|
+
type: "image_url";
|
|
9
|
+
image_url: {
|
|
10
|
+
url: string;
|
|
11
|
+
detail?: "auto" | "low" | "high";
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export type OpenAIContentPart = TextContentPart | ImageContentPart;
|
|
15
|
+
export interface OpenAISystemMessage {
|
|
16
|
+
role: "system";
|
|
17
|
+
content: string;
|
|
18
|
+
}
|
|
19
|
+
export interface OpenAIUserMessage {
|
|
20
|
+
role: "user";
|
|
21
|
+
content: string | OpenAIContentPart[];
|
|
22
|
+
}
|
|
23
|
+
export interface OpenAIToolCall {
|
|
24
|
+
id: string;
|
|
25
|
+
type: "function";
|
|
26
|
+
function: {
|
|
27
|
+
name: string;
|
|
28
|
+
arguments: string;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export interface OpenAIAssistantMessage {
|
|
32
|
+
role: "assistant";
|
|
33
|
+
content: string | null;
|
|
34
|
+
tool_calls?: OpenAIToolCall[];
|
|
35
|
+
}
|
|
36
|
+
export interface OpenAIToolMessage {
|
|
37
|
+
role: "tool";
|
|
38
|
+
content: string;
|
|
39
|
+
tool_call_id: string;
|
|
40
|
+
}
|
|
41
|
+
export type OpenAIMessage = OpenAISystemMessage | OpenAIUserMessage | OpenAIAssistantMessage | OpenAIToolMessage;
|
|
42
|
+
export interface CompressResult {
|
|
43
|
+
messages: OpenAIMessage[];
|
|
44
|
+
tokensBefore: number;
|
|
45
|
+
tokensAfter: number;
|
|
46
|
+
tokensSaved: number;
|
|
47
|
+
compressionRatio: number;
|
|
48
|
+
transformsApplied: string[];
|
|
49
|
+
ccrHashes: string[];
|
|
50
|
+
compressed: boolean;
|
|
51
|
+
}
|
|
52
|
+
export interface HeadroomConfig {
|
|
53
|
+
enabled: boolean;
|
|
54
|
+
baseUrl: string;
|
|
55
|
+
allowRemote: boolean;
|
|
56
|
+
autoStart: boolean;
|
|
57
|
+
command: string;
|
|
58
|
+
minContextTokens: number;
|
|
59
|
+
minMessageChars: number;
|
|
60
|
+
timeoutMs: number;
|
|
61
|
+
}
|
|
62
|
+
export interface HeadroomStats {
|
|
63
|
+
attempts: number;
|
|
64
|
+
applied: number;
|
|
65
|
+
guardSkips: number;
|
|
66
|
+
tokensSaved: number;
|
|
67
|
+
last?: {
|
|
68
|
+
tokensBefore: number;
|
|
69
|
+
tokensAfter: number;
|
|
70
|
+
tokensSaved: number;
|
|
71
|
+
compressionRatio: number;
|
|
72
|
+
transformsApplied: string[];
|
|
73
|
+
ccrHashes: string[];
|
|
74
|
+
appliedMessages: number;
|
|
75
|
+
};
|
|
76
|
+
lastError?: string;
|
|
77
|
+
lastSkipReason?: string;
|
|
78
|
+
}
|
|
79
|
+
export interface CompressionMapping {
|
|
80
|
+
sourceIndex: number;
|
|
81
|
+
message: OpenAIMessage;
|
|
82
|
+
applyTo: "toolResult" | null;
|
|
83
|
+
originalText: string;
|
|
84
|
+
}
|
|
85
|
+
export interface CompressionPayload {
|
|
86
|
+
messages: OpenAIMessage[];
|
|
87
|
+
mappings: CompressionMapping[];
|
|
88
|
+
candidateCount: number;
|
|
89
|
+
}
|
|
90
|
+
export interface ApplyCompressionOptions {
|
|
91
|
+
minMessageChars: number;
|
|
92
|
+
}
|
|
93
|
+
export type ApplyCompressionResult = {
|
|
94
|
+
ok: true;
|
|
95
|
+
messages: AgentMessage[];
|
|
96
|
+
appliedMessages: number;
|
|
97
|
+
} | {
|
|
98
|
+
ok: false;
|
|
99
|
+
reason: string;
|
|
100
|
+
};
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|