@cgh567/agent 2.4.3 → 2.4.4

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 (140) hide show
  1. package/agents/business/talisman-ceo.md +183 -0
  2. package/agents/business/talisman-comms.md +257 -0
  3. package/agents/business/talisman-cto.md +153 -0
  4. package/agents/business/talisman-finance.md +246 -0
  5. package/agents/business/talisman-marketing.md +240 -0
  6. package/agents/business/talisman-sales.md +242 -0
  7. package/agents/business/talisman-support.md +236 -0
  8. package/bin/helios-rpc.js +19 -0
  9. package/daemon/adapters/helios-rpc-adapter.js +5 -12
  10. package/daemon/context-enrichment.js +27 -0
  11. package/daemon/helios-api.js +290 -45
  12. package/daemon/helios-company-daemon.js +160 -50
  13. package/daemon/lib/blast-radius-analyzer.js +75 -0
  14. package/daemon/lib/domain-bootstrap-orchestrator.js +267 -0
  15. package/daemon/lib/forensic-log.js +113 -0
  16. package/daemon/lib/goal-research-pipeline.js +644 -0
  17. package/daemon/lib/harada/cascade-judge.js +84 -1
  18. package/daemon/lib/harada/cascade-research-dispatcher.js +282 -0
  19. package/daemon/lib/harada/pillar-dispatcher.js +23 -2
  20. package/daemon/lib/hbo-bridge.js +73 -5
  21. package/daemon/lib/headroom-middleware.js +129 -0
  22. package/daemon/lib/headroom-proxy-manager.js +309 -0
  23. package/daemon/lib/intelligence/department-page-generator.js +46 -1
  24. package/daemon/lib/interpretation-engine.js +92 -0
  25. package/daemon/lib/mental-model-cache.js +96 -0
  26. package/daemon/lib/project-factory.js +47 -0
  27. package/daemon/lib/session-log-reader.js +93 -0
  28. package/daemon/lib/standard-work-bootstrap.js +87 -1
  29. package/daemon/lib/task-completion-processor.js +12 -0
  30. package/daemon/package.json +2 -1
  31. package/daemon/routes/agents.js +51 -6
  32. package/daemon/routes/channels.js +116 -2
  33. package/daemon/routes/crm.js +85 -0
  34. package/daemon/routes/dashboard.js +62 -16
  35. package/daemon/routes/dept.js +10 -1
  36. package/daemon/routes/email-triage.js +19 -10
  37. package/daemon/routes/hbo.js +367 -13
  38. package/daemon/routes/hed.js +133 -0
  39. package/daemon/routes/inbox.js +397 -8
  40. package/daemon/routes/project.js +392 -9
  41. package/daemon/schema-definitions.js +10 -0
  42. package/daemon/schema-migrations-hbo.js +10 -0
  43. package/daemon/schema-migrations-proj.js +22 -0
  44. package/extensions/__tests__/codebase-index.test.ts +73 -0
  45. package/extensions/__tests__/extension-command-registration.test.ts +35 -0
  46. package/extensions/__tests__/git-push-guard.test.ts +68 -0
  47. package/extensions/context-compaction.ts +104 -76
  48. package/extensions/cortex/__tests__/cortex-core.test.ts +100 -0
  49. package/extensions/email/actions/draft-response.ts +21 -1
  50. package/extensions/email/auth/accounts.ts +5 -11
  51. package/extensions/email/auth/inbox-dog.ts +5 -2
  52. package/extensions/email/backfill.ts +20 -13
  53. package/extensions/email/providers/gmail.ts +164 -0
  54. package/extensions/email/providers/google-calendar.ts +34 -5
  55. package/extensions/helios-browser/__tests__/browser-routing.test.ts +57 -0
  56. package/extensions/helios-browser/backends/playwright.ts +3 -1
  57. package/extensions/helios-governance/__tests__/governance-gates.test.ts +40 -0
  58. package/extensions/helios-governance/__tests__/tournament-consumer.test.js +66 -0
  59. package/extensions/hema-dispatch-v3/headroom-compress.ts +103 -0
  60. package/extensions/hema-dispatch-v3/index.ts +33 -65
  61. package/extensions/interview/__tests__/server.test.ts +117 -0
  62. package/extensions/lib/helios-root.cjs +46 -0
  63. package/extensions/subagent-mesh/__tests__/handlers.test.ts +98 -0
  64. package/extensions/warm-tick/warm-tick-maintenance.ts +156 -0
  65. package/lib/__tests__/bulk-ingest.live.test.ts +66 -0
  66. package/lib/__tests__/crash-fixes.test.ts +49 -0
  67. package/lib/__tests__/maintenance-mission-wiring.test.ts +35 -0
  68. package/lib/broker/__tests__/jit-subscription.test.js +44 -1
  69. package/lib/broker/__tests__/lifecycle-channels.test.js +25 -1
  70. package/lib/compression/__tests__/ccr-store.test.js +138 -0
  71. package/lib/compression/__tests__/pipeline.test.js +280 -0
  72. package/lib/compression/__tests__/smart-crusher.test.js +242 -0
  73. package/lib/compression/dist/server.js +34 -1
  74. package/lib/compression/dist/start-server.js +77 -0
  75. package/lib/graph/learning/headroom-learn-bridge.js +175 -0
  76. package/lib/hbo-core-store.ts +71 -0
  77. package/lib/mission-loop/__tests__/research-handler.test.ts +143 -0
  78. package/lib/skill-sync.js +6 -1
  79. package/lib/startup-integrity.js +9 -2
  80. package/lib/triage-core/__tests__/classifier-fixture.test.ts +254 -0
  81. package/lib/triage-core/__tests__/classifier-post-norm.test.ts +1 -1
  82. package/lib/triage-core/__tests__/classifier.test.ts +45 -7
  83. package/lib/triage-core/__tests__/correction-detector.test.ts +36 -0
  84. package/lib/triage-core/__tests__/d6-dunbar-boost.test.ts +5 -5
  85. package/lib/triage-core/__tests__/orchestrator-pipeline.test.ts +107 -0
  86. package/lib/triage-core/__tests__/orchestrator.test.ts +113 -1
  87. package/lib/triage-core/__tests__/signals.test.ts +357 -0
  88. package/lib/triage-core/__tests__/sql-parity.test.ts +216 -0
  89. package/lib/triage-core/backfill-cost-estimator.ts +91 -0
  90. package/lib/triage-core/backfill-orchestrator.ts +119 -0
  91. package/lib/triage-core/classifier.ts +38 -6
  92. package/lib/triage-core/cos/cross-channel-escalation.ts +2 -3
  93. package/lib/triage-core/cos/response-debt.ts +2 -2
  94. package/lib/triage-core/graph/__tests__/batch-persistence.test.ts +283 -0
  95. package/lib/triage-core/graph/batch-persistence.ts +66 -2
  96. package/lib/triage-core/graph/betweenness-worker.js +75 -0
  97. package/lib/triage-core/graph/graph-rank-sql.ts +67 -0
  98. package/lib/triage-core/graph/persistence.ts +1 -1
  99. package/lib/triage-core/graph/schema-v2.ts +2 -0
  100. package/lib/triage-core/graph/schema.cypher +1 -0
  101. package/lib/triage-core/graph/triage-query.ts +1 -1
  102. package/lib/triage-core/learning.ts +15 -20
  103. package/lib/triage-core/mental-model/bedrock-config.ts +78 -0
  104. package/lib/triage-core/mental-model/cos-integration.ts +1 -1
  105. package/lib/triage-core/mental-model/entity-extractor.ts +51 -4
  106. package/lib/triage-core/mental-model/identity-resolver.ts +5 -5
  107. package/lib/triage-core/mental-model/model-assembler-sql.ts +200 -0
  108. package/lib/triage-core/mental-model/model-assembler.ts +16 -3
  109. package/lib/triage-core/orchestrator.ts +4 -4
  110. package/lib/triage-core/scheduled-sends.ts +39 -2
  111. package/lib/triage-core/signals/comms-style.ts +1 -1
  112. package/lib/triage-core/signals/cross-channel-escalation.ts +2 -2
  113. package/lib/triage-core/signals/favee-type.ts +6 -1
  114. package/lib/triage-core/signals/goal-relevance.ts +31 -2
  115. package/lib/triage-core/signals/personal-importance.ts +1 -1
  116. package/lib/triage-core/signals/referral-chain.ts +0 -1
  117. package/lib/triage-core/signals/relationship-decay.ts +4 -0
  118. package/lib/triage-core/signals/relationship-health.ts +6 -1
  119. package/lib/triage-core/signals/trajectory-signal.ts +38 -3
  120. package/lib/triage-core/tournament-runner.js +11 -1
  121. package/lib/triage-core/triage-llm-factory.ts +110 -0
  122. package/lib/triage-core/triage-local-llm.ts +145 -0
  123. package/lib/triage-core/triage-sql-store.ts +337 -0
  124. package/lib/triage-core/types.ts +2 -2
  125. package/lib/unified-graph.atomic.test.ts +52 -0
  126. package/lib/unified-graph.failure-categories.test.ts +55 -0
  127. package/package.json +10 -3
  128. package/prebuilds/darwin-arm64/better_sqlite3.node +0 -0
  129. package/prebuilds/linux-x64/better_sqlite3.node +0 -0
  130. package/prebuilds/win32-x64/better_sqlite3.node +0 -0
  131. package/skills/helios-bookkeeping/SKILL.md +321 -0
  132. package/skills/helios-briefer/SKILL.md +44 -0
  133. package/skills/helios-client-relations/SKILL.md +322 -0
  134. package/skills/helios-personal-triager/SKILL.md +45 -0
  135. package/skills/helios-recruitment/SKILL.md +317 -0
  136. package/skills/helios-relationship-nudger/SKILL.md +77 -0
  137. package/skills/helios-researcher/SKILL.md +44 -0
  138. package/skills/helios-scheduler/SKILL.md +58 -0
  139. package/skills/helios-tax-analyst/SKILL.md +280 -0
  140. package/lib/triage-core/orchestrator.ts.bak-r005-r006-r008 +0 -1823
