nubos-pilot 1.3.0 → 1.3.1

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 (38) hide show
  1. package/bin/np-tools/_commands.cjs +2 -0
  2. package/bin/np-tools/_elision-proxy-entry.cjs +13 -0
  3. package/bin/np-tools/elision-bench.cjs +67 -0
  4. package/bin/np-tools/elision-get.cjs +48 -0
  5. package/bin/np-tools/elision-get.test.cjs +66 -0
  6. package/bin/np-tools/loop-run-round.cjs +25 -11
  7. package/bin/np-tools/plan-milestone.cjs +1 -0
  8. package/bin/np-tools/research-phase.cjs +1 -1
  9. package/bin/np-tools/spawn-headless.cjs +62 -9
  10. package/lib/cache-align.cjs +78 -0
  11. package/lib/cache-align.test.cjs +69 -0
  12. package/lib/compress.cjs +495 -0
  13. package/lib/compress.test.cjs +267 -0
  14. package/lib/config-defaults.cjs +39 -0
  15. package/lib/config-schema.cjs +40 -4
  16. package/lib/elision-bench.cjs +409 -0
  17. package/lib/elision-bench.test.cjs +89 -0
  18. package/lib/elision-proxy.cjs +158 -0
  19. package/lib/elision-proxy.test.cjs +243 -0
  20. package/lib/elision.cjs +163 -0
  21. package/lib/elision.test.cjs +143 -0
  22. package/lib/nubosloop.cjs +1 -1
  23. package/lib/output-steering.cjs +68 -0
  24. package/lib/output-steering.test.cjs +74 -0
  25. package/lib/researcher-swarm.cjs +14 -3
  26. package/lib/runtime/agent-loop.cjs +36 -6
  27. package/lib/runtime/agent-loop.test.cjs +105 -0
  28. package/lib/runtime/dispatch.cjs +6 -6
  29. package/lib/runtime/dispatch.test.cjs +17 -3
  30. package/lib/runtime/providers/openai-compat.cjs +2 -1
  31. package/lib/runtime/providers/openai-compat.test.cjs +9 -0
  32. package/lib/runtime/tools/index.cjs +33 -1
  33. package/lib/runtime/tools/index.test.cjs +24 -0
  34. package/lib/schemas/data/elision-entry.v1.json +16 -0
  35. package/lib/token-cost.cjs +46 -0
  36. package/lib/token-cost.test.cjs +42 -0
  37. package/np-tools.cjs +2 -0
  38. package/package.json +1 -1
