@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.
- package/agents/business/talisman-ceo.md +183 -0
- package/agents/business/talisman-comms.md +257 -0
- package/agents/business/talisman-cto.md +153 -0
- package/agents/business/talisman-finance.md +246 -0
- package/agents/business/talisman-marketing.md +240 -0
- package/agents/business/talisman-sales.md +242 -0
- package/agents/business/talisman-support.md +236 -0
- package/bin/helios-rpc.js +19 -0
- package/daemon/adapters/helios-rpc-adapter.js +5 -12
- package/daemon/context-enrichment.js +27 -0
- package/daemon/helios-api.js +290 -45
- package/daemon/helios-company-daemon.js +160 -50
- package/daemon/lib/blast-radius-analyzer.js +75 -0
- package/daemon/lib/domain-bootstrap-orchestrator.js +267 -0
- package/daemon/lib/forensic-log.js +113 -0
- package/daemon/lib/goal-research-pipeline.js +644 -0
- package/daemon/lib/harada/cascade-judge.js +84 -1
- package/daemon/lib/harada/cascade-research-dispatcher.js +282 -0
- package/daemon/lib/harada/pillar-dispatcher.js +23 -2
- package/daemon/lib/hbo-bridge.js +73 -5
- package/daemon/lib/headroom-middleware.js +129 -0
- package/daemon/lib/headroom-proxy-manager.js +309 -0
- package/daemon/lib/intelligence/department-page-generator.js +46 -1
- package/daemon/lib/interpretation-engine.js +92 -0
- package/daemon/lib/mental-model-cache.js +96 -0
- package/daemon/lib/project-factory.js +47 -0
- package/daemon/lib/session-log-reader.js +93 -0
- package/daemon/lib/standard-work-bootstrap.js +87 -1
- package/daemon/lib/task-completion-processor.js +12 -0
- package/daemon/package.json +2 -1
- package/daemon/routes/agents.js +51 -6
- package/daemon/routes/channels.js +116 -2
- package/daemon/routes/crm.js +85 -0
- package/daemon/routes/dashboard.js +62 -16
- package/daemon/routes/dept.js +10 -1
- package/daemon/routes/email-triage.js +19 -10
- package/daemon/routes/hbo.js +367 -13
- package/daemon/routes/hed.js +133 -0
- package/daemon/routes/inbox.js +397 -8
- package/daemon/routes/project.js +392 -9
- package/daemon/schema-definitions.js +10 -0
- package/daemon/schema-migrations-hbo.js +10 -0
- package/daemon/schema-migrations-proj.js +22 -0
- package/extensions/__tests__/codebase-index.test.ts +73 -0
- package/extensions/__tests__/extension-command-registration.test.ts +35 -0
- package/extensions/__tests__/git-push-guard.test.ts +68 -0
- package/extensions/context-compaction.ts +104 -76
- package/extensions/cortex/__tests__/cortex-core.test.ts +100 -0
- package/extensions/email/actions/draft-response.ts +21 -1
- package/extensions/email/auth/accounts.ts +5 -11
- package/extensions/email/auth/inbox-dog.ts +5 -2
- package/extensions/email/backfill.ts +20 -13
- package/extensions/email/providers/gmail.ts +164 -0
- package/extensions/email/providers/google-calendar.ts +34 -5
- package/extensions/helios-browser/__tests__/browser-routing.test.ts +57 -0
- package/extensions/helios-browser/backends/playwright.ts +3 -1
- package/extensions/helios-governance/__tests__/governance-gates.test.ts +40 -0
- package/extensions/helios-governance/__tests__/tournament-consumer.test.js +66 -0
- package/extensions/hema-dispatch-v3/headroom-compress.ts +103 -0
- package/extensions/hema-dispatch-v3/index.ts +33 -65
- package/extensions/interview/__tests__/server.test.ts +117 -0
- package/extensions/lib/helios-root.cjs +46 -0
- package/extensions/subagent-mesh/__tests__/handlers.test.ts +98 -0
- package/extensions/warm-tick/warm-tick-maintenance.ts +156 -0
- package/lib/__tests__/bulk-ingest.live.test.ts +66 -0
- package/lib/__tests__/crash-fixes.test.ts +49 -0
- package/lib/__tests__/maintenance-mission-wiring.test.ts +35 -0
- package/lib/broker/__tests__/jit-subscription.test.js +44 -1
- package/lib/broker/__tests__/lifecycle-channels.test.js +25 -1
- package/lib/compression/__tests__/ccr-store.test.js +138 -0
- package/lib/compression/__tests__/pipeline.test.js +280 -0
- package/lib/compression/__tests__/smart-crusher.test.js +242 -0
- package/lib/compression/dist/server.js +34 -1
- package/lib/compression/dist/start-server.js +77 -0
- package/lib/graph/learning/headroom-learn-bridge.js +175 -0
- package/lib/hbo-core-store.ts +71 -0
- package/lib/mission-loop/__tests__/research-handler.test.ts +143 -0
- package/lib/skill-sync.js +6 -1
- package/lib/startup-integrity.js +9 -2
- package/lib/triage-core/__tests__/classifier-fixture.test.ts +254 -0
- package/lib/triage-core/__tests__/classifier-post-norm.test.ts +1 -1
- package/lib/triage-core/__tests__/classifier.test.ts +45 -7
- package/lib/triage-core/__tests__/correction-detector.test.ts +36 -0
- package/lib/triage-core/__tests__/d6-dunbar-boost.test.ts +5 -5
- package/lib/triage-core/__tests__/orchestrator-pipeline.test.ts +107 -0
- package/lib/triage-core/__tests__/orchestrator.test.ts +113 -1
- package/lib/triage-core/__tests__/signals.test.ts +357 -0
- package/lib/triage-core/__tests__/sql-parity.test.ts +216 -0
- package/lib/triage-core/backfill-cost-estimator.ts +91 -0
- package/lib/triage-core/backfill-orchestrator.ts +119 -0
- package/lib/triage-core/classifier.ts +38 -6
- package/lib/triage-core/cos/cross-channel-escalation.ts +2 -3
- package/lib/triage-core/cos/response-debt.ts +2 -2
- package/lib/triage-core/graph/__tests__/batch-persistence.test.ts +283 -0
- package/lib/triage-core/graph/batch-persistence.ts +66 -2
- package/lib/triage-core/graph/betweenness-worker.js +75 -0
- package/lib/triage-core/graph/graph-rank-sql.ts +67 -0
- package/lib/triage-core/graph/persistence.ts +1 -1
- package/lib/triage-core/graph/schema-v2.ts +2 -0
- package/lib/triage-core/graph/schema.cypher +1 -0
- package/lib/triage-core/graph/triage-query.ts +1 -1
- package/lib/triage-core/learning.ts +15 -20
- package/lib/triage-core/mental-model/bedrock-config.ts +78 -0
- package/lib/triage-core/mental-model/cos-integration.ts +1 -1
- package/lib/triage-core/mental-model/entity-extractor.ts +51 -4
- package/lib/triage-core/mental-model/identity-resolver.ts +5 -5
- package/lib/triage-core/mental-model/model-assembler-sql.ts +200 -0
- package/lib/triage-core/mental-model/model-assembler.ts +16 -3
- package/lib/triage-core/orchestrator.ts +4 -4
- package/lib/triage-core/scheduled-sends.ts +39 -2
- package/lib/triage-core/signals/comms-style.ts +1 -1
- package/lib/triage-core/signals/cross-channel-escalation.ts +2 -2
- package/lib/triage-core/signals/favee-type.ts +6 -1
- package/lib/triage-core/signals/goal-relevance.ts +31 -2
- package/lib/triage-core/signals/personal-importance.ts +1 -1
- package/lib/triage-core/signals/referral-chain.ts +0 -1
- package/lib/triage-core/signals/relationship-decay.ts +4 -0
- package/lib/triage-core/signals/relationship-health.ts +6 -1
- package/lib/triage-core/signals/trajectory-signal.ts +38 -3
- package/lib/triage-core/tournament-runner.js +11 -1
- package/lib/triage-core/triage-llm-factory.ts +110 -0
- package/lib/triage-core/triage-local-llm.ts +145 -0
- package/lib/triage-core/triage-sql-store.ts +337 -0
- package/lib/triage-core/types.ts +2 -2
- package/lib/unified-graph.atomic.test.ts +52 -0
- package/lib/unified-graph.failure-categories.test.ts +55 -0
- package/package.json +10 -3
- package/prebuilds/darwin-arm64/better_sqlite3.node +0 -0
- package/prebuilds/linux-x64/better_sqlite3.node +0 -0
- package/prebuilds/win32-x64/better_sqlite3.node +0 -0
- package/skills/helios-bookkeeping/SKILL.md +321 -0
- package/skills/helios-briefer/SKILL.md +44 -0
- package/skills/helios-client-relations/SKILL.md +322 -0
- package/skills/helios-personal-triager/SKILL.md +45 -0
- package/skills/helios-recruitment/SKILL.md +317 -0
- package/skills/helios-relationship-nudger/SKILL.md +77 -0
- package/skills/helios-researcher/SKILL.md +44 -0
- package/skills/helios-scheduler/SKILL.md +58 -0
- package/skills/helios-tax-analyst/SKILL.md +280 -0
- 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
|
-
|
|
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
|
+
});
|