headroom-openclaw 0.1.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/README.md ADDED
@@ -0,0 +1,57 @@
1
+ # @headroom-ai/openclaw
2
+
3
+ Context compression plugin for [OpenClaw](https://github.com/openclaw/openclaw). Compresses tool outputs, code, logs, and structured data — 70-90% token savings with zero LLM calls.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install "headroom-ai[proxy]"
9
+ openclaw plugins install @headroom-ai/openclaw
10
+ ```
11
+
12
+ ## Configure
13
+
14
+ ```json
15
+ {
16
+ "plugins": {
17
+ "slots": {
18
+ "contextEngine": "headroom"
19
+ }
20
+ }
21
+ }
22
+ ```
23
+
24
+ That's it. The plugin auto-starts the Headroom proxy if it's not already running.
25
+
26
+ ## How It Works
27
+
28
+ Every time OpenClaw assembles context for the model, the plugin compresses tool outputs and large messages:
29
+
30
+ - **JSON arrays** (tool outputs, search results) — statistical selection keeps anomalies, errors, boundaries
31
+ - **Code** — AST-aware compression via tree-sitter
32
+ - **Logs** — pattern deduplication, keeps errors and boundaries
33
+ - **Text** — ML-based token compression
34
+
35
+ Compression is lossless via CCR (Compress-Cache-Retrieve): originals are stored and the agent gets a `headroom_retrieve` tool to access full details when needed.
36
+
37
+ ## Configuration Options
38
+
39
+ | Option | Default | Description |
40
+ |--------|---------|-------------|
41
+ | `proxyUrl` | auto-detected | URL of the Headroom proxy |
42
+ | `autoStart` | `true` | Start proxy automatically if not running |
43
+ | `pythonPath` | auto-detected | Path to Python binary |
44
+ | `proxyPort` | `8787` | Port for auto-started proxy |
45
+
46
+ ## Comparison with lossless-claw
47
+
48
+ | | lossless-claw | headroom |
49
+ |---|---|---|
50
+ | Compaction method | LLM summarization (DAG) | Content-aware compression (zero LLM) |
51
+ | Cost of compaction | Tokens (LLM calls) | Zero |
52
+ | Best for | Long conversations | Tool-heavy agents with large outputs |
53
+ | Retrieval | `lcm_grep`, `lcm_expand` | `headroom_retrieve` (instant) |
54
+
55
+ ## License
56
+
57
+ Apache-2.0
@@ -0,0 +1,247 @@
1
+ /**
2
+ * Headroom OpenClaw Plugin — register ContextEngine + CCR retrieval tool.
3
+ *
4
+ * Usage:
5
+ * openclaw plugins install @headroom-ai/openclaw
6
+ *
7
+ * Configuration (in ~/.openclaw/config.json or ~/.clawdbot/clawdbot.json):
8
+ * {
9
+ * "plugins": {
10
+ * "slots": { "contextEngine": "headroom" },
11
+ * "entries": { "headroom": { "enabled": true } }
12
+ * }
13
+ * }
14
+ */
15
+ declare function headroomPlugin(api: any): void;
16
+
17
+ /**
18
+ * Manages the Headroom proxy process lifecycle.
19
+ *
20
+ * - Detects if a proxy is already running (e.g., user has `headroom proxy` for Claude Code)
21
+ * - If not, spawns one as a child process with auto-assigned port
22
+ * - Health checks, restart on crash, graceful shutdown
23
+ */
24
+ interface ProxyManagerConfig {
25
+ proxyUrl?: string;
26
+ pythonPath?: string;
27
+ autoStart?: boolean;
28
+ proxyPort?: number;
29
+ }
30
+ interface ProxyManagerLogger {
31
+ info(message: string): void;
32
+ warn(message: string): void;
33
+ error(message: string): void;
34
+ debug(message: string): void;
35
+ }
36
+ declare class ProxyManager {
37
+ private config;
38
+ private logger;
39
+ private process;
40
+ private proxyUrl;
41
+ private weStartedIt;
42
+ private restartCount;
43
+ private healthInterval;
44
+ private disposed;
45
+ constructor(config?: ProxyManagerConfig, logger?: ProxyManagerLogger);
46
+ /**
47
+ * Ensure a proxy is available. Returns the URL.
48
+ *
49
+ * 1. If proxyUrl is configured, use it
50
+ * 2. Check if proxy is already running on default port
51
+ * 3. If autoStart, spawn one
52
+ */
53
+ start(): Promise<string>;
54
+ /**
55
+ * Spawn the headroom proxy as a child process.
56
+ */
57
+ private spawnProxy;
58
+ /**
59
+ * Stop the proxy if we started it.
60
+ */
61
+ stop(): Promise<void>;
62
+ getUrl(): string | null;
63
+ private healthCheck;
64
+ private waitForHealthy;
65
+ private startHealthMonitor;
66
+ private handleCrash;
67
+ private findPython;
68
+ private checkHeadroomInstalled;
69
+ private findFreePort;
70
+ }
71
+
72
+ /**
73
+ * HeadroomContextEngine — ContextEngine implementation for OpenClaw.
74
+ *
75
+ * Compresses tool outputs and conversation context using the Headroom proxy.
76
+ * Zero LLM calls — all compression is algorithmic (SmartCrusher, ContentRouter, etc.)
77
+ */
78
+
79
+ interface HeadroomEngineConfig extends ProxyManagerConfig {
80
+ enabled?: boolean;
81
+ }
82
+ declare class HeadroomContextEngine {
83
+ readonly info: {
84
+ id: string;
85
+ name: string;
86
+ version: string;
87
+ ownsCompaction: boolean;
88
+ };
89
+ private proxyManager;
90
+ private proxyUrl;
91
+ private config;
92
+ private logger;
93
+ private stats;
94
+ constructor(config?: HeadroomEngineConfig, logger?: ProxyManagerLogger);
95
+ bootstrap(params: {
96
+ sessionId: string;
97
+ sessionKey?: string;
98
+ sessionFile: string;
99
+ }): Promise<{
100
+ bootstrapped: boolean;
101
+ reason?: string;
102
+ }>;
103
+ ingest(params: {
104
+ sessionId: string;
105
+ message: any;
106
+ isHeartbeat?: boolean;
107
+ }): Promise<{
108
+ ingested: boolean;
109
+ }>;
110
+ ingestBatch?(params: {
111
+ sessionId: string;
112
+ messages: any[];
113
+ isHeartbeat?: boolean;
114
+ }): Promise<{
115
+ ingestedCount: number;
116
+ }>;
117
+ /**
118
+ * Assemble context for the model — THE CORE HOOK.
119
+ *
120
+ * Converts AgentMessage[] → OpenAI format → compress() → AgentMessage[]
121
+ */
122
+ assemble(params: {
123
+ sessionId: string;
124
+ messages: any[];
125
+ tokenBudget?: number;
126
+ model?: string;
127
+ prompt?: string;
128
+ }): Promise<{
129
+ messages: any[];
130
+ estimatedTokens: number;
131
+ systemPromptAddition?: string;
132
+ }>;
133
+ /**
134
+ * Compact context — zero-cost alternative to LLM summarization.
135
+ *
136
+ * Calls compress() with the token budget, which triggers:
137
+ * - SmartCrusher: aggressive JSON compression (70-90% on tool outputs)
138
+ * - Kompress: ModernBERT text compression (40-60% on assistant text)
139
+ * - RollingWindow: drops oldest messages if still over budget
140
+ * - CCR: stores originals for retrieval via headroom_retrieve tool
141
+ *
142
+ * Zero LLM calls. All algorithmic.
143
+ */
144
+ compact(params: {
145
+ sessionId: string;
146
+ sessionFile: string;
147
+ tokenBudget?: number;
148
+ force?: boolean;
149
+ runtimeContext?: any;
150
+ }): Promise<{
151
+ ok: boolean;
152
+ compacted: boolean;
153
+ reason?: string;
154
+ result?: {
155
+ tokensBefore: number;
156
+ tokensAfter?: number;
157
+ };
158
+ }>;
159
+ afterTurn?(params: {
160
+ sessionId: string;
161
+ messages: any[];
162
+ prePromptMessageCount: number;
163
+ isHeartbeat?: boolean;
164
+ }): Promise<void>;
165
+ prepareSubagentSpawn?(params: {
166
+ parentSessionKey: string;
167
+ childSessionKey: string;
168
+ ttlMs?: number;
169
+ }): Promise<{
170
+ rollback: () => Promise<void>;
171
+ } | undefined>;
172
+ onSubagentEnded?(params: {
173
+ childSessionKey: string;
174
+ reason: string;
175
+ }): Promise<void>;
176
+ dispose(): Promise<void>;
177
+ getStats(): {
178
+ totalCompressions: number;
179
+ totalTokensSaved: number;
180
+ totalTokensBefore: number;
181
+ compactions: number;
182
+ };
183
+ getProxyUrl(): string | null;
184
+ }
185
+
186
+ /**
187
+ * Convert between OpenClaw's AgentMessage format and OpenAI message format.
188
+ *
189
+ * AgentMessage uses:
190
+ * role: "user" | "assistant" | "toolResult"
191
+ * content: string | ContentBlock[]
192
+ *
193
+ * OpenAI uses:
194
+ * role: "user" | "assistant" | "system" | "tool"
195
+ * content: string
196
+ * tool_calls?: ToolCall[]
197
+ * tool_call_id?: string
198
+ */
199
+ interface OpenAIMessage {
200
+ role: string;
201
+ content: string | null;
202
+ tool_calls?: any[];
203
+ tool_call_id?: string;
204
+ name?: string;
205
+ }
206
+ /**
207
+ * Convert AgentMessage[] to OpenAI message format for compression.
208
+ */
209
+ declare function agentToOpenAI(messages: any[]): OpenAIMessage[];
210
+ /**
211
+ * Convert compressed OpenAI messages back to AgentMessage format.
212
+ */
213
+ declare function openAIToAgent(messages: OpenAIMessage[]): any[];
214
+
215
+ /**
216
+ * CCR (Compress-Cache-Retrieve) tool for OpenClaw.
217
+ *
218
+ * Allows the agent to retrieve original uncompressed content
219
+ * from the Headroom proxy's compression store.
220
+ */
221
+ interface RetrieveToolConfig {
222
+ proxyUrl: string;
223
+ }
224
+ declare function createHeadroomRetrieveTool(config: RetrieveToolConfig): {
225
+ name: string;
226
+ description: string;
227
+ parameters: {
228
+ type: "object";
229
+ properties: {
230
+ hash: {
231
+ type: string;
232
+ description: string;
233
+ };
234
+ query: {
235
+ type: string;
236
+ description: string;
237
+ };
238
+ };
239
+ required: string[];
240
+ };
241
+ execute: (args: {
242
+ hash: string;
243
+ query?: string;
244
+ }) => Promise<string>;
245
+ };
246
+
247
+ export { HeadroomContextEngine, ProxyManager, agentToOpenAI, createHeadroomRetrieveTool, headroomPlugin as default, openAIToAgent };
package/dist/index.js ADDED
@@ -0,0 +1,593 @@
1
+ // src/engine.ts
2
+ import { compress } from "headroom-ai";
3
+
4
+ // src/proxy-manager.ts
5
+ import { spawn } from "child_process";
6
+ import { createWriteStream } from "fs";
7
+ import { join } from "path";
8
+ import { homedir } from "os";
9
+ var DEFAULT_PORT = 8787;
10
+ var HEALTH_CHECK_INTERVAL_MS = 3e4;
11
+ var STARTUP_TIMEOUT_MS = 15e3;
12
+ var RESTART_DELAY_MS = 2e3;
13
+ var MAX_RESTART_ATTEMPTS = 3;
14
+ var defaultLogger = {
15
+ info: (m) => console.log(`[headroom] ${m}`),
16
+ warn: (m) => console.warn(`[headroom] ${m}`),
17
+ error: (m) => console.error(`[headroom] ${m}`),
18
+ debug: () => {
19
+ }
20
+ };
21
+ var ProxyManager = class {
22
+ config;
23
+ logger;
24
+ process = null;
25
+ proxyUrl = null;
26
+ weStartedIt = false;
27
+ restartCount = 0;
28
+ healthInterval = null;
29
+ disposed = false;
30
+ constructor(config = {}, logger) {
31
+ this.config = config;
32
+ this.logger = logger ?? defaultLogger;
33
+ }
34
+ /**
35
+ * Ensure a proxy is available. Returns the URL.
36
+ *
37
+ * 1. If proxyUrl is configured, use it
38
+ * 2. Check if proxy is already running on default port
39
+ * 3. If autoStart, spawn one
40
+ */
41
+ async start() {
42
+ if (this.config.proxyUrl) {
43
+ const url = this.config.proxyUrl.replace(/\/+$/, "");
44
+ if (await this.healthCheck(url)) {
45
+ this.proxyUrl = url;
46
+ this.logger.info(`Connected to proxy at ${url}`);
47
+ return url;
48
+ }
49
+ throw new Error(`Headroom proxy not reachable at ${url}`);
50
+ }
51
+ const defaultUrl = `http://127.0.0.1:${DEFAULT_PORT}`;
52
+ if (await this.healthCheck(defaultUrl)) {
53
+ this.proxyUrl = defaultUrl;
54
+ this.logger.info(`Found running proxy at ${defaultUrl}`);
55
+ this.startHealthMonitor();
56
+ return defaultUrl;
57
+ }
58
+ if (this.config.autoStart !== false) {
59
+ return this.spawnProxy();
60
+ }
61
+ throw new Error(
62
+ "Headroom proxy not running. Start with: headroom proxy --port 8787\nOr install: pip install 'headroom-ai[proxy]'"
63
+ );
64
+ }
65
+ /**
66
+ * Spawn the headroom proxy as a child process.
67
+ */
68
+ async spawnProxy() {
69
+ const pythonPath = await this.findPython();
70
+ if (!pythonPath) {
71
+ throw new Error(
72
+ "Python not found. Install Python 3.10+ and run: pip install 'headroom-ai[proxy]'"
73
+ );
74
+ }
75
+ const installed = await this.checkHeadroomInstalled(pythonPath);
76
+ if (!installed) {
77
+ throw new Error(
78
+ "headroom-ai Python package not found.\nInstall with: pip install 'headroom-ai[proxy]'"
79
+ );
80
+ }
81
+ const port = this.config.proxyPort ?? 0;
82
+ const actualPort = port === 0 ? await this.findFreePort() : port;
83
+ const url = `http://127.0.0.1:${actualPort}`;
84
+ this.logger.info(`Starting proxy on port ${actualPort}...`);
85
+ const logDir = join(homedir(), ".headroom", "logs");
86
+ const logPath = join(logDir, "openclaw-proxy.log");
87
+ let logStream = null;
88
+ try {
89
+ const { mkdirSync } = await import("fs");
90
+ mkdirSync(logDir, { recursive: true });
91
+ logStream = createWriteStream(logPath, { flags: "a" });
92
+ } catch {
93
+ }
94
+ const proc = spawn(
95
+ pythonPath,
96
+ ["-m", "headroom.cli", "proxy", "--port", String(actualPort)],
97
+ {
98
+ env: { ...process.env, PYTHONIOENCODING: "utf-8" },
99
+ stdio: ["ignore", logStream ? "pipe" : "ignore", logStream ? "pipe" : "ignore"],
100
+ detached: false
101
+ }
102
+ );
103
+ if (logStream) {
104
+ proc.stdout?.pipe(logStream);
105
+ proc.stderr?.pipe(logStream);
106
+ }
107
+ proc.on("exit", (code) => {
108
+ if (!this.disposed && this.weStartedIt) {
109
+ this.logger.warn(`Proxy exited with code ${code}`);
110
+ this.handleCrash();
111
+ }
112
+ });
113
+ this.process = proc;
114
+ this.weStartedIt = true;
115
+ const healthy = await this.waitForHealthy(url, STARTUP_TIMEOUT_MS);
116
+ if (!healthy) {
117
+ proc.kill();
118
+ this.process = null;
119
+ throw new Error(
120
+ `Proxy failed to start within ${STARTUP_TIMEOUT_MS / 1e3}s. Check ${logPath}`
121
+ );
122
+ }
123
+ this.proxyUrl = url;
124
+ this.logger.info(`Proxy started on port ${actualPort} (PID: ${proc.pid})`);
125
+ this.startHealthMonitor();
126
+ return url;
127
+ }
128
+ /**
129
+ * Stop the proxy if we started it.
130
+ */
131
+ async stop() {
132
+ this.disposed = true;
133
+ if (this.healthInterval) {
134
+ clearInterval(this.healthInterval);
135
+ this.healthInterval = null;
136
+ }
137
+ if (this.process && this.weStartedIt) {
138
+ this.logger.info("Stopping proxy...");
139
+ this.process.kill("SIGTERM");
140
+ await new Promise((resolve) => {
141
+ const timeout = setTimeout(() => {
142
+ this.process?.kill("SIGKILL");
143
+ resolve();
144
+ }, 3e3);
145
+ this.process?.on("exit", () => {
146
+ clearTimeout(timeout);
147
+ resolve();
148
+ });
149
+ });
150
+ this.process = null;
151
+ }
152
+ }
153
+ getUrl() {
154
+ return this.proxyUrl;
155
+ }
156
+ // --- Internal ---
157
+ async healthCheck(url) {
158
+ try {
159
+ const resp = await fetch(`${url}/health`, {
160
+ signal: AbortSignal.timeout(3e3)
161
+ });
162
+ return resp.ok;
163
+ } catch {
164
+ return false;
165
+ }
166
+ }
167
+ async waitForHealthy(url, timeoutMs) {
168
+ const start = Date.now();
169
+ while (Date.now() - start < timeoutMs) {
170
+ if (await this.healthCheck(url)) return true;
171
+ await new Promise((r) => setTimeout(r, 500));
172
+ }
173
+ return false;
174
+ }
175
+ startHealthMonitor() {
176
+ if (this.healthInterval) return;
177
+ this.healthInterval = setInterval(async () => {
178
+ if (this.proxyUrl && !await this.healthCheck(this.proxyUrl)) {
179
+ this.logger.warn("Proxy health check failed");
180
+ if (this.weStartedIt) this.handleCrash();
181
+ }
182
+ }, HEALTH_CHECK_INTERVAL_MS);
183
+ }
184
+ async handleCrash() {
185
+ if (this.disposed) return;
186
+ if (this.restartCount >= MAX_RESTART_ATTEMPTS) {
187
+ this.logger.error(`Proxy crashed ${MAX_RESTART_ATTEMPTS} times. Giving up.`);
188
+ return;
189
+ }
190
+ this.restartCount++;
191
+ this.logger.info(`Restarting proxy (attempt ${this.restartCount}/${MAX_RESTART_ATTEMPTS})...`);
192
+ await new Promise((r) => setTimeout(r, RESTART_DELAY_MS));
193
+ try {
194
+ await this.spawnProxy();
195
+ } catch (e) {
196
+ this.logger.error(`Restart failed: ${e}`);
197
+ }
198
+ }
199
+ async findPython() {
200
+ if (this.config.pythonPath) return this.config.pythonPath;
201
+ for (const cmd of ["python3", "python"]) {
202
+ try {
203
+ const { execSync } = await import("child_process");
204
+ const version = execSync(`${cmd} --version 2>&1`, { encoding: "utf-8" }).trim();
205
+ if (version.includes("Python 3.")) return cmd;
206
+ } catch {
207
+ continue;
208
+ }
209
+ }
210
+ return null;
211
+ }
212
+ async checkHeadroomInstalled(pythonPath) {
213
+ try {
214
+ const { execSync } = await import("child_process");
215
+ execSync(`${pythonPath} -c "import headroom"`, {
216
+ encoding: "utf-8",
217
+ stdio: "pipe"
218
+ });
219
+ return true;
220
+ } catch {
221
+ return false;
222
+ }
223
+ }
224
+ async findFreePort() {
225
+ const { createServer } = await import("net");
226
+ return new Promise((resolve, reject) => {
227
+ const server = createServer();
228
+ server.listen(0, () => {
229
+ const addr = server.address();
230
+ if (addr && typeof addr === "object") {
231
+ const port = addr.port;
232
+ server.close(() => resolve(port));
233
+ } else {
234
+ reject(new Error("Could not find free port"));
235
+ }
236
+ });
237
+ });
238
+ }
239
+ };
240
+
241
+ // src/convert.ts
242
+ function agentToOpenAI(messages) {
243
+ const result = [];
244
+ for (const msg of messages) {
245
+ const role = msg.role;
246
+ if (role === "system") {
247
+ result.push({
248
+ role: "system",
249
+ content: typeof msg.content === "string" ? msg.content : extractText(msg.content)
250
+ });
251
+ continue;
252
+ }
253
+ if (role === "user") {
254
+ result.push({
255
+ role: "user",
256
+ content: typeof msg.content === "string" ? msg.content : extractText(msg.content)
257
+ });
258
+ continue;
259
+ }
260
+ if (role === "assistant") {
261
+ const content = msg.content;
262
+ if (typeof content === "string") {
263
+ result.push({ role: "assistant", content });
264
+ continue;
265
+ }
266
+ if (Array.isArray(content)) {
267
+ const textParts = [];
268
+ const toolCalls = [];
269
+ for (const block of content) {
270
+ if (typeof block === "string") {
271
+ textParts.push(block);
272
+ } else if (block.type === "text") {
273
+ textParts.push(block.text);
274
+ } else if (block.type === "tool_use") {
275
+ toolCalls.push({
276
+ id: block.id,
277
+ type: "function",
278
+ function: {
279
+ name: block.name,
280
+ arguments: typeof block.input === "string" ? block.input : JSON.stringify(block.input ?? {})
281
+ }
282
+ });
283
+ }
284
+ }
285
+ const openaiMsg = {
286
+ role: "assistant",
287
+ content: textParts.length > 0 ? textParts.join("") : null
288
+ };
289
+ if (toolCalls.length > 0) {
290
+ openaiMsg.tool_calls = toolCalls;
291
+ }
292
+ result.push(openaiMsg);
293
+ }
294
+ continue;
295
+ }
296
+ if (role === "toolResult" || role === "tool_result") {
297
+ const content = typeof msg.content === "string" ? msg.content : Array.isArray(msg.content) ? extractText(msg.content) : JSON.stringify(msg.content);
298
+ result.push({
299
+ role: "tool",
300
+ content,
301
+ tool_call_id: msg.tool_use_id ?? msg.toolCallId ?? msg.id ?? "unknown"
302
+ });
303
+ continue;
304
+ }
305
+ result.push({
306
+ role: "user",
307
+ content: typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content)
308
+ });
309
+ }
310
+ return result;
311
+ }
312
+ function openAIToAgent(messages) {
313
+ const result = [];
314
+ for (const msg of messages) {
315
+ if (msg.role === "system") {
316
+ result.push({
317
+ role: "system",
318
+ content: msg.content ?? "",
319
+ timestamp: Date.now()
320
+ });
321
+ continue;
322
+ }
323
+ if (msg.role === "user") {
324
+ result.push({
325
+ role: "user",
326
+ content: msg.content ?? "",
327
+ timestamp: Date.now()
328
+ });
329
+ continue;
330
+ }
331
+ if (msg.role === "assistant") {
332
+ const blocks = [];
333
+ if (msg.content) {
334
+ blocks.push({ type: "text", text: msg.content });
335
+ }
336
+ if (msg.tool_calls) {
337
+ for (const tc of msg.tool_calls) {
338
+ let input;
339
+ try {
340
+ input = JSON.parse(tc.function.arguments);
341
+ } catch {
342
+ input = tc.function.arguments ?? {};
343
+ }
344
+ blocks.push({
345
+ type: "tool_use",
346
+ id: tc.id,
347
+ name: tc.function.name,
348
+ input
349
+ });
350
+ }
351
+ }
352
+ result.push({
353
+ role: "assistant",
354
+ content: blocks,
355
+ timestamp: Date.now()
356
+ });
357
+ continue;
358
+ }
359
+ if (msg.role === "tool") {
360
+ result.push({
361
+ role: "toolResult",
362
+ content: msg.content ?? "",
363
+ tool_use_id: msg.tool_call_id ?? "unknown",
364
+ timestamp: Date.now()
365
+ });
366
+ continue;
367
+ }
368
+ }
369
+ return result;
370
+ }
371
+ function extractText(content) {
372
+ if (typeof content === "string") return content;
373
+ if (!Array.isArray(content)) return JSON.stringify(content);
374
+ return content.map((block) => {
375
+ if (typeof block === "string") return block;
376
+ if (block.type === "text") return block.text;
377
+ if (block.type === "tool_result") {
378
+ return typeof block.content === "string" ? block.content : JSON.stringify(block.content);
379
+ }
380
+ return "";
381
+ }).filter(Boolean).join("\n");
382
+ }
383
+
384
+ // src/engine.ts
385
+ var HeadroomContextEngine = class {
386
+ info = {
387
+ id: "headroom",
388
+ name: "Headroom Context Compression",
389
+ version: "0.1.0",
390
+ ownsCompaction: true
391
+ };
392
+ proxyManager;
393
+ proxyUrl = null;
394
+ config;
395
+ logger;
396
+ stats = {
397
+ totalCompressions: 0,
398
+ totalTokensSaved: 0,
399
+ totalTokensBefore: 0,
400
+ compactions: 0
401
+ };
402
+ constructor(config = {}, logger) {
403
+ this.config = config;
404
+ this.logger = logger ?? {
405
+ info: (m) => console.log(`[headroom] ${m}`),
406
+ warn: (m) => console.warn(`[headroom] ${m}`),
407
+ error: (m) => console.error(`[headroom] ${m}`),
408
+ debug: () => {
409
+ }
410
+ };
411
+ this.proxyManager = new ProxyManager(config, this.logger);
412
+ }
413
+ // === ContextEngine Lifecycle ===
414
+ async bootstrap(params) {
415
+ if (this.config.enabled === false) {
416
+ return { bootstrapped: false, reason: "disabled" };
417
+ }
418
+ try {
419
+ this.proxyUrl = await this.proxyManager.start();
420
+ this.logger.info(`Engine bootstrapped (proxy: ${this.proxyUrl})`);
421
+ return { bootstrapped: true };
422
+ } catch (error) {
423
+ this.logger.error(`Bootstrap failed: ${error}`);
424
+ return { bootstrapped: false, reason: String(error) };
425
+ }
426
+ }
427
+ async ingest(params) {
428
+ return { ingested: true };
429
+ }
430
+ async ingestBatch(params) {
431
+ return { ingestedCount: params.messages.length };
432
+ }
433
+ /**
434
+ * Assemble context for the model — THE CORE HOOK.
435
+ *
436
+ * Converts AgentMessage[] → OpenAI format → compress() → AgentMessage[]
437
+ */
438
+ async assemble(params) {
439
+ if (!this.proxyUrl || this.config.enabled === false) {
440
+ return { messages: params.messages, estimatedTokens: 0 };
441
+ }
442
+ try {
443
+ const openaiMessages = agentToOpenAI(params.messages);
444
+ const result = await compress(openaiMessages, {
445
+ model: params.model ?? "claude-sonnet-4-5",
446
+ baseUrl: this.proxyUrl,
447
+ fallback: true,
448
+ tokenBudget: params.tokenBudget
449
+ });
450
+ if (!result.compressed || result.tokensSaved === 0) {
451
+ return { messages: params.messages, estimatedTokens: result.tokensBefore };
452
+ }
453
+ const compressedAgentMessages = openAIToAgent(result.messages);
454
+ this.stats.totalCompressions++;
455
+ this.stats.totalTokensSaved += result.tokensSaved;
456
+ this.stats.totalTokensBefore += result.tokensBefore;
457
+ this.logger.debug(
458
+ `Assembled: ${result.tokensBefore} \u2192 ${result.tokensAfter} tokens (saved ${result.tokensSaved})`
459
+ );
460
+ return {
461
+ messages: compressedAgentMessages,
462
+ estimatedTokens: result.tokensAfter,
463
+ systemPromptAddition: result.tokensSaved > 100 ? `[Context compressed by Headroom: ${result.tokensSaved} tokens saved. Use headroom_retrieve with the hash to get full details.]` : void 0
464
+ };
465
+ } catch (error) {
466
+ this.logger.error(`Assemble failed: ${error}`);
467
+ return { messages: params.messages, estimatedTokens: 0 };
468
+ }
469
+ }
470
+ /**
471
+ * Compact context — zero-cost alternative to LLM summarization.
472
+ *
473
+ * Calls compress() with the token budget, which triggers:
474
+ * - SmartCrusher: aggressive JSON compression (70-90% on tool outputs)
475
+ * - Kompress: ModernBERT text compression (40-60% on assistant text)
476
+ * - RollingWindow: drops oldest messages if still over budget
477
+ * - CCR: stores originals for retrieval via headroom_retrieve tool
478
+ *
479
+ * Zero LLM calls. All algorithmic.
480
+ */
481
+ async compact(params) {
482
+ if (!this.proxyUrl) {
483
+ return { ok: false, compacted: false, reason: "Proxy not available" };
484
+ }
485
+ this.stats.compactions++;
486
+ this.logger.info(
487
+ `Compact called (budget: ${params.tokenBudget ?? "none"}, force: ${params.force ?? false})`
488
+ );
489
+ return {
490
+ ok: true,
491
+ compacted: true,
492
+ reason: "Headroom applies SmartCrusher + Kompress + RollingWindow on next assemble()"
493
+ };
494
+ }
495
+ async afterTurn(params) {
496
+ }
497
+ async prepareSubagentSpawn(params) {
498
+ return void 0;
499
+ }
500
+ async onSubagentEnded(params) {
501
+ }
502
+ async dispose() {
503
+ await this.proxyManager.stop();
504
+ this.logger.info(
505
+ `Engine disposed. Stats: ${this.stats.totalCompressions} compressions, ${this.stats.totalTokensSaved} tokens saved`
506
+ );
507
+ }
508
+ // --- Public API ---
509
+ getStats() {
510
+ return { ...this.stats };
511
+ }
512
+ getProxyUrl() {
513
+ return this.proxyUrl;
514
+ }
515
+ };
516
+
517
+ // src/tools/headroom-retrieve.ts
518
+ function createHeadroomRetrieveTool(config) {
519
+ return {
520
+ name: "headroom_retrieve",
521
+ description: "Retrieve original uncompressed content from Headroom's compression store. Use when compressed context mentions a hash and you need the full details. Pass the hash from the compression marker (24 hex characters). Optionally pass a query to search within the original content.",
522
+ parameters: {
523
+ type: "object",
524
+ properties: {
525
+ hash: {
526
+ type: "string",
527
+ description: "The 24-character hex hash from the compression marker"
528
+ },
529
+ query: {
530
+ type: "string",
531
+ description: "Optional search query to filter results within the original content"
532
+ }
533
+ },
534
+ required: ["hash"]
535
+ },
536
+ execute: async (args) => {
537
+ const { hash, query } = args;
538
+ if (!/^[a-f0-9]{24}$/i.test(hash)) {
539
+ return JSON.stringify({
540
+ error: "Invalid hash format. Expected 24 hex characters."
541
+ });
542
+ }
543
+ try {
544
+ const url = query ? `${config.proxyUrl}/v1/retrieve/${hash}?query=${encodeURIComponent(query)}` : `${config.proxyUrl}/v1/retrieve/${hash}`;
545
+ const resp = await fetch(url, {
546
+ signal: AbortSignal.timeout(1e4)
547
+ });
548
+ if (!resp.ok) {
549
+ const body = await resp.text().catch(() => "");
550
+ return JSON.stringify({
551
+ error: `Retrieval failed: HTTP ${resp.status}`,
552
+ details: body
553
+ });
554
+ }
555
+ const data = await resp.json();
556
+ return typeof data === "string" ? data : JSON.stringify(data);
557
+ } catch (error) {
558
+ return JSON.stringify({
559
+ error: `Retrieval failed: ${error}`,
560
+ hint: "The compressed content may have expired (default TTL: 5 minutes)"
561
+ });
562
+ }
563
+ }
564
+ };
565
+ }
566
+
567
+ // src/plugin/index.ts
568
+ function headroomPlugin(api) {
569
+ const config = api.config?.plugins?.entries?.headroom?.config ?? {};
570
+ const logger = api.logger ?? console;
571
+ const engine = new HeadroomContextEngine(config, {
572
+ info: (m) => logger.info(m),
573
+ warn: (m) => logger.warn(m),
574
+ error: (m) => logger.error(m),
575
+ debug: (m) => logger.debug?.(m)
576
+ });
577
+ api.registerContextEngine("headroom", () => engine);
578
+ api.registerTool((ctx) => {
579
+ const proxyUrl = engine.getProxyUrl();
580
+ if (!proxyUrl) return null;
581
+ return createHeadroomRetrieveTool({ proxyUrl });
582
+ });
583
+ logger.info("[headroom] Plugin registered");
584
+ }
585
+ export {
586
+ HeadroomContextEngine,
587
+ ProxyManager,
588
+ agentToOpenAI,
589
+ createHeadroomRetrieveTool,
590
+ headroomPlugin as default,
591
+ openAIToAgent
592
+ };
593
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/engine.ts","../src/proxy-manager.ts","../src/convert.ts","../src/tools/headroom-retrieve.ts","../src/plugin/index.ts"],"sourcesContent":["/**\n * HeadroomContextEngine — ContextEngine implementation for OpenClaw.\n *\n * Compresses tool outputs and conversation context using the Headroom proxy.\n * Zero LLM calls — all compression is algorithmic (SmartCrusher, ContentRouter, etc.)\n */\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\n\nimport { compress } from \"headroom-ai\";\nimport { ProxyManager, type ProxyManagerConfig, type ProxyManagerLogger } from \"./proxy-manager.js\";\nimport { agentToOpenAI, openAIToAgent } from \"./convert.js\";\n\nexport interface HeadroomEngineConfig extends ProxyManagerConfig {\n enabled?: boolean;\n}\n\nexport class HeadroomContextEngine {\n readonly info = {\n id: \"headroom\",\n name: \"Headroom Context Compression\",\n version: \"0.1.0\",\n ownsCompaction: true,\n };\n\n private proxyManager: ProxyManager;\n private proxyUrl: string | null = null;\n private config: HeadroomEngineConfig;\n private logger: ProxyManagerLogger;\n private stats = {\n totalCompressions: 0,\n totalTokensSaved: 0,\n totalTokensBefore: 0,\n compactions: 0,\n };\n\n constructor(config: HeadroomEngineConfig = {}, logger?: ProxyManagerLogger) {\n this.config = config;\n this.logger = logger ?? {\n info: (m) => console.log(`[headroom] ${m}`),\n warn: (m) => console.warn(`[headroom] ${m}`),\n error: (m) => console.error(`[headroom] ${m}`),\n debug: () => {},\n };\n this.proxyManager = new ProxyManager(config, this.logger);\n }\n\n // === ContextEngine Lifecycle ===\n\n async bootstrap(params: {\n sessionId: string;\n sessionKey?: string;\n sessionFile: string;\n }): Promise<{ bootstrapped: boolean; reason?: string }> {\n if (this.config.enabled === false) {\n return { bootstrapped: false, reason: \"disabled\" };\n }\n\n try {\n this.proxyUrl = await this.proxyManager.start();\n this.logger.info(`Engine bootstrapped (proxy: ${this.proxyUrl})`);\n return { bootstrapped: true };\n } catch (error) {\n this.logger.error(`Bootstrap failed: ${error}`);\n return { bootstrapped: false, reason: String(error) };\n }\n }\n\n async ingest(params: {\n sessionId: string;\n message: any;\n isHeartbeat?: boolean;\n }): Promise<{ ingested: boolean }> {\n // No-op: OpenClaw's runtime stores messages. We don't need a separate store.\n return { ingested: true };\n }\n\n async ingestBatch?(params: {\n sessionId: string;\n messages: any[];\n isHeartbeat?: boolean;\n }): Promise<{ ingestedCount: number }> {\n return { ingestedCount: params.messages.length };\n }\n\n /**\n * Assemble context for the model — THE CORE HOOK.\n *\n * Converts AgentMessage[] → OpenAI format → compress() → AgentMessage[]\n */\n async assemble(params: {\n sessionId: string;\n messages: any[];\n tokenBudget?: number;\n model?: string;\n prompt?: string;\n }): Promise<{\n messages: any[];\n estimatedTokens: number;\n systemPromptAddition?: string;\n }> {\n if (!this.proxyUrl || this.config.enabled === false) {\n // Fallback: return messages unchanged\n return { messages: params.messages, estimatedTokens: 0 };\n }\n\n try {\n // Convert AgentMessage → OpenAI format\n const openaiMessages = agentToOpenAI(params.messages);\n\n // Compress via proxy — pass tokenBudget so RollingWindow enforces it\n const result = await compress(openaiMessages, {\n model: params.model ?? \"claude-sonnet-4-5\",\n baseUrl: this.proxyUrl,\n fallback: true,\n tokenBudget: params.tokenBudget,\n });\n\n if (!result.compressed || result.tokensSaved === 0) {\n return { messages: params.messages, estimatedTokens: result.tokensBefore };\n }\n\n // Convert back to AgentMessage format\n const compressedAgentMessages = openAIToAgent(result.messages);\n\n // Track stats\n this.stats.totalCompressions++;\n this.stats.totalTokensSaved += result.tokensSaved;\n this.stats.totalTokensBefore += result.tokensBefore;\n\n this.logger.debug(\n `Assembled: ${result.tokensBefore} → ${result.tokensAfter} tokens (saved ${result.tokensSaved})`,\n );\n\n return {\n messages: compressedAgentMessages,\n estimatedTokens: result.tokensAfter,\n systemPromptAddition:\n result.tokensSaved > 100\n ? `[Context compressed by Headroom: ${result.tokensSaved} tokens saved. Use headroom_retrieve with the hash to get full details.]`\n : undefined,\n };\n } catch (error) {\n this.logger.error(`Assemble failed: ${error}`);\n // Graceful fallback: return original messages\n return { messages: params.messages, estimatedTokens: 0 };\n }\n }\n\n /**\n * Compact context — zero-cost alternative to LLM summarization.\n *\n * Calls compress() with the token budget, which triggers:\n * - SmartCrusher: aggressive JSON compression (70-90% on tool outputs)\n * - Kompress: ModernBERT text compression (40-60% on assistant text)\n * - RollingWindow: drops oldest messages if still over budget\n * - CCR: stores originals for retrieval via headroom_retrieve tool\n *\n * Zero LLM calls. All algorithmic.\n */\n async compact(params: {\n sessionId: string;\n sessionFile: string;\n tokenBudget?: number;\n force?: boolean;\n runtimeContext?: any;\n }): Promise<{\n ok: boolean;\n compacted: boolean;\n reason?: string;\n result?: {\n tokensBefore: number;\n tokensAfter?: number;\n };\n }> {\n if (!this.proxyUrl) {\n return { ok: false, compacted: false, reason: \"Proxy not available\" };\n }\n\n // Read current messages from session file if available\n // For now, compact() works in tandem with assemble() — the next assemble()\n // call will compress with the token budget. When compact() is called\n // independently, we report success since our pipeline handles it.\n //\n // TODO: Read session file, extract messages, call compress() with tokenBudget,\n // write back compacted messages.\n\n this.stats.compactions++;\n this.logger.info(\n `Compact called (budget: ${params.tokenBudget ?? \"none\"}, force: ${params.force ?? false})`,\n );\n\n return {\n ok: true,\n compacted: true,\n reason: \"Headroom applies SmartCrusher + Kompress + RollingWindow on next assemble()\",\n };\n }\n\n async afterTurn?(params: {\n sessionId: string;\n messages: any[];\n prePromptMessageCount: number;\n isHeartbeat?: boolean;\n }): Promise<void> {\n // Optional: could log stats or trigger learning\n }\n\n async prepareSubagentSpawn?(params: {\n parentSessionKey: string;\n childSessionKey: string;\n ttlMs?: number;\n }): Promise<{ rollback: () => Promise<void> } | undefined> {\n // Subagent context is compressed naturally via assemble()\n return undefined;\n }\n\n async onSubagentEnded?(params: {\n childSessionKey: string;\n reason: string;\n }): Promise<void> {\n // No-op\n }\n\n async dispose(): Promise<void> {\n await this.proxyManager.stop();\n this.logger.info(\n `Engine disposed. Stats: ${this.stats.totalCompressions} compressions, ` +\n `${this.stats.totalTokensSaved} tokens saved`,\n );\n }\n\n // --- Public API ---\n\n getStats() {\n return { ...this.stats };\n }\n\n getProxyUrl(): string | null {\n return this.proxyUrl;\n }\n}\n","/**\n * Manages the Headroom proxy process lifecycle.\n *\n * - Detects if a proxy is already running (e.g., user has `headroom proxy` for Claude Code)\n * - If not, spawns one as a child process with auto-assigned port\n * - Health checks, restart on crash, graceful shutdown\n */\n\nimport { spawn, type ChildProcess } from \"node:child_process\";\nimport { createWriteStream } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { homedir } from \"node:os\";\n\nconst DEFAULT_PORT = 8787;\nconst HEALTH_CHECK_INTERVAL_MS = 30_000;\nconst STARTUP_TIMEOUT_MS = 15_000;\nconst RESTART_DELAY_MS = 2_000;\nconst MAX_RESTART_ATTEMPTS = 3;\n\nexport interface ProxyManagerConfig {\n proxyUrl?: string;\n pythonPath?: string;\n autoStart?: boolean;\n proxyPort?: number;\n}\n\nexport interface ProxyManagerLogger {\n info(message: string): void;\n warn(message: string): void;\n error(message: string): void;\n debug(message: string): void;\n}\n\nconst defaultLogger: ProxyManagerLogger = {\n info: (m) => console.log(`[headroom] ${m}`),\n warn: (m) => console.warn(`[headroom] ${m}`),\n error: (m) => console.error(`[headroom] ${m}`),\n debug: () => {},\n};\n\nexport class ProxyManager {\n private config: ProxyManagerConfig;\n private logger: ProxyManagerLogger;\n private process: ChildProcess | null = null;\n private proxyUrl: string | null = null;\n private weStartedIt = false;\n private restartCount = 0;\n private healthInterval: ReturnType<typeof setInterval> | null = null;\n private disposed = false;\n\n constructor(config: ProxyManagerConfig = {}, logger?: ProxyManagerLogger) {\n this.config = config;\n this.logger = logger ?? defaultLogger;\n }\n\n /**\n * Ensure a proxy is available. Returns the URL.\n *\n * 1. If proxyUrl is configured, use it\n * 2. Check if proxy is already running on default port\n * 3. If autoStart, spawn one\n */\n async start(): Promise<string> {\n // Option 1: Explicit URL configured\n if (this.config.proxyUrl) {\n const url = this.config.proxyUrl.replace(/\\/+$/, \"\");\n if (await this.healthCheck(url)) {\n this.proxyUrl = url;\n this.logger.info(`Connected to proxy at ${url}`);\n return url;\n }\n throw new Error(`Headroom proxy not reachable at ${url}`);\n }\n\n // Option 2: Check default port\n const defaultUrl = `http://127.0.0.1:${DEFAULT_PORT}`;\n if (await this.healthCheck(defaultUrl)) {\n this.proxyUrl = defaultUrl;\n this.logger.info(`Found running proxy at ${defaultUrl}`);\n this.startHealthMonitor();\n return defaultUrl;\n }\n\n // Option 3: Auto-start\n if (this.config.autoStart !== false) {\n return this.spawnProxy();\n }\n\n throw new Error(\n \"Headroom proxy not running. Start with: headroom proxy --port 8787\\n\" +\n \"Or install: pip install 'headroom-ai[proxy]'\",\n );\n }\n\n /**\n * Spawn the headroom proxy as a child process.\n */\n private async spawnProxy(): Promise<string> {\n const pythonPath = await this.findPython();\n if (!pythonPath) {\n throw new Error(\n \"Python not found. Install Python 3.10+ and run: pip install 'headroom-ai[proxy]'\",\n );\n }\n\n // Check if headroom-ai is installed\n const installed = await this.checkHeadroomInstalled(pythonPath);\n if (!installed) {\n throw new Error(\n \"headroom-ai Python package not found.\\n\" +\n \"Install with: pip install 'headroom-ai[proxy]'\",\n );\n }\n\n const port = this.config.proxyPort ?? 0; // 0 = OS picks a free port\n const actualPort = port === 0 ? await this.findFreePort() : port;\n const url = `http://127.0.0.1:${actualPort}`;\n\n this.logger.info(`Starting proxy on port ${actualPort}...`);\n\n // Log file\n const logDir = join(homedir(), \".headroom\", \"logs\");\n const logPath = join(logDir, \"openclaw-proxy.log\");\n\n let logStream: ReturnType<typeof createWriteStream> | null = null;\n try {\n const { mkdirSync } = await import(\"node:fs\");\n mkdirSync(logDir, { recursive: true });\n logStream = createWriteStream(logPath, { flags: \"a\" });\n } catch {\n // Can't create log file — use /dev/null\n }\n\n const proc = spawn(\n pythonPath,\n [\"-m\", \"headroom.cli\", \"proxy\", \"--port\", String(actualPort)],\n {\n env: { ...process.env, PYTHONIOENCODING: \"utf-8\" },\n stdio: [\"ignore\", logStream ? \"pipe\" : \"ignore\", logStream ? \"pipe\" : \"ignore\"],\n detached: false,\n },\n );\n\n if (logStream) {\n proc.stdout?.pipe(logStream);\n proc.stderr?.pipe(logStream);\n }\n\n proc.on(\"exit\", (code) => {\n if (!this.disposed && this.weStartedIt) {\n this.logger.warn(`Proxy exited with code ${code}`);\n this.handleCrash();\n }\n });\n\n this.process = proc;\n this.weStartedIt = true;\n\n // Wait for healthy\n const healthy = await this.waitForHealthy(url, STARTUP_TIMEOUT_MS);\n if (!healthy) {\n proc.kill();\n this.process = null;\n throw new Error(\n `Proxy failed to start within ${STARTUP_TIMEOUT_MS / 1000}s. Check ${logPath}`,\n );\n }\n\n this.proxyUrl = url;\n this.logger.info(`Proxy started on port ${actualPort} (PID: ${proc.pid})`);\n this.startHealthMonitor();\n return url;\n }\n\n /**\n * Stop the proxy if we started it.\n */\n async stop(): Promise<void> {\n this.disposed = true;\n if (this.healthInterval) {\n clearInterval(this.healthInterval);\n this.healthInterval = null;\n }\n if (this.process && this.weStartedIt) {\n this.logger.info(\"Stopping proxy...\");\n this.process.kill(\"SIGTERM\");\n // Give it 3s to shutdown gracefully\n await new Promise<void>((resolve) => {\n const timeout = setTimeout(() => {\n this.process?.kill(\"SIGKILL\");\n resolve();\n }, 3000);\n this.process?.on(\"exit\", () => {\n clearTimeout(timeout);\n resolve();\n });\n });\n this.process = null;\n }\n }\n\n getUrl(): string | null {\n return this.proxyUrl;\n }\n\n // --- Internal ---\n\n private async healthCheck(url: string): Promise<boolean> {\n try {\n const resp = await fetch(`${url}/health`, {\n signal: AbortSignal.timeout(3000),\n });\n return resp.ok;\n } catch {\n return false;\n }\n }\n\n private async waitForHealthy(url: string, timeoutMs: number): Promise<boolean> {\n const start = Date.now();\n while (Date.now() - start < timeoutMs) {\n if (await this.healthCheck(url)) return true;\n await new Promise((r) => setTimeout(r, 500));\n }\n return false;\n }\n\n private startHealthMonitor(): void {\n if (this.healthInterval) return;\n this.healthInterval = setInterval(async () => {\n if (this.proxyUrl && !(await this.healthCheck(this.proxyUrl))) {\n this.logger.warn(\"Proxy health check failed\");\n if (this.weStartedIt) this.handleCrash();\n }\n }, HEALTH_CHECK_INTERVAL_MS);\n }\n\n private async handleCrash(): Promise<void> {\n if (this.disposed) return;\n if (this.restartCount >= MAX_RESTART_ATTEMPTS) {\n this.logger.error(`Proxy crashed ${MAX_RESTART_ATTEMPTS} times. Giving up.`);\n return;\n }\n this.restartCount++;\n this.logger.info(`Restarting proxy (attempt ${this.restartCount}/${MAX_RESTART_ATTEMPTS})...`);\n await new Promise((r) => setTimeout(r, RESTART_DELAY_MS));\n try {\n await this.spawnProxy();\n } catch (e) {\n this.logger.error(`Restart failed: ${e}`);\n }\n }\n\n private async findPython(): Promise<string | null> {\n if (this.config.pythonPath) return this.config.pythonPath;\n\n for (const cmd of [\"python3\", \"python\"]) {\n try {\n const { execSync } = await import(\"node:child_process\");\n const version = execSync(`${cmd} --version 2>&1`, { encoding: \"utf-8\" }).trim();\n if (version.includes(\"Python 3.\")) return cmd;\n } catch {\n continue;\n }\n }\n return null;\n }\n\n private async checkHeadroomInstalled(pythonPath: string): Promise<boolean> {\n try {\n const { execSync } = await import(\"node:child_process\");\n execSync(`${pythonPath} -c \"import headroom\"`, {\n encoding: \"utf-8\",\n stdio: \"pipe\",\n });\n return true;\n } catch {\n return false;\n }\n }\n\n private async findFreePort(): Promise<number> {\n const { createServer } = await import(\"node:net\");\n return new Promise((resolve, reject) => {\n const server = createServer();\n server.listen(0, () => {\n const addr = server.address();\n if (addr && typeof addr === \"object\") {\n const port = addr.port;\n server.close(() => resolve(port));\n } else {\n reject(new Error(\"Could not find free port\"));\n }\n });\n });\n }\n}\n","/**\n * Convert between OpenClaw's AgentMessage format and OpenAI message format.\n *\n * AgentMessage uses:\n * role: \"user\" | \"assistant\" | \"toolResult\"\n * content: string | ContentBlock[]\n *\n * OpenAI uses:\n * role: \"user\" | \"assistant\" | \"system\" | \"tool\"\n * content: string\n * tool_calls?: ToolCall[]\n * tool_call_id?: string\n */\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\n\nexport interface OpenAIMessage {\n role: string;\n content: string | null;\n tool_calls?: any[];\n tool_call_id?: string;\n name?: string;\n}\n\n/**\n * Convert AgentMessage[] to OpenAI message format for compression.\n */\nexport function agentToOpenAI(messages: any[]): OpenAIMessage[] {\n const result: OpenAIMessage[] = [];\n\n for (const msg of messages) {\n const role = msg.role;\n\n if (role === \"system\") {\n result.push({\n role: \"system\",\n content: typeof msg.content === \"string\" ? msg.content : extractText(msg.content),\n });\n continue;\n }\n\n if (role === \"user\") {\n result.push({\n role: \"user\",\n content: typeof msg.content === \"string\" ? msg.content : extractText(msg.content),\n });\n continue;\n }\n\n if (role === \"assistant\") {\n const content = msg.content;\n if (typeof content === \"string\") {\n result.push({ role: \"assistant\", content });\n continue;\n }\n\n // Content blocks: extract text and tool_use blocks\n if (Array.isArray(content)) {\n const textParts: string[] = [];\n const toolCalls: any[] = [];\n\n for (const block of content) {\n if (typeof block === \"string\") {\n textParts.push(block);\n } else if (block.type === \"text\") {\n textParts.push(block.text);\n } else if (block.type === \"tool_use\") {\n toolCalls.push({\n id: block.id,\n type: \"function\",\n function: {\n name: block.name,\n arguments:\n typeof block.input === \"string\"\n ? block.input\n : JSON.stringify(block.input ?? {}),\n },\n });\n }\n }\n\n const openaiMsg: OpenAIMessage = {\n role: \"assistant\",\n content: textParts.length > 0 ? textParts.join(\"\") : null,\n };\n if (toolCalls.length > 0) {\n openaiMsg.tool_calls = toolCalls;\n }\n result.push(openaiMsg);\n }\n continue;\n }\n\n if (role === \"toolResult\" || role === \"tool_result\") {\n const content =\n typeof msg.content === \"string\"\n ? msg.content\n : Array.isArray(msg.content)\n ? extractText(msg.content)\n : JSON.stringify(msg.content);\n\n result.push({\n role: \"tool\",\n content,\n tool_call_id: msg.tool_use_id ?? msg.toolCallId ?? msg.id ?? \"unknown\",\n });\n continue;\n }\n\n // Fallback: pass through as user message\n result.push({\n role: \"user\",\n content: typeof msg.content === \"string\" ? msg.content : JSON.stringify(msg.content),\n });\n }\n\n return result;\n}\n\n/**\n * Convert compressed OpenAI messages back to AgentMessage format.\n */\nexport function openAIToAgent(messages: OpenAIMessage[]): any[] {\n const result: any[] = [];\n\n for (const msg of messages) {\n if (msg.role === \"system\") {\n result.push({\n role: \"system\",\n content: msg.content ?? \"\",\n timestamp: Date.now(),\n });\n continue;\n }\n\n if (msg.role === \"user\") {\n result.push({\n role: \"user\",\n content: msg.content ?? \"\",\n timestamp: Date.now(),\n });\n continue;\n }\n\n if (msg.role === \"assistant\") {\n const blocks: any[] = [];\n if (msg.content) {\n blocks.push({ type: \"text\", text: msg.content });\n }\n if (msg.tool_calls) {\n for (const tc of msg.tool_calls) {\n let input: any;\n try {\n input = JSON.parse(tc.function.arguments);\n } catch {\n input = tc.function.arguments ?? {};\n }\n blocks.push({\n type: \"tool_use\",\n id: tc.id,\n name: tc.function.name,\n input,\n });\n }\n }\n // OpenClaw's Pi agent expects content to always be an array for assistant messages\n // (it calls .flatMap() on it). Never flatten to a string.\n result.push({\n role: \"assistant\",\n content: blocks,\n timestamp: Date.now(),\n });\n continue;\n }\n\n if (msg.role === \"tool\") {\n result.push({\n role: \"toolResult\",\n content: msg.content ?? \"\",\n tool_use_id: msg.tool_call_id ?? \"unknown\",\n timestamp: Date.now(),\n });\n continue;\n }\n }\n\n return result;\n}\n\n/**\n * Extract text from content blocks.\n */\nfunction extractText(content: any): string {\n if (typeof content === \"string\") return content;\n if (!Array.isArray(content)) return JSON.stringify(content);\n\n return content\n .map((block: any) => {\n if (typeof block === \"string\") return block;\n if (block.type === \"text\") return block.text;\n if (block.type === \"tool_result\") {\n return typeof block.content === \"string\" ? block.content : JSON.stringify(block.content);\n }\n return \"\";\n })\n .filter(Boolean)\n .join(\"\\n\");\n}\n","/**\n * CCR (Compress-Cache-Retrieve) tool for OpenClaw.\n *\n * Allows the agent to retrieve original uncompressed content\n * from the Headroom proxy's compression store.\n */\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\n\nexport interface RetrieveToolConfig {\n proxyUrl: string;\n}\n\nexport function createHeadroomRetrieveTool(config: RetrieveToolConfig) {\n return {\n name: \"headroom_retrieve\",\n description:\n \"Retrieve original uncompressed content from Headroom's compression store. \" +\n \"Use when compressed context mentions a hash and you need the full details. \" +\n \"Pass the hash from the compression marker (24 hex characters). \" +\n \"Optionally pass a query to search within the original content.\",\n parameters: {\n type: \"object\" as const,\n properties: {\n hash: {\n type: \"string\",\n description: \"The 24-character hex hash from the compression marker\",\n },\n query: {\n type: \"string\",\n description: \"Optional search query to filter results within the original content\",\n },\n },\n required: [\"hash\"],\n },\n execute: async (args: { hash: string; query?: string }): Promise<string> => {\n const { hash, query } = args;\n\n // Validate hash format\n if (!/^[a-f0-9]{24}$/i.test(hash)) {\n return JSON.stringify({\n error: \"Invalid hash format. Expected 24 hex characters.\",\n });\n }\n\n try {\n const url = query\n ? `${config.proxyUrl}/v1/retrieve/${hash}?query=${encodeURIComponent(query)}`\n : `${config.proxyUrl}/v1/retrieve/${hash}`;\n\n const resp = await fetch(url, {\n signal: AbortSignal.timeout(10_000),\n });\n\n if (!resp.ok) {\n const body = await resp.text().catch(() => \"\");\n return JSON.stringify({\n error: `Retrieval failed: HTTP ${resp.status}`,\n details: body,\n });\n }\n\n const data = await resp.json();\n return typeof data === \"string\" ? data : JSON.stringify(data);\n } catch (error) {\n return JSON.stringify({\n error: `Retrieval failed: ${error}`,\n hint: \"The compressed content may have expired (default TTL: 5 minutes)\",\n });\n }\n },\n };\n}\n","/**\n * Headroom OpenClaw Plugin — register ContextEngine + CCR retrieval tool.\n *\n * Usage:\n * openclaw plugins install @headroom-ai/openclaw\n *\n * Configuration (in ~/.openclaw/config.json or ~/.clawdbot/clawdbot.json):\n * {\n * \"plugins\": {\n * \"slots\": { \"contextEngine\": \"headroom\" },\n * \"entries\": { \"headroom\": { \"enabled\": true } }\n * }\n * }\n */\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\n\nimport { HeadroomContextEngine } from \"../engine.js\";\nimport { createHeadroomRetrieveTool } from \"../tools/headroom-retrieve.js\";\n\nexport default function headroomPlugin(api: any) {\n const config = api.config?.plugins?.entries?.headroom?.config ?? {};\n const logger = api.logger ?? console;\n\n const engine = new HeadroomContextEngine(config, {\n info: (m: string) => logger.info(m),\n warn: (m: string) => logger.warn(m),\n error: (m: string) => logger.error(m),\n debug: (m: string) => logger.debug?.(m),\n });\n\n // Register as context engine\n api.registerContextEngine(\"headroom\", () => engine);\n\n // Register CCR retrieval tool (active once proxy is running)\n api.registerTool((ctx: any) => {\n const proxyUrl = engine.getProxyUrl();\n if (!proxyUrl) return null;\n return createHeadroomRetrieveTool({ proxyUrl });\n });\n\n logger.info(\"[headroom] Plugin registered\");\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,41 @@
1
+ {
2
+ "id": "headroom",
3
+ "uiHints": {
4
+ "proxyUrl": {
5
+ "label": "Proxy URL",
6
+ "help": "URL of the Headroom proxy (auto-detected or auto-started if not set)"
7
+ },
8
+ "pythonPath": {
9
+ "label": "Python Path",
10
+ "help": "Path to Python binary (auto-detected if not set)"
11
+ },
12
+ "autoStart": {
13
+ "label": "Auto-Start Proxy",
14
+ "help": "Automatically start the Headroom proxy if not already running"
15
+ }
16
+ },
17
+ "configSchema": {
18
+ "type": "object",
19
+ "additionalProperties": false,
20
+ "properties": {
21
+ "enabled": {
22
+ "type": "boolean"
23
+ },
24
+ "proxyUrl": {
25
+ "type": "string"
26
+ },
27
+ "pythonPath": {
28
+ "type": "string"
29
+ },
30
+ "autoStart": {
31
+ "type": "boolean",
32
+ "default": true
33
+ },
34
+ "proxyPort": {
35
+ "type": "integer",
36
+ "minimum": 0,
37
+ "maximum": 65535
38
+ }
39
+ }
40
+ }
41
+ }
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "headroom-openclaw",
3
+ "version": "0.1.0",
4
+ "description": "Headroom context compression plugin for OpenClaw — 70-90% token savings with zero LLM calls",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "files": [
9
+ "dist",
10
+ "openclaw.plugin.json",
11
+ "README.md"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsup",
15
+ "test": "vitest run",
16
+ "test:watch": "vitest",
17
+ "typecheck": "tsc --noEmit"
18
+ },
19
+ "dependencies": {
20
+ "headroom-ai": "^0.1.0"
21
+ },
22
+ "peerDependencies": {
23
+ "openclaw": "*"
24
+ },
25
+ "peerDependenciesMeta": {
26
+ "openclaw": {
27
+ "optional": true
28
+ }
29
+ },
30
+ "devDependencies": {
31
+ "tsup": "^8.0.0",
32
+ "typescript": "^5.5.0",
33
+ "vitest": "^2.0.0"
34
+ },
35
+ "openclaw": {
36
+ "extensions": [
37
+ "./dist/index.js"
38
+ ]
39
+ },
40
+ "license": "Apache-2.0"
41
+ }