@@ -1,8 +1,14 @@
1
1
  import { describe, test, expect } from 'vitest';
2
2
  const fs = require('fs');
3
3
  const path = require('path');
4
+ const os = require('os');
4
5
 
5
- const HOOKS_PATH = path.join(require('os').homedir(), 'helios-agent/extensions/lifecycle-hooks.ts');
6
+ // Desktop-first path resolution (P1-B2 fix)
7
+ const DESKTOP_ROOT = path.join(os.homedir(), 'Desktop', 'Helios', 'helios-agent-main');
8
+ const AGENT_ROOT = fs.existsSync(path.join(DESKTOP_ROOT, 'extensions'))
9
+ ? DESKTOP_ROOT
10
+ : path.join(os.homedir(), 'helios-agent');
11
+ const HOOKS_PATH = path.join(AGENT_ROOT, 'extensions/lifecycle-hooks.ts');
6
12
 
7
13
  describe('lifecycle-hooks channel usage', () => {
8
14
  let content;
@@ -40,4 +46,22 @@ describe('lifecycle-hooks channel usage', () => {
40
46
  expect(hasBlackboard).toBe(true);
41
47
  }
42
48
  });
49
+
50
+ // P2-C2: graphWrite channel verification
51
+ test('lifecycle.mission channel string is passed to graphWrite (not safeWrite)', () => {
52
+ content = content || (fs.existsSync(HOOKS_PATH) ? fs.readFileSync(HOOKS_PATH, 'utf8') : null);
53
+ if (!content) return;
54
+ // graphWrite is the broker-aware write — verify lifecycle.mission channel is threaded to it
55
+ const hasGraphWrite = content.includes('graphWrite') || content.includes('safeWrite');
56
+ const hasMissionChannel = content.includes("'lifecycle.mission'") || content.includes('"lifecycle.mission"');
57
+ expect(hasGraphWrite).toBe(true);
58
+ expect(hasMissionChannel).toBe(true);
59
+ });
60
+
61
+ test('lifecycle.step channel string is passed to graphWrite', () => {
62
+ content = content || (fs.existsSync(HOOKS_PATH) ? fs.readFileSync(HOOKS_PATH, 'utf8') : null);
63
+ if (!content) return;
64
+ const hasStepChannel = content.includes("'lifecycle.step'") || content.includes('"lifecycle.step"');
65
+ expect(hasStepChannel).toBe(true);
66
+ });
43
67
  });