@@ -89,6 +89,8 @@ const COMMANDS = [
89
89
  { name: 'knowledge-index', category: 'Utility', description: 'Build BM25-light index over .nubos-pilot/**/*.md → .nubos-pilot/state/knowledge-index.json', description_de: 'Baut BM25-Light-Index über .nubos-pilot/**/*.md → .nubos-pilot/state/knowledge-index.json' },
90
90
  { name: 'knowledge-search', category: 'Utility', description: 'Query the knowledge index; returns top-N JSON hits (rel_path + lines + score + preview). Pass --task <id> inside a Nubosloop task to record Rule 9 audit evidence', description_de: 'Sucht im Knowledge-Index; liefert Top-N-JSON-Treffer (rel_path + Zeilen + Score + Preview). --task <id> innerhalb eines Nubosloop-Tasks schreibt den Rule-9-Audit-Nachweis' },
91
91
  { name: 'knowledge-stats', category: 'Utility', description: 'Print knowledge-index size + grouping (auto-builds if missing)', description_de: 'Gibt Knowledge-Index-Größe + Gruppierung aus (baut auto bei Fehlen)' },
92
+ { name: 'elision-get', category: 'Utility', description: 'Retrieve the original text behind a ⟦elided:<hash>⟧ compression marker. Reversible context compression (ADR-Elision). Positional <hash> or --hash; --json for envelope', description_de: 'Holt den Originaltext hinter einem ⟦elided:<hash>⟧ Kompressions-Marker. Reversible Kontext-Kompression (ADR-Elision). Positional <hash> oder --hash; --json für Envelope' },
93
+ { name: 'elision-bench', category: 'Utility', description: 'Measure elision compression: deterministic fidelity (ratio + critical-line preservation + byte-exact reversibility) over a fixture corpus; --with-model adds answer-equivalence (raw vs compressed). --size <small|medium|large> runs a scaled corpus with a deterministic --holdout control group (--max-cases N) and an estimated token saving (--price-per-mtok <p> [--currency EUR] adds a cost estimate). --json, --tier <name>', description_de: 'Misst die Elision-Kompression: deterministische Fidelity (Ratio + Erhalt kritischer Zeilen + byte-exakte Reversibilität) über ein Fixture-Korpus; --with-model ergänzt Antwort-Äquivalenz (roh vs. komprimiert). --size <small|medium|large> fährt ein skaliertes Korpus mit deterministischer --holdout-Kontrollgruppe (--max-cases N) und geschätzter Token-Ersparnis (--price-per-mtok <p> [--currency EUR] ergänzt eine Kostenschätzung). --json, --tier <name>' },
92
94
  { name: 'context-stats', category: 'Utility', description: 'Aggregated context-budget stats (file counts + bytes per group, knowledge-index size)', description_de: 'Aggregierte Context-Budget-Stats (Dateien/Bytes pro Gruppe, Knowledge-Index-Größe)' },
93
95
  { name: 'session-snapshot-write', category: 'Utility', description: 'Capture session snapshot (current_task + recent commits + open handoffs) for resume', description_de: 'Erfasst Session-Snapshot (current_task + letzte Commits + offene Handoffs) für Resume' },
94
96
  { name: 'session-snapshot-read', category: 'Utility', description: 'Print last session snapshot as JSON', description_de: 'Gibt letzten Session-Snapshot als JSON aus' },
@@ -0,0 +1,13 @@
1
+ 'use strict';
2
+
3
+ const proxy = require('../../lib/elision-proxy.cjs');
4
+
5
+ proxy.start({
6
+ cwd: process.env.ELISION_PROXY_CWD || process.cwd(),
7
+ upstream: process.env.ELISION_PROXY_UPSTREAM || undefined,
8
+ }).then(({ baseUrl }) => {
9
+ if (process.send) process.send({ ready: true, baseUrl });
10
+ }).catch((err) => {
11
+ if (process.send) process.send({ ready: false, error: String(err && err.message) });
12
+ process.exit(1);
13
+ });
@@ -0,0 +1,67 @@
1
+ 'use strict';
2
+
3
+ const bench = require('../../lib/elision-bench.cjs');
4
+
5
+ function _parseArgs(args) {
6
+ const out = { json: false, withModel: false, tier: 'frontier', size: null, holdout: 0.2, maxCases: null, charsPerToken: null, pricePerMTok: null, currency: null };
7
+ for (let i = 0; i < (args || []).length; i += 1) {
8
+ const a = args[i];
9
+ if (a === '--json') { out.json = true; continue; }
10
+ if (a === '--with-model') { out.withModel = true; continue; }
11
+ if (a === '--tier') { out.tier = args[++i] || out.tier; continue; }
12
+ if (a === '--size') { out.size = args[++i] || out.size; continue; }
13
+ if (a === '--holdout') { const v = parseFloat(args[++i]); if (Number.isFinite(v)) out.holdout = v; continue; }
14
+ if (a === '--max-cases') { const v = parseInt(args[++i], 10); if (Number.isFinite(v)) out.maxCases = v; continue; }
15
+ if (a === '--chars-per-token') { const v = parseFloat(args[++i]); if (Number.isFinite(v)) out.charsPerToken = v; continue; }
16
+ if (a === '--price-per-mtok') { const v = parseFloat(args[++i]); if (Number.isFinite(v)) out.pricePerMTok = v; continue; }
17
+ if (a === '--currency') { out.currency = args[++i] || out.currency; continue; }
18
+ }
19
+ return out;
20
+ }
21
+
22
+ async function run(args, ctx) {
23
+ const context = ctx || {};
24
+ const cwd = context.cwd || process.cwd();
25
+ const stdout = context.stdout || process.stdout;
26
+ const parsed = _parseArgs(args || []);
27
+
28
+ if (parsed.size) {
29
+ const scale = bench.runScale({ cwd, size: parsed.size, holdoutRatio: parsed.holdout, maxCases: parsed.maxCases, charsPerToken: parsed.charsPerToken, pricePerMTok: parsed.pricePerMTok, currency: parsed.currency });
30
+ stdout.write((parsed.json ? JSON.stringify({ scale }) : bench.formatScale(scale)) + '\n');
31
+ return scale.summary.invariants_ok ? 0 : 1;
32
+ }
33
+
34
+ const fidelity = bench.runFidelity({ cwd });
35
+ let equivalence = null;
36
+
37
+ if (parsed.withModel) {
38
+ const { resolveFromConfig } = require('./resolve-model.cjs');
39
+ const { chat } = require('../../lib/runtime/providers/openai-compat.cjs');
40
+ const res = resolveFromConfig({ agentOrTier: parsed.tier, cwd, format: 'json' });
41
+ if (!res || !res.model || !res.baseUrl) {
42
+ stdout.write((parsed.json ? JSON.stringify({ fidelity, equivalence: null, error: 'no-provider' }) : 'No off-host provider resolved for tier "' + parsed.tier + '" — equivalence skipped.') + '\n');
43
+ return fidelity.summary.invariants_ok ? 0 : 1;
44
+ }
45
+ const provider = { baseUrl: res.baseUrl, apiKeyEnv: res.apiKeyEnv, model: res.model };
46
+ equivalence = await bench.runEquivalence({ chatImpl: chat, provider });
47
+ }
48
+
49
+ if (parsed.json) {
50
+ stdout.write(JSON.stringify({ fidelity, equivalence }) + '\n');
51
+ } else {
52
+ stdout.write(bench.formatReport(fidelity) + '\n');
53
+ if (equivalence) {
54
+ stdout.write('\nAnswer equivalence — ' + equivalence.summary.equivalent + '/' + equivalence.summary.cases
55
+ + ' equivalent, regressions: ' + (equivalence.summary.regressions.length ? equivalence.summary.regressions.join(', ') : 'none') + '\n');
56
+ for (const c of equivalence.cases) {
57
+ stdout.write(' ' + (c.regression ? '✗' : '✓') + ' ' + c.name + ' (raw '
58
+ + (c.raw_ok ? 'ok' : 'miss') + ', compressed ' + (c.compressed_ok ? 'ok' : 'miss') + ')\n');
59
+ }
60
+ }
61
+ }
62
+
63
+ const ok = fidelity.summary.invariants_ok && (!equivalence || equivalence.summary.no_regression);
64
+ return ok ? 0 : 1;
65
+ }
66
+
67
+ module.exports = { run, _parseArgs };
@@ -0,0 +1,48 @@
1
+ 'use strict';
2
+
3
+ const { NubosPilotError } = require('../../lib/core.cjs');
4
+ const elision = require('../../lib/elision.cjs');
5
+
6
+ function _parseArgs(args) {
7
+ const out = { hash: null, json: false };
8
+ const positional = [];
9
+ for (let i = 0; i < args.length; i++) {
10
+ const a = args[i];
11
+ if (a === '--hash') { out.hash = args[++i] || null; continue; }
12
+ if (a === '--json') { out.json = true; continue; }
13
+ positional.push(a);
14
+ }
15
+ if (out.hash == null) out.hash = positional.join('').trim() || null;
16
+ return out;
17
+ }
18
+
19
+ function run(args, ctx) {
20
+ const context = ctx || {};
21
+ const cwd = context.cwd || process.cwd();
22
+ const stdout = context.stdout || process.stdout;
23
+ const parsed = _parseArgs(args || []);
24
+ if (!parsed.hash) {
25
+ throw new NubosPilotError(
26
+ 'elision-get-missing-hash',
27
+ 'elision-get requires a hash (positional or --hash)',
28
+ { hint: 'hash is the 12-char id from a ⟦elided:…⟧ marker' },
29
+ );
30
+ }
31
+ const result = elision.retrieve(parsed.hash, cwd);
32
+ if (parsed.json) {
33
+ stdout.write(JSON.stringify(result));
34
+ return result.status === 'ok' ? 0 : 1;
35
+ }
36
+ if (result.status === 'ok') {
37
+ stdout.write(result.original);
38
+ return 0;
39
+ }
40
+ if (result.status === 'expired') {
41
+ stdout.write('[elision ' + parsed.hash + ' expired — original no longer cached (ttl elapsed)]\n');
42
+ return 1;
43
+ }
44
+ stdout.write('[elision ' + parsed.hash + ' not found]\n');
45
+ return 1;
46
+ }
47
+
48
+ module.exports = { run, _parseArgs };
@@ -0,0 +1,66 @@
1
+ 'use strict';
2
+
3
+ const { test } = require('node:test');
4
+ const assert = require('node:assert/strict');
5
+ const fs = require('node:fs');
6
+ const os = require('node:os');
7
+ const path = require('node:path');
8
+
9
+ const elisionGet = require('./elision-get.cjs');
10
+ const elision = require('../../lib/elision.cjs');
11
+ const { NubosPilotError } = require('../../lib/core.cjs');
12
+
13
+ function sandbox() {
14
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'np-elisionget-'));
15
+ fs.mkdirSync(path.join(root, '.nubos-pilot'), { recursive: true });
16
+ return root;
17
+ }
18
+
19
+ function capture() {
20
+ const chunks = [];
21
+ return { stream: { write: (s) => chunks.push(s) }, text: () => chunks.join('') };
22
+ }
23
+
24
+ test('ELISIONGET-1: prints the original for a known hash', () => {
25
+ const root = sandbox();
26
+ try {
27
+ const hash = elision.store('the original payload', { type: 'plain' }, root);
28
+ const out = capture();
29
+ const code = elisionGet.run([hash], { cwd: root, stdout: out.stream });
30
+ assert.equal(code, 0);
31
+ assert.equal(out.text(), 'the original payload');
32
+ } finally {
33
+ fs.rmSync(root, { recursive: true, force: true });
34
+ }
35
+ });
36
+
37
+ test('ELISIONGET-2: unknown hash exits 1 with a not-found notice', () => {
38
+ const root = sandbox();
39
+ try {
40
+ const out = capture();
41
+ const code = elisionGet.run(['aaaaaaaaaaaa'], { cwd: root, stdout: out.stream });
42
+ assert.equal(code, 1);
43
+ assert.match(out.text(), /not found/);
44
+ } finally {
45
+ fs.rmSync(root, { recursive: true, force: true });
46
+ }
47
+ });
48
+
49
+ test('ELISIONGET-3: --json emits the retrieval envelope', () => {
50
+ const root = sandbox();
51
+ try {
52
+ const hash = elision.store('payload', { type: 'plain' }, root);
53
+ const out = capture();
54
+ const code = elisionGet.run([hash, '--json'], { cwd: root, stdout: out.stream });
55
+ assert.equal(code, 0);
56
+ const env = JSON.parse(out.text());
57
+ assert.equal(env.status, 'ok');
58
+ assert.equal(env.original, 'payload');
59
+ } finally {
60
+ fs.rmSync(root, { recursive: true, force: true });
61
+ }
62
+ });
63
+
64
+ test('ELISIONGET-4: missing hash throws', () => {
65
+ assert.throws(() => elisionGet.run([], { cwd: process.cwd(), stdout: { write() {} } }), NubosPilotError);
66
+ });
@@ -6,16 +6,37 @@ const path = require('node:path');
6
6
  const { NubosPilotError, safeAssign } = require('../../lib/core.cjs');
