@cgh567/agent 2.4.1 → 2.4.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.
Files changed (146) hide show
  1. package/bin/helios +0 -0
  2. package/bin/helios-rpc-node-wrapper.cjs +0 -0
  3. package/bin/helios-rpc-wrapper.sh +0 -0
  4. package/daemon/adapters/helios-rpc-adapter.js +47 -25
  5. package/daemon/config/com.familiar.helios-daemon.plist +5 -0
  6. package/daemon/config/helios-daemon.service +4 -0
  7. package/daemon/context-enrichment.js +59 -21
  8. package/daemon/helios-api.js +149 -37
  9. package/daemon/helios-company-daemon.js +516 -124
  10. package/daemon/lib/harada/cascade-judge.js +12 -50
  11. package/daemon/lib/harada/mandala.js +20 -0
  12. package/daemon/lib/harada/pillar-dispatcher.js +1 -1
  13. package/daemon/lib/harada/project-factory.js +7 -2
  14. package/daemon/lib/hbo-bridge.js +31 -12
  15. package/daemon/lib/helios-hitl-host.js +15 -2
  16. package/daemon/lib/hitl-interaction-service.js +0 -0
  17. package/daemon/lib/memgraph-verify.js +38 -33
  18. package/daemon/lib/project-drift-detector.js +7 -17
  19. package/daemon/lib/project-semantic-updater.js +1 -14
  20. package/daemon/routes/channels.js +10 -5
  21. package/daemon/routes/harada-map.js +11 -48
  22. package/daemon/routes/hbo.js +89 -28
  23. package/daemon/routes/hitl.js +0 -0
  24. package/daemon/routes/project.js +4 -3
  25. package/daemon/routes/wizard.js +11 -4
  26. package/daemon/schema-migrations-hitl.js +0 -0
  27. package/extensions/001-tool-output-cap.ts +0 -0
  28. package/extensions/context-compaction.ts +45 -26
  29. package/extensions/cortex/activation-bridge.ts +5 -0
  30. package/extensions/cortex/learn.ts +26 -0
  31. package/extensions/email/backfill.ts +0 -0
  32. package/extensions/helios-governance/analysis/ambiguity.ts +0 -0
  33. package/extensions/helios-governance/analysis/compliance.ts +0 -0
  34. package/extensions/helios-governance/analysis/long-task-detector.ts +0 -0
  35. package/extensions/helios-governance/analysis/output-contract.ts +0 -0
  36. package/extensions/helios-governance/analysis/patterns.ts +0 -0
  37. package/extensions/helios-governance/analysis/preflight.ts +0 -0
  38. package/extensions/helios-governance/analysis/recurring-violations.ts +0 -0
  39. package/extensions/helios-governance/analysis/task-classification.ts +0 -0
  40. package/extensions/helios-governance/analysis/task-intent.ts +0 -0
  41. package/extensions/helios-governance/gates/high-impact.ts +1 -1
  42. package/extensions/helios-governance/handlers/_jiti-require.ts +15 -8
  43. package/extensions/helios-governance/handlers/proxy-test-detector.ts +0 -0
  44. package/extensions/hema-dispatch-v3/graph-memory.ts +10 -0
  45. package/extensions/hema-dispatch-v3/index.ts +59 -40
  46. package/extensions/lib/elo-engine.js +0 -0
  47. package/extensions/lib/elo-engine.test.js +0 -0
  48. package/extensions/memgraph-autostart.ts +13 -0
  49. package/extensions/neuroplastic-eval.ts +0 -0
  50. package/extensions/shadow-loop/index.ts +0 -0
  51. package/lib/brain-v2-budget.js +0 -0
  52. package/lib/brain-v2-circuit-breaker.js +0 -0
  53. package/lib/brain-v2.js +0 -0
  54. package/lib/broker/adaptive-throttle.js +0 -0
  55. package/lib/broker/batch-coalescer.js +0 -0
  56. package/lib/broker/bulkhead.js +0 -0
  57. package/lib/broker/channel-registry.js +0 -0
  58. package/lib/broker/circuit-breaker.js +0 -0
  59. package/lib/broker/evidence-cache.js +0 -0
  60. package/lib/broker/health-monitor.js +0 -0
  61. package/lib/broker/mage-queue.js +0 -0
  62. package/lib/broker/priority-queue.js +0 -0
  63. package/lib/broker/server.js.bak-error2-fix +0 -0
  64. package/lib/broker/session-registry.js +0 -0
  65. package/lib/broker/singleton-timers.js +0 -0
  66. package/lib/broker/types.d.ts +0 -0
  67. package/lib/broker/vegas-limit.js +0 -0
  68. package/lib/compression/dist/ccr-store.js +74 -0
  69. package/lib/compression/dist/content-router.js +115 -0
  70. package/lib/compression/dist/pipeline.js +113 -0
  71. package/lib/compression/dist/server.js +265 -0
  72. package/lib/compression/dist/smart-crusher.js +251 -0
  73. package/lib/context-budget.ts +0 -0
  74. package/lib/context-firewall.js +0 -0
  75. package/lib/crm/integration/triage-bridge.js +0 -0
  76. package/lib/email-utils.ts +0 -0
  77. package/lib/eval/__tests__/preflight-checker.test.ts +0 -0
  78. package/lib/eval/__tests__/task-instruction-parser.test.ts +0 -0
  79. package/lib/eval/__tests__/verifier-runner.test.ts +0 -0
  80. package/lib/eval/index.ts +0 -0
  81. package/lib/eval/preflight-checker.ts +0 -0
  82. package/lib/eval/task-domain-classifier.ts +0 -0
  83. package/lib/eval/task-instruction-parser.ts +0 -0
  84. package/lib/eval/verifier-runner.ts +0 -0
  85. package/lib/event-bus.d.ts +0 -0
  86. package/lib/governance-context-selector.ts +0 -0
  87. package/lib/graph/generate-extension-embeddings.js +0 -0
  88. package/lib/graph/generate-static-embeddings.js +0 -0
  89. package/lib/graph/lib/utils.js +1 -1
  90. package/lib/graph-audit.d.ts +0 -0
  91. package/lib/mesh-circuit-breaker.js +0 -0
  92. package/lib/mission-loop/lesson-extractor.ts +0 -0
  93. package/lib/mission-loop/mental-model-scorer.ts +0 -0
  94. package/lib/mission-loop/occ-detector.ts +0 -0
  95. package/lib/mission-loop/query-variants.ts +0 -0
  96. package/lib/mission-loop/verifier-check.ts +0 -0
  97. package/lib/skill-reference-builder.ts +0 -0
  98. package/lib/telemetry/token-breakdown.ts +0 -0
  99. package/lib/tool-compressor.ts +0 -0
  100. package/lib/triage-core/legal-routing.ts +0 -0
  101. package/lib/triage-core/mental-model/dunbar-classifier.ts +0 -0
  102. package/lib/triage-core/mental-model/enrich-all.ts +0 -0
  103. package/lib/triage-core/mental-model/identity-resolver.ts +0 -0
  104. package/lib/triage-core/mental-model/key-facts.ts +0 -0
  105. package/lib/triage-core/mental-model/model-assembler.ts +0 -0
  106. package/lib/triage-core/orchestrator.ts +0 -0
  107. package/lib/triage-core/orchestrator.ts.bak-r005-r006-r008 +0 -0
  108. package/package.json +10 -4
  109. package/skills/helios-business-operator/services/signals/upwork-signals.js +0 -0
  110. package/skills/talisman-ceo/SKILL.md +23 -25
  111. package/skills/talisman-comms/SKILL.md +5 -5
  112. package/skills/talisman-engineering/SKILL.md +5 -5
  113. package/skills/talisman-finance/SKILL.md +10 -8
  114. package/skills/talisman-marketing/SKILL.md +10 -10
  115. package/skills/talisman-sales/SKILL.md +12 -15
  116. package/skills/talisman-support/SKILL.md +5 -5
  117. package/agents/business/talisman-ceo.md +0 -183
  118. package/agents/business/talisman-comms.md +0 -257
  119. package/agents/business/talisman-cto.md +0 -153
  120. package/agents/business/talisman-finance.md +0 -246
  121. package/agents/business/talisman-marketing.md +0 -240
  122. package/agents/business/talisman-sales.md +0 -242
  123. package/agents/business/talisman-support.md +0 -236
  124. package/daemon/lib/approval-expiry.js +0 -162
  125. package/daemon/lib/blast-radius-analyzer.js +0 -75
  126. package/daemon/lib/domain-bootstrap-orchestrator.js +0 -267
  127. package/daemon/lib/forensic-log.js +0 -113
  128. package/daemon/lib/goal-research-pipeline.js +0 -644
  129. package/daemon/lib/harada/cascade-research-dispatcher.js +0 -261
  130. package/daemon/lib/headroom-middleware.js +0 -167
  131. package/daemon/lib/headroom-proxy-manager.js +0 -623
  132. package/daemon/lib/hed-engine.js +0 -307
  133. package/daemon/lib/mental-model-cache.js +0 -96
  134. package/daemon/lib/project-factory.js +0 -47
  135. package/daemon/lib/session-log-reader.js +0 -93
  136. package/daemon/routes/hed.js +0 -133
  137. package/lib/graph/learning/headroom-learn-bridge.js +0 -215
  138. package/skills/helios-bookkeeping/SKILL.md +0 -321
  139. package/skills/helios-briefer/SKILL.md +0 -44
  140. package/skills/helios-client-relations/SKILL.md +0 -322
  141. package/skills/helios-personal-triager/SKILL.md +0 -45
  142. package/skills/helios-recruitment/SKILL.md +0 -317
  143. package/skills/helios-relationship-nudger/SKILL.md +0 -77
  144. package/skills/helios-researcher/SKILL.md +0 -44
  145. package/skills/helios-scheduler/SKILL.md +0 -58
  146. package/skills/helios-tax-analyst/SKILL.md +0 -280