@@ -0,0 +1,138 @@
1
+ /**
2
+ * lib/compression/__tests__/ccr-store.test.js
3
+ *
4
+ * Unit tests for CcrStore (TTL-based in-memory key/value store for CCR dropped rows).
5
+ * Tests: set/get, TTL expiry, delete, has, stats, destroy, singleton.
6
+ *
7
+ * No mocks — tests real CcrStore behavior.
8
+ */
9
+ 'use strict';
10
+
11
+ const { describe, it, expect, beforeEach, afterEach } = require('vitest');
12
+ const { CcrStore, getCcrStore } = require('../dist/ccr-store.js');
13
+
14
+ describe('CcrStore — basic operations', () => {
15
+ let store;
16
+ beforeEach(() => { store = new CcrStore(60); }); // 60s TTL
17
+ afterEach(() => { store.destroy(); });
18
+
19
+ it('set and get returns stored value', () => {
20
+ store.set('abc123', '["row1","row2"]');
21
+ expect(store.get('abc123')).toBe('["row1","row2"]');
22
+ });
23
+
24
+ it('get returns null for missing key', () => {
25
+ expect(store.get('nonexistent')).toBeNull();
26
+ });
27
+
28
+ it('has returns true for existing key', () => {
29
+ store.set('key1', 'data');
30
+ expect(store.has('key1')).toBe(true);
31
+ });
32
+
33
+ it('has returns false for missing key', () => {
34
+ expect(store.has('missing')).toBe(false);
35
+ });
36
+
37
+ it('delete removes a key', () => {
38
+ store.set('del-key', 'value');
39
+ store.delete('del-key');
40
+ expect(store.get('del-key')).toBeNull();
41
+ expect(store.has('del-key')).toBe(false);
42
+ });
43
+
44
+ it('delete on missing key does not throw', () => {
45
+ expect(() => store.delete('no-such-key')).not.toThrow();
46
+ });
47
+ });
48
+
49
+ describe('CcrStore — TTL expiry', () => {
50
+ it('expires entries after TTL', async () => {
51
+ const store = new CcrStore(0); // 0s TTL — expires immediately
52
+ store.set('exp-key', 'expiring data');
53
+ // After 0s TTL, the entry should already be past expiry
54
+ await new Promise((r) => setTimeout(r, 5));
55
+ expect(store.get('exp-key')).toBeNull();
56
+ store.destroy();
57
+ });
58
+
59
+ it('does not expire entries before TTL', () => {
60
+ const store = new CcrStore(3600); // 1h TTL
61
+ store.set('live-key', 'live data');
62
+ expect(store.get('live-key')).toBe('live data');
63
+ store.destroy();
64
+ });
65
+ });
66
+
67
+ describe('CcrStore — stats', () => {
68
+ let store;
69
+ beforeEach(() => { store = new CcrStore(60); });
70
+ afterEach(() => { store.destroy(); });
71
+
72
+ it('stats returns 0 entries for empty store', () => {
73
+ const s = store.stats();
74
+ expect(s.entries).toBe(0);
75
+ expect(s.totalOriginalBytes).toBe(0);
76
+ });
77
+
78
+ it('stats reflects current live entries', () => {
79
+ const data1 = 'data-string-one'; // 15 bytes
80
+ const data2 = 'data-string-two'; // 15 bytes
81
+ store.set('h1', data1);
82
+ store.set('h2', data2);
83
+ const s = store.stats();
84
+ expect(s.entries).toBe(2);
85
+ expect(s.totalOriginalBytes).toBe(data1.length + data2.length);
86
+ });
87
+
88
+ it('stats decrements after delete', () => {
89
+ store.set('a', 'aaa');
90
+ store.set('b', 'bbb');
91
+ store.delete('a');
92
+ const s = store.stats();
93
+ expect(s.entries).toBe(1);
94
+ });
95
+
96
+ it('stats excludes expired entries', async () => {
97
+ const storeShort = new CcrStore(0);
98
+ storeShort.set('exp', 'expiring');
99
+ await new Promise((r) => setTimeout(r, 10));
100
+ const s = storeShort.stats();
101
+ expect(s.entries).toBe(0);
102
+ storeShort.destroy();
103
+ });
104
+ });
105
+
106
+ describe('CcrStore — destroy', () => {
107
+ it('destroy clears all entries', () => {
108
+ const store = new CcrStore(60);
109
+ store.set('k1', 'v1');
110
+ store.set('k2', 'v2');
111
+ store.destroy();
112
+ expect(store.get('k1')).toBeNull();
113
+ expect(store.stats().entries).toBe(0);
114
+ });
115
+
116
+ it('destroy clears the cleanup interval (no unref error)', () => {
117
+ const store = new CcrStore(60);
118
+ expect(() => store.destroy()).not.toThrow();
119
+ // Second destroy is idempotent
120
+ expect(() => store.destroy()).not.toThrow();
121
+ });
122
+ });
123
+
124
+ describe('CcrStore — getCcrStore singleton', () => {
125
+ it('getCcrStore returns same instance on repeated calls', () => {
126
+ const a = getCcrStore();
127
+ const b = getCcrStore();
128
+ expect(a).toBe(b); // referential equality — same singleton
129
+ });
130
+
131
+ it('singleton accepts set/get operations', () => {
132
+ const store = getCcrStore();
133
+ const testKey = `test-singleton-${Date.now()}`;
134
+ store.set(testKey, 'singleton-data');
135
+ expect(store.get(testKey)).toBe('singleton-data');
136
+ store.delete(testKey); // cleanup
137
+ });
138
+ });
@@ -0,0 +1,280 @@
1
+ /**
2
+ * lib/compression/__tests__/pipeline.test.js
3
+ *
4
+ * Unit tests for CompressionPipeline and ContentRouter.
5
+ * Tests: message routing, system message skip, tool_result compression,
6
+ * headroom_retrieve protection, content type detection.
7
+ *
8
+ * No mocks — tests real behavior.
9
+ */
10
+ 'use strict';
11
+
12
+ const { describe, it, expect, beforeEach, afterEach } = require('vitest');
13
+ const { CompressionPipeline } = require('../dist/pipeline.js');
14
+ const { ContentRouter, detectContentType } = require('../dist/content-router.js');
15
+ const { CcrStore } = require('../dist/ccr-store.js');
16
+
17
+ // ── Helpers ───────────────────────────────────────────────────────────────────
18
+
19
+ function makeUniformJson(n) {
20
+ return JSON.stringify(
21
+ Array.from({ length: n }, (_, i) => ({
22
+ id: `lead-${String(i).padStart(4, '0')}`,
23
+ type: 'signal',
24
+ status: i % 2 === 0 ? 'active' : 'inactive',
25
+ score: 70 + (i % 30),
26
+ source: 'salesforce',
27
+ ts: `2026-06-${String((i % 28) + 1).padStart(2, '0')}T00:00:00Z`,
28
+ }))
29
+ );
30
+ }
31
+
32
+ function makeMessages(toolResultContent) {
33
+ return [
34
+ { role: 'system', content: 'You are a helpful assistant.' },
35
+ { role: 'user', content: 'What signals are there?' },
36
+ {
37
+ role: 'assistant',
38
+ content: [{ type: 'tool_use', id: 'tu_001', name: 'get_signals', input: {} }],
39
+ },
40
+ {
41
+ role: 'user',
42
+ content: [
43
+ {
44
+ type: 'tool_result',
45
+ tool_use_id: 'tu_001',
46
+ content: toolResultContent,
47
+ },
48
+ ],
49
+ },
50
+ ];
51
+ }
52
+
53
+ // ── detectContentType ─────────────────────────────────────────────────────────
54
+
55
+ describe('detectContentType', () => {
56
+ it('detects JSON_ARRAY for array input', () => {
57
+ expect(detectContentType('[{"id":1},{"id":2}]')).toBe('JSON_ARRAY');
58
+ });
59
+
60
+ it('detects JSON_OBJECT for object input', () => {
61
+ expect(detectContentType('{"key":"value"}')).toBe('JSON_OBJECT');
62
+ });
63
+
64
+ it('detects SOURCE_CODE for backtick code block', () => {
65
+ expect(detectContentType('```js\nconsole.log("hi")\n```')).toBe('SOURCE_CODE');
66
+ });
67
+
68
+ it('detects SOURCE_CODE for function keyword', () => {
69
+ expect(detectContentType('Some text\nfunction foo() { return 1; }')).toBe('SOURCE_CODE');
70
+ });
71
+
72
+ it('detects PLAIN_TEXT for regular prose', () => {
73
+ expect(detectContentType('The agent should focus on high-priority leads.')).toBe('PLAIN_TEXT');
74
+ });
75
+
76
+ it('returns PLAIN_TEXT for malformed JSON that starts with [', () => {
77
+ expect(detectContentType('[not valid json')).toBe('PLAIN_TEXT');
78
+ });
79
+ });
80
+
81
+ // ── ContentRouter ─────────────────────────────────────────────────────────────
82
+
83
+ describe('ContentRouter — protection gates', () => {
84
+ let router;
85
+ let store;
86
+ beforeEach(() => {
87
+ router = new ContentRouter();
88
+ store = new CcrStore(60);
89
+ });
90
+ afterEach(() => { store.destroy(); });
91
+
92
+ it('returns passthrough for content shorter than 500 chars', () => {
93
+ const short = makeUniformJson(3); // tiny array
94
+ expect(short.length).toBeLessThan(500);
95
+ const result = router.route(short, 'some_tool', store);
96
+ expect(result.compressed).toBe(false);
97
+ expect(result.tokensSaved).toBe(0);
98
+ });
99
+
100
+ it('never compresses headroom_retrieve tool results (infinite loop prevention)', () => {
101
+ const large = makeUniformJson(30);
102
+ expect(large.length).toBeGreaterThan(500);
103
+ const result = router.route(large, 'headroom_retrieve', store);
104
+ expect(result.compressed).toBe(false);
105
+ expect(result.strategy).toBe('passthrough');
106
+ });
107
+
108
+ it('does not compress small error outputs (<=8000 chars + error keyword)', () => {
109
+ const errorOut = JSON.stringify({ error: 'timeout', traceback: 'at line 42' }) + ' '.repeat(100);
110
+ const result = router.route(errorOut, 'bash', store);
111
+ // Passthrough because: contains "error" AND length <= 8000
112
+ expect(result.compressed).toBe(false);
113
+ });
114
+
115
+ it('compresses large error-containing payloads (>8000 chars)', () => {
116
+ // > 8000 chars with error key — protection does NOT apply
117
+ const largeWithError = JSON.stringify(
118
+ Array.from({ length: 30 }, (_, i) => ({
119
+ id: `e-${i}`,
120
+ type: 'signal',
121
+ status: 'active',
122
+ score: 80,
123
+ source: 'crm',
124
+ ts: `2026-06-${String(i % 28 + 1).padStart(2, '0')}T00:00:00Z`,
125
+ extra: 'padding'.repeat(20),
126
+ }))
127
+ );
128
+ expect(largeWithError.length).toBeGreaterThan(8000);
129
+ const result = router.route(largeWithError, 'get_signals', store);
130
+ // Should be compressed (not a small error output)
131
+ expect(result.compressed).toBe(true);
132
+ });
133
+ });
134
+
135
+ describe('ContentRouter — compression', () => {
136
+ let router;
137
+ let store;
138
+ beforeEach(() => {
139
+ router = new ContentRouter();
140
+ store = new CcrStore(60);
141
+ });
142
+ afterEach(() => { store.destroy(); });
143
+
144
+ it('compresses a large JSON_ARRAY to lossless-csv', () => {
145
+ const content = makeUniformJson(30);
146
+ const result = router.route(content, 'get_signals', store);
147
+ expect(result.compressed).toBe(true);
148
+ expect(result.contentType).toBe('JSON_ARRAY');
149
+ expect(result.tokensSaved).toBeGreaterThan(0);
150
+ });
151
+
152
+ it('returns passthrough for PLAIN_TEXT', () => {
153
+ const text = 'The agent should focus on high-priority leads. '.repeat(30);
154
+ expect(text.length).toBeGreaterThan(500);
155
+ const result = router.route(text, 'recall', store);
156
+ expect(result.compressed).toBe(false);
157
+ });
158
+
159
+ it('returns passthrough for JSON_OBJECT', () => {
160
+ const obj = JSON.stringify({ items: [1, 2, 3], count: 3, total: 6 }) + ' '.repeat(600);
161
+ const result = router.route(obj, 'get_stats', store);
162
+ expect(result.compressed).toBe(false);
163
+ });
164
+ });
165
+
166
+ // ── CompressionPipeline ───────────────────────────────────────────────────────
167
+
168
+ describe('CompressionPipeline — message routing', () => {
169
+ let pipeline;
170
+ let store;
171
+ beforeEach(() => {
172
+ pipeline = new CompressionPipeline();
173
+ store = new CcrStore(60);
174
+ });
175
+ afterEach(() => { store.destroy(); });
176
+
177
+ it('compresses tool_result blocks in user messages', () => {
178
+ const messages = makeMessages(makeUniformJson(30));
179
+ const result = pipeline.compress(messages, store);
180
+ expect(result.tokensSaved).toBeGreaterThan(0);
181
+ expect(result.transformsApplied.length).toBeGreaterThan(0);
182
+ });
183
+
184
+ it('does NOT mutate original messages array', () => {
185
+ const messages = makeMessages(makeUniformJson(30));
186
+ const originalJson = JSON.stringify(messages);
187
+ pipeline.compress(messages, store);
188
+ expect(JSON.stringify(messages)).toBe(originalJson);
189
+ });
190
+
191
+ it('skips system messages (preserves prompt cache stability)', () => {
192
+ const systemContent = 'You are a helpful assistant.';
193
+ const messages = [
194
+ { role: 'system', content: systemContent },
195
+ {
196
+ role: 'user',
197
+ content: [
198
+ { type: 'tool_result', tool_use_id: 'tu_1', content: makeUniformJson(30) },
199
+ ],
200
+ },
201
+ ];
202
+ const result = pipeline.compress(messages, store);
203
+ // System message content must be unchanged
204
+ expect(result.messages[0].content).toBe(systemContent);
205
+ });
206
+
207
+ it('skips non-tool_result blocks in user messages', () => {
208
+ const messages = [
209
+ {
210
+ role: 'user',
211
+ content: [
212
+ { type: 'text', text: 'What are the signals?' },
213
+ { type: 'tool_result', tool_use_id: 'tu_1', content: makeUniformJson(30) },
214
+ ],
215
+ },
216
+ ];
217
+ const result = pipeline.compress(messages, store);
218
+ // Text block must be unchanged
219
+ expect(result.messages[0].content[0].type).toBe('text');
220
+ expect(result.messages[0].content[0].text).toBe('What are the signals?');
221
+ // tool_result block should be compressed
222
+ expect(result.tokensSaved).toBeGreaterThan(0);
223
+ });
224
+
225
+ it('returns compressionRatio < 1 when compression succeeds', () => {
226
+ const messages = makeMessages(makeUniformJson(30));
227
+ const result = pipeline.compress(messages, store);
228
+ expect(result.compressionRatio).toBeGreaterThan(0);
229
+ expect(result.compressionRatio).toBeLessThan(1);
230
+ });
231
+
232
+ it('returns compressionRatio = 1 for uncompressible input', () => {
233
+ // String content that's too short to compress
234
+ const messages = [
235
+ { role: 'user', content: [{ type: 'tool_result', tool_use_id: 'tu_1', content: 'hi' }] },
236
+ ];
237
+ const result = pipeline.compress(messages, store);
238
+ expect(result.tokensSaved).toBe(0);
239
+ expect(result.compressionRatio).toBe(1);
240
+ });
241
+
242
+ it('collects ccrHashes when lossy-sample is applied', () => {
243
+ // Sparse array to trigger lossy-sample
244
+ const pad = 'x'.repeat(200);
245
+ const sparse = JSON.stringify(
246
+ Array.from({ length: 40 }, (_, i) => {
247
+ const o = { id: `item-${String(i).padStart(5, '0')}` };
248
+ if (i % 2 === 0) o.payload = `data_${i}_${pad}`;
249
+ if (i % 3 === 0) o.stage = 'active';
250
+ if (i % 4 === 0) o.value = i * 100;
251
+ if (i % 5 === 0) o.ts = `2026-06-${String((i % 28) + 1).padStart(2, '0')}`;
252
+ if (i % 6 === 0) o.owner = `rep-${i % 8}`;
253
+ if (i % 7 === 0) o.src = 'crm';
254
+ return o;
255
+ })
256
+ );
257
+ const messages = makeMessages(sparse);
258
+ const result = pipeline.compress(messages, store);
259
+ if (result.transformsApplied.includes('lossy-sample')) {
260
+ expect(result.ccrHashes.length).toBeGreaterThan(0);
261
+ }
262
+ });
263
+
264
+ it('handles empty messages array', () => {
265
+ const result = pipeline.compress([], store);
266
+ expect(result.messages).toEqual([]);
267
+ expect(result.tokensSaved).toBe(0);
268
+ expect(result.compressionRatio).toBe(1);
269
+ });
270
+
271
+ it('handles messages with string content (not array)', () => {
272
+ const messages = [
273
+ { role: 'user', content: 'simple string content' },
274
+ { role: 'assistant', content: 'simple assistant response' },
275
+ ];
276
+ const result = pipeline.compress(messages, store);
277
+ expect(result.messages[0].content).toBe('simple string content');
278
+ expect(result.tokensSaved).toBe(0);
279
+ });
280
+ });