7
7
  const safePath = require('../../lib/safe-path.cjs');
8
8
 
9
- function _resolveInsideCwdOrTmp(p, cwd, label, errorCode) {
10
- return safePath.assertInsideCwdOrTmp(p, cwd, label, errorCode);
11
- }
12
9
  const checkpoint = require('../../lib/checkpoint.cjs');
13
10
  const nubosloop = require('../../lib/nubosloop.cjs');
14
11
  const messaging = require('../../lib/messaging.cjs');
12
+ const compress = require('../../lib/compress.cjs');
13
+ const elision = require('../../lib/elision.cjs');
14
+ const logger = require('../../lib/logger.cjs').child('loop-run-round');
15
15
  const args = require('./_args.cjs');
16
16
 
17
17
  const { TASK_ID_RE } = require('../../lib/ids.cjs');
18
18
 
19
+ function _resolveInsideCwdOrTmp(p, cwd, label, errorCode) {
20
+ return safePath.assertInsideCwdOrTmp(p, cwd, label, errorCode);
21
+ }
22
+
23
+ function _verifyExcerpt(verifyOutput, cwd) {
24
+ const s = String(verifyOutput);
25
+ const cx = elision.compressionContext(cwd);
26
+ const maxBytes = cx.verifyMaxBytes;
27
+ if (s.length <= maxBytes) return s;
28
+ if (cx.enabled) {
29
+ try {
30
+ const crushed = compress.crushLogToBudget(s, maxBytes);
31
+ if (crushed && crushed.length < s.length) {
32
+ const hash = cx.store ? cx.store(s, 'log') : null;
33
+ return crushed + (hash ? '\n' + compress.marker(hash, 'full verify log') : '');
34
+ }
35
+ } catch (err) { logger.warn('verify compression skipped', { cause: err && err.message }); }
36
+ }
37
+ return '…[truncated head, original ' + s.length + ' bytes — see VERIFY_LOG]\n' + s.slice(-maxBytes);
38
+ }
39
+
19
40
  const VALID_PHASES = new Set([
20
41
  'preflight',
21
42
  'post-researcher',
@@ -182,16 +203,9 @@ function _runPostExecutor(taskId, list, cwd) {
182
203
  }
183
204
  }
184
205
  const green = code === 0;
185
- const VERIFY_TAIL_BYTES = 2000;
186
206
  let verifyExcerpt = null;
187
207
  if (verifyOutput) {
188
- const s = String(verifyOutput);
189
- if (s.length > VERIFY_TAIL_BYTES) {
190
- verifyExcerpt = '…[truncated head, original ' + s.length + ' bytes — see VERIFY_LOG]\n'
191
- + s.slice(-VERIFY_TAIL_BYTES);
192
- } else {
193
- verifyExcerpt = s;
194
- }
208
+ verifyExcerpt = _verifyExcerpt(verifyOutput, cwd);
195
209
  }
196
210
  const merged = checkpoint.mergeCheckpoint(
197
211
  taskId,
@@ -95,6 +95,7 @@ async function _initPayload(mNum, cwd, opts) {
95
95
  const spawnSpecs = swarm.buildSpawnSpecs(
96
96
  { milestone: mNum, milestone_id: layout.mId(mNum), goal: def.goal || '' },
97
97
  swarmOpts.k,
98
+ { cwd },
98
99
  );
99
100
  let cacheHit = null;
100
101
  let cacheMiss = null;
@@ -107,7 +107,7 @@ async function run(args, ctx) {
107
107
  const tmDetail = textMode.resolveTextModeDetail(cwd);
108
108
 
109
109
  const swarmOpts = swarm.resolveSwarmOpts(cwd);
110
- const spawnSpecs = swarm.buildSpawnSpecs({ milestone: mNum, milestone_id: mIdStr, goal: def.goal || '' }, swarmOpts.k);
110
+ const spawnSpecs = swarm.buildSpawnSpecs({ milestone: mNum, milestone_id: mIdStr, goal: def.goal || '' }, swarmOpts.k, { cwd });
111
111
 
112
112
  let cacheHit = null;
113
113
  let cacheMiss = null;
@@ -9,8 +9,35 @@ const { NubosPilotError, atomicWriteFileSync, appendJsonl, findProjectRoot } = r
9
9
  const runContext = require('../../lib/run-context.cjs');
10
10
  const safePath = require('../../lib/safe-path.cjs');
11
11
  const headlessGuard = require('../../lib/headless-guard.cjs');
12
+ const compress = require('../../lib/compress.cjs');
13
+ const elision = require('../../lib/elision.cjs');
14
+ const elisionProxy = require('../../lib/elision-proxy.cjs');
15
+ const steering = require('../../lib/output-steering.cjs');
16
+ const logger = require('../../lib/logger.cjs').child('spawn-headless');
12
17
  const args = require('./_args.cjs');
13
18
 
19
+ const PROXY_START_TIMEOUT_MS = 5000;
20
+
21
+ function _startCompressionProxy(cwd) {
22
+ const upstream = process.env.ANTHROPIC_BASE_URL || '';
23
+ return new Promise((resolve, reject) => {
24
+ const proc = child_process.fork(path.join(__dirname, '_elision-proxy-entry.cjs'), [], {
25
+ env: Object.assign({}, process.env, { ELISION_PROXY_CWD: cwd, ELISION_PROXY_UPSTREAM: upstream }),
26
+ stdio: ['ignore', 'ignore', 'inherit', 'ipc'],
27
+ });
28
+ const timer = setTimeout(() => {
29
+ try { proc.kill(); } catch { /* already gone */ }
30
+ reject(new NubosPilotError('elision-proxy-start-timeout', 'compression proxy did not report ready in time', {}));
31
+ }, PROXY_START_TIMEOUT_MS);
32
+ proc.once('message', (m) => {
33
+ clearTimeout(timer);
34
+ if (m && m.ready) resolve({ proc, baseUrl: m.baseUrl });
35
+ else { try { proc.kill(); } catch { /* already gone */ } reject(new NubosPilotError('elision-proxy-start-failed', String(m && m.error), {})); }
36
+ });
37
+ proc.once('error', (err) => { clearTimeout(timer); reject(err); });
38
+ });
39
+ }
40
+
14
41
  function _sha256(s) {
15
42
  return crypto.createHash('sha256').update(s == null ? '' : String(s)).digest('hex');
16
43
  }
@@ -159,6 +186,20 @@ function _composePrompt(agentBody, userPrompt) {
159
186
  return agentBody.trimEnd() + '\n\n---\n\n' + userPrompt.trimEnd() + '\n';
160
187
  }
161
188
 
189
+ function _maybeCompressPrompt(prompt, cwd) {
190
+ const none = { prompt, stats: null };
191
+ const cx = elision.compressionContext(cwd);
192
+ if (!cx.enabled) return none;
193
+ try {
194
+ const out = compress.compressPrompt(prompt, { minBlockBytes: cx.minBlockBytes, store: cx.store });
195
+ if (!out || out.stats.blocks_compressed === 0) return none;
196
+ return { prompt: out.text, stats: out.stats };
197
+ } catch (err) {
198
+ logger.warn('compression skipped', { cause: err && err.message });
199
+ return none;
200
+ }
201
+ }
202
+
162
203
  function _stripFrontmatter(md) {
163
204
  if (!md.startsWith('---\n')) return md;
164
205
  const end = md.indexOf('\n---\n', 4);
@@ -304,20 +345,15 @@ async function run(argv, ctx) {
304
345
  }
305
346
 
306
347
  const agentPath = _resolveAgentPath(agent, cwd);
307
- const agentBody = _stripFrontmatter(fs.readFileSync(agentPath, 'utf-8'));
348
+ let agentBody = _stripFrontmatter(fs.readFileSync(agentPath, 'utf-8'));
349
+ const _os = elision.compressionContext(cwd).outputSteering;
350
+ if (_os && _os.enabled) agentBody = steering.enrichSystemPrompt(agentBody, _os.profile);
308
351
  const userPrompt = _readPromptFile(promptPath, cwd);
309
- const composedPrompt = _composePrompt(agentBody, userPrompt);
352
+ let composedPrompt = _composePrompt(agentBody, userPrompt);
310
353
  const resolvedOutput = _ensureOutputDir(outputPath, cwd);
311
354
 
312
355
  const runId = runContext.getRunId();
313
356
 
314
- // ADR-0021: off-host routing. If the agent routes to an openai-compat provider,
315
- // run the nubos-pilot dispatch loop instead of `claude -p`, writing the result
316
- // in the same {result,...} envelope `claude --output-format json` produces so the
317
- // callers (review/extract parseXxxOutput → outer.result) parse it unchanged. The
318
- // native path below is untouched. run() is async; the CLI dispatcher already
319
- // awaits returned promises (handleRunResult), and the two in-process callers
320
- // (review.cjs / extract.cjs) await runReview/runExtract.
321
357
  const resolveKind = context.resolveImpl || _defaultResolveKind;
322
358
  let routed;
323
359
  try { routed = resolveKind(agent, cwd); } catch { routed = { kind: 'native' }; }
@@ -329,6 +365,9 @@ async function run(argv, ctx) {
329
365
  });
330
366
  }
331
367
 
368
+ const compression = _maybeCompressPrompt(composedPrompt, cwd);
369
+ composedPrompt = compression.prompt;
370
+
332
371
  let lockRoot;
333
372
  try { lockRoot = findProjectRoot(cwd); }
334
373
  catch { lockRoot = cwd; }
@@ -344,6 +383,17 @@ async function run(argv, ctx) {
344
383
  const childEnv = _filterSpawnEnv(process.env);
345
384
  Object.assign(childEnv, headlessGuard.childSpawnEnv(process.env));
346
385
 
386
+ let proxyProc = null;
387
+ if (elisionProxy.proxyEnabled(cwd)) {
388
+ try {
389
+ const started = await _startCompressionProxy(cwd);
390
+ proxyProc = started.proc;
391
+ childEnv.ANTHROPIC_BASE_URL = started.baseUrl;
392
+ } catch (err) {
393
+ logger.warn('compression proxy unavailable; spawning without it', { cause: err && err.message });
394
+ }
395
+ }
396
+
347
397
  const bin = _claudeBinary();
348
398
  const claudeArgs = ['-p', '--output-format', 'json'];
349
399
  const startedAt = new Date().toISOString();
@@ -365,6 +415,7 @@ async function run(argv, ctx) {
365
415
  { bin, cause: err && err.code },
366
416
  );
367
417
  } finally {
418
+ if (proxyProc) { try { proxyProc.kill(); } catch { /* already exited */ } }
368
419
  lock.release();
369
420
  }
370
421
  if (result.error && result.error.code === 'ENOENT') {
@@ -406,6 +457,7 @@ async function run(argv, ctx) {
406
457
  tokens_in: claudeMeta.tokens_in == null ? null : claudeMeta.tokens_in,
407
458
  tokens_out: claudeMeta.tokens_out == null ? null : claudeMeta.tokens_out,
408
459
  payload_parse_ok: claudeMeta.parse_ok,
460
+ compression: compression.stats || null,
409
461
  };
410
462
  try { appendJsonl(spawnTrailPath, spawnRecord, { maxLineBytes: 16 * 1024, mode: 0o600 }); }
411
463
  catch (err) {
@@ -431,6 +483,7 @@ async function run(argv, ctx) {
431
483
  tokens_in: spawnRecord.tokens_in,
432
484
  tokens_out: spawnRecord.tokens_out,
433
485
  payload_parse_ok: spawnRecord.payload_parse_ok,
486
+ compression: compression.stats || null,
434
487
  };
435
488
  stdout.write(JSON.stringify(payload) + '\n');
436
489
  if (exitCode !== 0) return 2;
@@ -0,0 +1,78 @@
1
+ 'use strict';
2
+
3
+ const UUID_RE = /\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/i;
4
+ const ISO_RE = /\b\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/;
5
+ const JWT_RE = /\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/;
6
+ const HEX_RE = /\b[0-9a-f]{32,64}\b/i;
7
+ const VOLATILE = Object.freeze([
8
+ ['uuid', UUID_RE],
9
+ ['iso_datetime', ISO_RE],
10
+ ['jwt', JWT_RE],
11
+ ['hex_hash', HEX_RE],
12
+ ]);
13
+
14
+ function _systemText(system) {
15
+ if (typeof system === 'string') return system;
16
+ if (Array.isArray(system)) {
17
+ return system.map((b) => (b && typeof b.text === 'string' ? b.text : '')).join('\n');
18
+ }
19
+ return '';
20
+ }
21
+
22
+ function detectVolatile(system) {
23
+ const text = _systemText(system);
24
+ const findings = [];
25
+ for (const [kind, re] of VOLATILE) {
26
+ const m = text.match(re);
27
+ if (m) findings.push({ kind, sample_len: m[0].length });
28
+ }
29
+ return findings;
30
+ }
31
+
32
+ function _sortKeys(v) {
33
+ if (Array.isArray(v)) return v.map(_sortKeys);
34
+ if (v && typeof v === 'object') {
35
+ const out = {};
36
+ for (const k of Object.keys(v).sort()) out[k] = _sortKeys(v[k]);
37
+ return out;
38
+ }
39
+ return v;
40
+ }
41
+
42
+ function _byCodepoint(a, b) {
43
+ return a < b ? -1 : a > b ? 1 : 0;
44
+ }
45
+
46
+ function normalizeTools(tools) {
47
+ return tools
48
+ .map(_sortKeys)
49
+ .map((t) => ({ t, name: String(t && t.name), body: JSON.stringify(t) }))
50
+ .sort((a, b) => _byCodepoint(a.name, b.name) || _byCodepoint(a.body, b.body))
51
+ .map((x) => x.t);
52
+ }
53
+
54
+ function _anyCacheControl(system, tools) {
55
+ if (Array.isArray(system) && system.some((b) => b && b.cache_control)) return true;
56
+ if (Array.isArray(tools) && tools.some((t) => t && t.cache_control)) return true;
57
+ return false;
58
+ }
59
+
60
+ function alignAnthropicBody(body) {
61
+ const findings = detectVolatile(body && body.system);
62
+ if (!body || !Array.isArray(body.tools) || body.tools.length === 0) {
63
+ return { body, findings, applied: false };
64
+ }
65
+ if (_anyCacheControl(body.system, body.tools)) {
66
+ return { body, findings, applied: false };
67
+ }
68
+ const tools = normalizeTools(body.tools);
69
+ const last = tools.length - 1;
70
+ tools[last] = Object.assign({}, tools[last], { cache_control: { type: 'ephemeral' } });
71
+ return { body: Object.assign({}, body, { tools }), findings, applied: true };
72
+ }
73
+
74
+ module.exports = {
75
+ detectVolatile,
76
+ normalizeTools,
77
+ alignAnthropicBody,
78
+ };
@@ -0,0 +1,69 @@
1
+ 'use strict';
2
+
3
+ const { test } = require('node:test');
4
+ const assert = require('node:assert/strict');
5
+
6
+ const ca = require('./cache-align.cjs');
7
+
8
+ test('CA-1: detectVolatile flags UUID/ISO-date/JWT/hex in the system prompt', () => {
9
+ const sys = 'Run id 550e8400-e29b-41d4-a716-446655440000 at 2026-06-23T10:00:00Z '
10
+ + 'token eyJhbGc.eyJzdWI.sig hash d41d8cd98f00b204e9800998ecf8427e';
11
+ const kinds = ca.detectVolatile(sys).map((f) => f.kind).sort();
12
+ assert.deepEqual(kinds, ['hex_hash', 'iso_datetime', 'jwt', 'uuid']);
13
+ });
14
+
15
+ test('CA-2: detectVolatile reads an array-form system prompt and is clean when stable', () => {
16
+ assert.deepEqual(ca.detectVolatile([{ type: 'text', text: 'You are a stable agent.' }]), []);
17
+ assert.deepEqual(ca.detectVolatile('plain stable instructions'), []);
18
+ });
19
+
20
+ test('CA-3: normalizeTools sorts tools by name and recursively sorts schema keys; arrays keep order', () => {
21
+ const tools = [
22
+ { name: 'zeta', description: 'z', input_schema: { type: 'object', required: ['b', 'a'], properties: { y: {}, x: {} } } },
23
+ { name: 'alpha', description: 'a', input_schema: { properties: { n: {} }, type: 'object' } },
24
+ ];
25
+ const out = ca.normalizeTools(tools);
26
+ assert.deepEqual(out.map((t) => t.name), ['alpha', 'zeta']);
27
+ assert.deepEqual(Object.keys(out[1]), ['description', 'input_schema', 'name']);
28
+ assert.deepEqual(Object.keys(out[1].input_schema.properties), ['x', 'y']);
29
+ assert.deepEqual(out[1].input_schema.required, ['b', 'a']);
30
+ });
31
+
32
+ test('CA-4: alignAnthropicBody adds exactly one ephemeral breakpoint on the last tool and is idempotent', () => {
33
+ const body = { system: 'sys', tools: [{ name: 'b' }, { name: 'a' }], messages: [] };
34
+ const r1 = ca.alignAnthropicBody(body);
35
+ assert.equal(r1.applied, true);
36
+ assert.deepEqual(r1.body.tools.map((t) => t.name), ['a', 'b']);
37
+ assert.equal(r1.body.tools[0].cache_control, undefined);
38
+ assert.deepEqual(r1.body.tools[1].cache_control, { type: 'ephemeral' });
39
+ const r2 = ca.alignAnthropicBody(r1.body);
40
+ assert.equal(r2.applied, false, 'second pass is a no-op — breakpoint already present');
41
+ assert.equal(JSON.stringify(r2.body), JSON.stringify(r1.body), 'byte-identical second pass');
42
+ });
43
+
44
+ test('CA-4b: normalizeTools is byte-deterministic regardless of input order (codepoint, not locale)', () => {
45
+ const a = [{ name: 'get_A' }, { name: 'get-a' }, { name: 'get_a' }, { name: 'tool10' }, { name: 'tool2' }];
46
+ const b = a.slice().reverse();
47
+ assert.equal(JSON.stringify(ca.normalizeTools(a)), JSON.stringify(ca.normalizeTools(b)), 'order-independent, stable bytes');
48
+ assert.deepEqual(ca.normalizeTools(a).map((t) => t.name), ['get-a', 'get_A', 'get_a', 'tool10', 'tool2']);
49
+ });
50
+
51
+ test('CA-4c: duplicate/degenerate names still order deterministically via body tiebreak', () => {
52
+ const x = [{ name: 'x', id: 2 }, { name: 'x', id: 1 }];
53
+ assert.equal(JSON.stringify(ca.normalizeTools(x)), JSON.stringify(ca.normalizeTools(x.slice().reverse())));
54
+ });
55
+
56
+ test('CA-5: customer-placement-wins — a caller-set cache_control is never disturbed', () => {
57
+ const body = { tools: [{ name: 'z', cache_control: { type: 'ephemeral' } }, { name: 'a' }], messages: [] };
58
+ const r = ca.alignAnthropicBody(body);
59
+ assert.equal(r.applied, false);
60
+ assert.deepEqual(r.body.tools.map((t) => t.name), ['z', 'a'], 'tool order untouched when caller manages caching');
61
+ });
62
+
63
+ test('CA-6: no tools → no normalization, detection still runs', () => {
64
+ const body = { system: 'id 550e8400-e29b-41d4-a716-446655440000', messages: [] };
65
+ const r = ca.alignAnthropicBody(body);
66
+ assert.equal(r.applied, false);
67
+ assert.equal(r.body, body);
68
+ assert.deepEqual(r.findings.map((f) => f.kind), ['uuid']);
69
+ });