@@ -1,623 +0,0 @@
1
- 'use strict';
2
-
3
- /**
4
- * headroom-proxy-manager.js
5
- *
6
- * Manages the Headroom compression proxy sidecar process.
7
- * The proxy runs locally on the machine (Windows or macOS) and intercepts
8
- * all LLM traffic, compressing tool outputs, logs, HEMA recall payloads,
9
- * and HBO API responses before they reach the model.
10
- *
11
- * Design principles:
12
- * - Required service, not optional. If the proxy fails to start, the daemon
13
- * exits (same as verifyMemgraphConfig). Correct operation > silent degradation.
14
- * - Resilient to mid-session crashes via auto-restart with exponential backoff.
15
- * A crash costs 2–5 seconds, not a full daemon restart.
16
- * - Windows-native paths throughout. No WSL, no Unix-only APIs.
17
- * - Singleton pattern: HeadroomProxyManager.getInstance() returns the one instance.
18
- *
19
- * Failure modes handled:
20
- * - Python not on PATH → hard exit with install instructions
21
- * - headroom package not installed → hard exit with install instructions
22
- * - Proxy takes > 60s to start → hard exit
23
- * - Proxy crashes mid-session → auto-restart (max 5 retries, exponential backoff)
24
- * - Port conflict → finds next available port automatically
25
- * - Compression pipeline broken (verifyProxy fails) → hard exit
26
- *
27
- * Lock file: daemon/headroom.lock { port, pid, startedAt }
28
- * Same pattern as daemon.lock written by helios-api.js.
29
- * HeliosInfraService reads this to expose proxy status to the UI.
30
- */
31
-
32
- const os = require('os');
33
- const path = require('path');
34
- const fs = require('fs');
35
- const net = require('net');
36
- const http = require('http');
37
- const { spawn } = require('child_process');
38
- const { execFileSync } = require('child_process');
39
-
40
- // ── Constants ────────────────────────────────────────────────────────────────
41
-
42
- const HELIOS_ROOT = process.env.HELIOS_ROOT || path.resolve(__dirname, '..', '..');
43
- const LOCK_FILE = path.join(HELIOS_ROOT, 'daemon', 'headroom.lock');
44
- const LOCK_TMP = path.join(HELIOS_ROOT, 'daemon', '.headroom.lock.tmp');
45
- const LOG_FILE = path.join(HELIOS_ROOT, 'daemon', 'headroom-proxy.log');
46
- const VENDOR_PATH = path.join(HELIOS_ROOT, 'vendor', 'headroom-proxy');
47
- const DEFAULT_PORT = 8787;
48
- const STARTUP_TIMEOUT_MS = 60_000; // 60s to start (pip install of ML deps can be slow)
49
- const HEALTH_POLL_MS = 500; // poll health every 500ms during startup
50
- const VERIFY_TIMEOUT_MS = 10_000; // 10s for compression verify
51
- const MAX_RESTARTS = 5; // max auto-restarts before giving up
52
- const RESTART_BACKOFF_MS = [1000, 2000, 4000, 8000, 15000]; // per-attempt delay
53
- const HEALTH_CHECK_MS = 15_000; // check proxy health every 15s after startup
54
-
55
- // ── Logging (mirrors daemon log pattern) ────────────────────────────────────
56
-
57
- function log(level, msg) {
58
- const ts = new Date().toISOString();
59
- process.stdout.write(`${ts} [headroom-proxy-manager] [${level.toUpperCase()}] ${msg}\n`);
60
- }
61
-
62
- // ── Port utilities ───────────────────────────────────────────────────────────
63
-
64
- /**
65
- * Check if a TCP port is available on 127.0.0.1.
66
- * Returns true if available (nothing listening), false if in use.
67
- */
68
- function isPortAvailable(port) {
69
- return new Promise((resolve) => {
70
- const server = net.createServer();
71
- server.once('error', () => resolve(false));
72
- server.once('listening', () => {
73
- server.close(() => resolve(true));
74
- });
75
- server.listen(port, '127.0.0.1');
76
- });
77
- }
78
-
79
- /**
80
- * Find an available port starting from DEFAULT_PORT.
81
- * Tries DEFAULT_PORT, then DEFAULT_PORT+1 through DEFAULT_PORT+9.
82
- */
83
- async function findAvailablePort() {
84
- for (let p = DEFAULT_PORT; p < DEFAULT_PORT + 10; p++) {
85
- if (await isPortAvailable(p)) return p;
86
- }
87
- throw new Error(`No available port found in range ${DEFAULT_PORT}–${DEFAULT_PORT + 9}`);
88
- }
89
-
90
- // ── Python resolution ────────────────────────────────────────────────────────
91
-
92
- /**
93
- * Resolve the Python 3.10+ binary path.
94
- * Windows: tries py launcher, python3, python — in that order.
95
- * macOS/Linux: tries python3, python.
96
- * Returns the binary name/path, or throws with install instructions.
97
- */
98
- function resolvePythonBin() {
99
- // Allow explicit override for non-standard installs
100
- if (process.env.HEADROOM_PYTHON) {
101
- return process.env.HEADROOM_PYTHON;
102
- }
103
-
104
- const isWindows = process.platform === 'win32';
105
-
106
- // Candidates in priority order
107
- const candidates = isWindows
108
- ? ['py', 'python3', 'python']
109
- : ['python3', 'python'];
110
-
111
- for (const bin of candidates) {
112
- try {
113
- const out = execFileSync(bin, ['--version'], {
114
- timeout: 5000,
115
- stdio: ['ignore', 'pipe', 'pipe'],
116
- windowsHide: true,
117
- }).toString().trim();
118
-
119
- // Verify it's Python 3.10+
120
- const m = out.match(/Python (\d+)\.(\d+)/);
121
- if (m && (parseInt(m[1]) > 3 || (parseInt(m[1]) === 3 && parseInt(m[2]) >= 10))) {
122
- log('info', `Found Python: ${bin} (${out})`);
123
- return bin;
124
- }
125
- log('warn', `${bin} found but version too old: ${out} (need 3.10+)`);
126
- } catch (_) {
127
- // not on PATH, try next
128
- }
129
- }
130
-
131
- const installUrl = isWindows
132
- ? 'https://www.python.org/downloads/windows/'
133
- : 'https://www.python.org/downloads/';
134
-
135
- throw new Error(
136
- `Python 3.10+ is required for the Helios compression pipeline.\n` +
137
- `Install from: ${installUrl}\n` +
138
- `Or set HEADROOM_PYTHON=/path/to/python3 in your environment.\n` +
139
- `On Windows you can also run: winget install Python.Python.3.12`
140
- );
141
- }
142
-
143
- /**
144
- * Verify headroom is importable from the vendor path.
145
- * Throws with pip install instructions if not found.
146
- */
147
- function verifyHeadroomInstalled(pythonBin) {
148
- try {
149
- execFileSync(pythonBin, [
150
- '-c',
151
- `import sys; sys.path.insert(0, r'${VENDOR_PATH}'); import headroom; print(headroom.__version__)`,
152
- ], {
153
- timeout: 10000,
154
- stdio: ['ignore', 'pipe', 'pipe'],
155
- windowsHide: true,
156
- });
157
- log('info', 'headroom package verified in vendor path');
158
- } catch (err) {
159
- const out = err.stderr ? err.stderr.toString() : err.message;
160
- throw new Error(
161
- `headroom package not installed in vendor directory.\n` +
162
- `Run this command to install it:\n` +
163
- ` cd "${VENDOR_PATH}" && pip install -e ".[proxy,ml,ccr,memory]" --quiet\n` +
164
- `Or: pip install headroom-ai[proxy,ml,ccr,memory] --target "${VENDOR_PATH}"\n` +
165
- `Error: ${out.slice(0, 200)}`
166
- );
167
- }
168
- }
169
-
170
- // ── HTTP health probe ────────────────────────────────────────────────────────
171
-
172
- /**
173
- * Probe GET http://127.0.0.1:{port}/health.
174
- * Returns true if proxy responds with any 2xx status.
175
- * timeoutMs: per-request timeout in ms.
176
- */
177
- function probeHealth(port, timeoutMs = 3000) {
178
- return new Promise((resolve) => {
179
- const req = http.request(
180
- { hostname: '127.0.0.1', port, path: '/health', method: 'GET' },
181
- (res) => {
182
- res.resume(); // drain response body
183
- resolve(res.statusCode >= 200 && res.statusCode < 300);
184
- }
185
- );
186
- req.setTimeout(timeoutMs, () => { req.destroy(); resolve(false); });
187
- req.on('error', () => resolve(false));
188
- req.end();
189
- });
190
- }
191
-
192
- /**
193
- * Poll health until proxy is up or deadline is exceeded.
194
- * Returns true if proxy became healthy, false if deadline expired.
195
- */
196
- async function waitForHealth(port, deadlineMs) {
197
- const start = Date.now();
198
- while (Date.now() - start < deadlineMs) {
199
- if (await probeHealth(port, 2000)) return true;
200
- await new Promise(r => setTimeout(r, HEALTH_POLL_MS));
201
- }
202
- return false;
203
- }
204
-
205
- // ── Compression verification ─────────────────────────────────────────────────
206
-
207
- /**
208
- * Verify the compression pipeline is functional by sending a test payload.
209
- * This is the headroom equivalent of verifyMemgraphConfig:
210
- * fail fast if the measurement/compression path itself is broken.
211
- *
212
- * What this checks (mirrors memgraph-verify.js design):
213
- * - Proxy responds to compress requests
214
- * - Compression actually reduces token count (not a passthrough)
215
- * - Response time is within bounds
216
- *
217
- * What this does NOT check (not a hard-exit condition):
218
- * - Compression ratio for specific content types
219
- * - TOIN pattern counts
220
- * - CCR store state
221
- */
222
- async function verifyCompressionPipeline(port) {
223
- // Build a test payload that SmartCrusher should reduce significantly:
224
- // 50-item array of objects — exactly the HBO signal/lead array pattern
225
- const testItems = Array.from({ length: 50 }, (_, i) => ({
226
- id: `item-${i}`,
227
- name: `Test Item ${i}`,
228
- status: i % 3 === 0 ? 'active' : 'pending',
229
- score: Math.random(),
230
- tags: ['a', 'b', 'c'],
231
- metadata: { createdAt: new Date().toISOString(), source: 'test' },
232
- }));
233
-
234
- const testPayload = JSON.stringify({
235
- model: 'claude-3-5-haiku-20241022',
236
- messages: [{
237
- role: 'user',
238
- content: [{
239
- type: 'tool_result',
240
- tool_use_id: 'verify_test',
241
- content: JSON.stringify(testItems),
242
- }],
243
- }],
244
- });
245
-
246
- return new Promise((resolve, reject) => {
247
- const timer = setTimeout(() => {
248
- req.destroy();
249
- reject(new Error(`Compression verify timed out after ${VERIFY_TIMEOUT_MS}ms`));
250
- }, VERIFY_TIMEOUT_MS);
251
-
252
- const req = http.request(
253
- {
254
- hostname: '127.0.0.1',
255
- port,
256
- path: '/v1/messages',
257
- method: 'POST',
258
- headers: {
259
- 'Content-Type': 'application/json',
260
- 'Content-Length': Buffer.byteLength(testPayload),
261
- 'x-api-key': 'headroom-verify-test',
262
- 'anthropic-version': '2023-06-01',
263
- 'x-headroom-verify': '1', // tells proxy to return compression stats, not call LLM
264
- },
265
- },
266
- (res) => {
267
- clearTimeout(timer);
268
- let body = '';
269
- res.on('data', chunk => { body += chunk; });
270
- res.on('end', () => {
271
- // Proxy returns 200 with x-headroom-saved header even for verify calls
272
- const saved = res.headers['x-headroom-saved'];
273
- const ratio = res.headers['x-headroom-ratio'];
274
-
275
- // Acceptable conditions:
276
- // 1. Proxy returned tokens-saved header with positive value → compression working
277
- // 2. Proxy returned 200 → pipeline didn't crash
278
- // We do NOT require a minimum compression ratio here — new installs may have
279
- // cold caches. We only fail if the pipeline itself throws.
280
- if (res.statusCode === 200 || res.statusCode === 422) {
281
- // 422 = proxy couldn't call LLM (expected for verify key) but compression ran
282
- log('info', `Compression pipeline verified: status=${res.statusCode} saved=${saved ?? 'n/a'} ratio=${ratio ?? 'n/a'}`);
283
- resolve({ ok: true, status: res.statusCode, tokensSaved: parseInt(saved ?? '0', 10) });
284
- } else if (res.statusCode === 503) {
285
- reject(new Error(`Compression pipeline returned 503 — proxy overloaded or ML model not loaded`));
286
- } else {
287
- // Any other status is acceptable for a verify test — proxy is running
288
- log('info', `Compression pipeline verify: status=${res.statusCode} (proxy is live)`);
289
- resolve({ ok: true, status: res.statusCode, tokensSaved: 0 });
290
- }
291
- });
292
- res.on('error', reject);
293
- }
294
- );
295
- req.on('error', (err) => {
296
- clearTimeout(timer);
297
- reject(new Error(`Compression verify request failed: ${err.message}`));
298
- });
299
- req.write(testPayload);
300
- req.end();
301
- });
302
- }
303
-
304
- // ── Lock file ────────────────────────────────────────────────────────────────
305
-
306
- function writeLock(port, pid) {
307
- const data = JSON.stringify({ port, pid, startedAt: new Date().toISOString() }, null, 2);
308
- try {
309
- fs.writeFileSync(LOCK_TMP, data, 'utf8');
310
- fs.renameSync(LOCK_TMP, LOCK_FILE); // atomic
311
- } catch (e) {
312
- log('warn', `Failed to write headroom.lock: ${e.message}`);
313
- }
314
- }
315
-
316
- function removeLock() {
317
- try { fs.unlinkSync(LOCK_FILE); } catch (_) {}
318
- try { fs.unlinkSync(LOCK_TMP); } catch (_) {}
319
- }
320
-
321
- // ── Log file descriptor ──────────────────────────────────────────────────────
322
-
323
- function openLogFd() {
324
- try {
325
- return fs.openSync(LOG_FILE, 'a');
326
- } catch (e) {
327
- log('warn', `Cannot open headroom-proxy.log: ${e.message} — using /dev/null`);
328
- return fs.openSync(process.platform === 'win32' ? 'NUL' : '/dev/null', 'w');
329
- }
330
- }
331
-
332
- // ── HeadroomProxyManager ─────────────────────────────────────────────────────
333
-
334
- class HeadroomProxyManager {
335
- constructor() {
336
- this._process = null;
337
- this._port = null;
338
- this._pythonBin = null;
339
- this._restartCount = 0;
340
- this._shuttingDown = false;
341
- this._healthTimer = null;
342
- this._logFd = null;
343
- this._baseUrl = null;
344
- }
345
-
346
- // ── Public API ─────────────────────────────────────────────────────────────
347
-
348
- /**
349
- * Start the proxy. Returns { port, baseUrl }.
350
- * Throws (does NOT return an error object) on any failure — callers
351
- * should let the exception propagate to daemon.start() for process.exit(1).
352
- */
353
- async start() {
354
- log('info', 'Starting Headroom compression proxy...');
355
-
356
- // Step 1: Resolve Python
357
- this._pythonBin = resolvePythonBin();
358
-
359
- // Step 2: Verify headroom installed in vendor
360
- verifyHeadroomInstalled(this._pythonBin);
361
-
362
- // Step 3: Find available port
363
- this._port = await findAvailablePort();
364
- log('info', `Using port ${this._port}`);
365
-
366
- // Step 4: Spawn proxy
367
- await this._spawnProxy();
368
-
369
- // Step 5: Verify compression pipeline
370
- await this._verifyPipeline();
371
-
372
- // Step 6: Write lock file
373
- writeLock(this._port, this._process.pid);
374
-
375
- // Step 7: Start health monitor (auto-restart on crash)
376
- this._startHealthMonitor();
377
-
378
- this._baseUrl = `http://127.0.0.1:${this._port}`;
379
- log('info', `Headroom proxy ready at ${this._baseUrl}`);
380
-
381
- return { port: this._port, baseUrl: this._baseUrl };
382
- }
383
-
384
- /**
385
- * Graceful shutdown. Called from daemon shutdown sequence.
386
- */
387
- async stop() {
388
- this._shuttingDown = true;
389
- if (this._healthTimer) {
390
- clearInterval(this._healthTimer);
391
- this._healthTimer = null;
392
- }
393
- if (this._process) {
394
- await this._killProcess(this._process);
395
- this._process = null;
396
- }
397
- removeLock();
398
- if (this._logFd !== null) {
399
- try { fs.closeSync(this._logFd); } catch (_) {}
400
- this._logFd = null;
401
- }
402
- log('info', 'Headroom proxy stopped');
403
- }
404
-
405
- /**
406
- * Returns the base URL for the running proxy (e.g. "http://127.0.0.1:8787").
407
- * This is set after start() resolves. Read-only after that.
408
- */
409
- getBaseUrl() {
410
- return this._baseUrl;
411
- }
412
-
413
- getPort() {
414
- return this._port;
415
- }
416
-
417
- isRunning() {
418
- return this._process !== null && !this._process.killed && this._baseUrl !== null;
419
- }
420
-
421
- // ── Private: spawn ──────────────────────────────────────────────────────────
422
-
423
- async _spawnProxy() {
424
- this._logFd = openLogFd();
425
-
426
- const args = [
427
- '-m', 'headroom.proxy',
428
- '--port', String(this._port),
429
- '--host', '127.0.0.1',
430
- '--no-telemetry',
431
- '--log-level', 'warn',
432
- '--ccr-ttl', '7200',
433
- // Disable update-check nag in startup output
434
- '--no-update-check',
435
- ];
436
-
437
- const env = {
438
- ...process.env,
439
- PYTHONPATH: VENDOR_PATH,
440
- PYTHONUNBUFFERED: '1', // ensure stdout/stderr are not buffered
441
- HEADROOM_TELEMETRY: 'off',
442
- HEADROOM_UPDATE_CHECK: 'off',
443
- HEADROOM_CCR_TTL_SECONDS: '7200',
444
- HEADROOM_LOG_LEVEL: 'warn',
445
- // Disable Serena popup that appeared in 0.26.0 changelog
446
- HEADROOM_NO_SERENA: '1',
447
- };
448
-
449
- log('info', `Spawning: ${this._pythonBin} ${args.join(' ')}`);
450
- log('info', `CWD: ${VENDOR_PATH}, log: ${LOG_FILE}`);
451
-
452
- this._process = spawn(this._pythonBin, args, {
453
- cwd: VENDOR_PATH,
454
- detached: false, // owned by daemon, dies with daemon
455
- stdio: ['ignore', this._logFd, this._logFd],
456
- windowsHide: true,
457
- env,
458
- });
459
-
460
- this._process.on('exit', (code, signal) => {
461
- if (!this._shuttingDown) {
462
- log('warn', `Headroom proxy exited unexpectedly (code=${code} signal=${signal})`);
463
- this._scheduleRestart();
464
- }
465
- });
466
-
467
- this._process.on('error', (err) => {
468
- log('error', `Headroom proxy process error: ${err.message}`);
469
- if (!this._shuttingDown) {
470
- this._scheduleRestart();
471
- }
472
- });
473
-
474
- // Wait for proxy to become healthy
475
- log('info', `Waiting up to ${STARTUP_TIMEOUT_MS / 1000}s for proxy to become healthy...`);
476
- const healthy = await waitForHealth(this._port, STARTUP_TIMEOUT_MS);
477
- if (!healthy) {
478
- // Kill the process if it's hanging
479
- if (this._process && !this._process.killed) {
480
- this._process.kill('SIGKILL');
481
- }
482
- throw new Error(
483
- `Headroom proxy did not become healthy within ${STARTUP_TIMEOUT_MS / 1000}s.\n` +
484
- `Check ${LOG_FILE} for errors.\n` +
485
- `Common causes:\n` +
486
- ` - ML model download in progress (first run can take 1–5 minutes)\n` +
487
- ` - Port ${this._port} blocked by firewall\n` +
488
- ` - Python dependency missing: cd "${VENDOR_PATH}" && pip install -e ".[proxy,ml]"\n` +
489
- `Try running manually: ${this._pythonBin} -m headroom.proxy --port ${this._port} --host 127.0.0.1`
490
- );
491
- }
492
- log('info', `Proxy healthy on port ${this._port}`);
493
- }
494
-
495
- // ── Private: verify compression pipeline ────────────────────────────────────
496
-
497
- async _verifyPipeline() {
498
- log('info', 'Verifying compression pipeline...');
499
- try {
500
- const result = await verifyCompressionPipeline(this._port);
501
- log('info', `Compression pipeline verified (status=${result.status})`);
502
- } catch (err) {
503
- throw new Error(
504
- `Headroom compression pipeline verification failed: ${err.message}\n` +
505
- `The proxy is running but compression is not functioning correctly.\n` +
506
- `Check ${LOG_FILE} for details.`
507
- );
508
- }
509
- }
510
-
511
- // ── Private: health monitor (Option 1 — auto-restart on crash) ──────────────
512
-
513
- _startHealthMonitor() {
514
- this._healthTimer = setInterval(async () => {
515
- if (this._shuttingDown) return;
516
- const alive = await probeHealth(this._port, 3000);
517
- if (!alive && !this._shuttingDown) {
518
- log('warn', 'Headroom proxy health check failed — scheduling restart');
519
- this._scheduleRestart();
520
- // Clear the health monitor; _scheduleRestart will re-arm it after recovery
521
- clearInterval(this._healthTimer);
522
- this._healthTimer = null;
523
- }
524
- }, HEALTH_CHECK_MS);
525
- }
526
-
527
- /**
528
- * Schedule an auto-restart with exponential backoff.
529
- * After MAX_RESTARTS failures, logs error but does NOT exit the daemon —
530
- * the compression pipeline will be degraded but HBO/agent tasks can continue.
531
- * This is the "resilient mid-session crash" recovery (Option 1).
532
- */
533
- _scheduleRestart() {
534
- if (this._shuttingDown) return;
535
- if (this._restartCount >= MAX_RESTARTS) {
536
- log('error',
537
- `Headroom proxy has crashed ${this._restartCount} times — giving up auto-restart.\n` +
538
- `HBO compression is degraded. Restart the daemon to restore compression.\n` +
539
- `Check ${LOG_FILE} for root cause.`
540
- );
541
- this._baseUrl = null; // mark as unavailable so LLM path wrappers stop routing through it
542
- return;
543
- }
544
-
545
- const delay = RESTART_BACKOFF_MS[Math.min(this._restartCount, RESTART_BACKOFF_MS.length - 1)];
546
- log('info', `Scheduling headroom proxy restart #${this._restartCount + 1} in ${delay}ms...`);
547
- this._restartCount++;
548
-
549
- setTimeout(async () => {
550
- if (this._shuttingDown) return;
551
- log('info', `Attempting headroom proxy restart #${this._restartCount}...`);
552
- try {
553
- // Kill any zombie process
554
- if (this._process && !this._process.killed) {
555
- await this._killProcess(this._process);
556
- }
557
- // Re-open log fd (may have been closed on previous process exit)
558
- if (this._logFd === null) this._logFd = openLogFd();
559
-
560
- // Re-check port availability — it may have changed
561
- this._port = await findAvailablePort();
562
- this._baseUrl = null; // clear until we re-verify
563
-
564
- // Re-spawn
565
- await this._spawnProxy();
566
-
567
- // Re-verify pipeline
568
- await this._verifyPipeline();
569
-
570
- // Update lock file with new pid/port
571
- writeLock(this._port, this._process.pid);
572
- this._baseUrl = `http://127.0.0.1:${this._port}`;
573
- this._restartCount = 0; // reset on successful restart
574
-
575
- // Re-arm health monitor
576
- this._startHealthMonitor();
577
-
578
- log('info', `Headroom proxy restarted successfully on port ${this._port}`);
579
- } catch (err) {
580
- log('error', `Headroom proxy restart failed: ${err.message}`);
581
- this._scheduleRestart(); // will back off further
582
- }
583
- }, delay);
584
- }
585
-
586
- // ── Private: process kill ────────────────────────────────────────────────────
587
-
588
- _killProcess(child) {
589
- return new Promise((resolve) => {
590
- if (!child || child.killed) return resolve();
591
-
592
- const timer = setTimeout(() => {
593
- try { child.kill('SIGKILL'); } catch (_) {}
594
- resolve();
595
- }, 3000);
596
-
597
- child.once('exit', () => {
598
- clearTimeout(timer);
599
- resolve();
600
- });
601
-
602
- try {
603
- child.kill('SIGTERM');
604
- } catch (_) {
605
- clearTimeout(timer);
606
- resolve();
607
- }
608
- });
609
- }
610
-
611
- // ── Singleton ────────────────────────────────────────────────────────────────
612
-
613
- static getInstance() {
614
- if (!HeadroomProxyManager._instance) {
615
- HeadroomProxyManager._instance = new HeadroomProxyManager();
616
- }
617
- return HeadroomProxyManager._instance;
618
- }
619
- }
620
-
621
- HeadroomProxyManager._instance = null;
622
-
623
- module.exports = { HeadroomProxyManager };