entroly-wasm 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +67 -0
- package/js/agentskills_export.js +129 -0
- package/js/auto_index.js +262 -0
- package/js/autotune.js +627 -0
- package/js/checkpoint.js +108 -0
- package/js/cli.js +321 -0
- package/js/cogops.js +513 -0
- package/js/config.js +53 -0
- package/js/distill.js +155 -0
- package/js/evolution_daemon.js +335 -0
- package/js/federation.js +444 -0
- package/js/gateways.js +170 -0
- package/js/multimodal.js +64 -0
- package/js/repo_map.js +112 -0
- package/js/server.js +465 -0
- package/js/skills.js +124 -0
- package/js/value_tracker.js +181 -0
- package/js/vault.js +237 -0
- package/js/vault_observer.js +129 -0
- package/js/workspace.js +116 -0
- package/package.json +51 -0
- package/pkg/entroly_wasm.d.ts +156 -0
- package/pkg/entroly_wasm.js +468 -0
- package/pkg/entroly_wasm_bg.wasm +0 -0
package/js/distill.js
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Response Distillation — Output-Side Token Optimization (JavaScript)
|
|
3
|
+
* ====================================================================
|
|
4
|
+
*
|
|
5
|
+
* Strips filler, pleasantries, hedging, and meta-commentary from LLM
|
|
6
|
+
* responses while preserving all code blocks and technical content.
|
|
7
|
+
*
|
|
8
|
+
* Grounded in:
|
|
9
|
+
* - Selective Context (Li et al., EMNLP 2023) — self-information scoring
|
|
10
|
+
* - TRIM (arXiv 2025) — omit inferable words
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* const { distillResponse } = require('entroly-wasm/js/distill');
|
|
14
|
+
* const { text, originalTokens, compressedTokens } = distillResponse(llmOutput);
|
|
15
|
+
*
|
|
16
|
+
* @module distill
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
'use strict';
|
|
20
|
+
|
|
21
|
+
// ── Filler patterns: high-frequency, zero-information phrases ──
|
|
22
|
+
|
|
23
|
+
const FILLER_PATTERNS = [
|
|
24
|
+
// Pleasantries (zero technical content)
|
|
25
|
+
[/^(?:Sure!?\s*|Of course!?\s*|Absolutely!?\s*|Great question!?\s*|Happy to help!?\s*|I'd be happy to help\.?\s*|Let me help you with that\.?\s*|No problem!?\s*|Certainly!?\s*|You're welcome!?\s*)/i, ''],
|
|
26
|
+
// Preamble ("I'm going to...")
|
|
27
|
+
[/^(?:Let me |I'll |I will |I'm going to |I can |I need to )(?:take a look|look at|review|examine|analyze|check|investigate)\b[^.]*\.\s*/i, ''],
|
|
28
|
+
// Meta-commentary ("Here's what I found")
|
|
29
|
+
[/^(?:Here(?:'s| is) (?:what|how|the|my|a|an)\b[^:]*:\s*)/i, ''],
|
|
30
|
+
// Hedging (near-zero information)
|
|
31
|
+
[/\b(?:I think |I believe |It seems like |It looks like |It appears that |As far as I can tell,?\s*|From what I can see,?\s*|If I understand correctly,?\s*)/gi, ''],
|
|
32
|
+
// Filler transitions
|
|
33
|
+
[/^(?:Now,?\s*|Next,?\s*|Then,?\s*|After that,?\s*|Moving on,?\s*|With that said,?\s*|That being said,?\s*|Having said that,?\s*)(?:let(?:'s| us) )/i, ''],
|
|
34
|
+
// Closing pleasantries
|
|
35
|
+
[/(?:Let me know if (?:you (?:have|need)|there(?:'s| is)|that)\b[^.!]*[.!]?\s*$)/i, ''],
|
|
36
|
+
[/(?:Feel free to (?:ask|reach out|let me know)\b[^.!]*[.!]?\s*$)/i, ''],
|
|
37
|
+
[/(?:Hope (?:this|that) helps!?\s*$)/i, ''],
|
|
38
|
+
[/(?:Is there anything else\b[^.?!]*[.?!]?\s*$)/i, ''],
|
|
39
|
+
// Redundant acknowledgments
|
|
40
|
+
[/^(?:I see\.|I understand\.|Got it\.|Right\.|Okay\.)\s*/i, ''],
|
|
41
|
+
// Verbose connectors
|
|
42
|
+
[/\bIn order to\b/i, 'To'],
|
|
43
|
+
[/\bDue to the fact that\b/i, 'Because'],
|
|
44
|
+
[/\bAt this point in time\b/i, 'Now'],
|
|
45
|
+
[/\bFor the purpose of\b/i, 'For'],
|
|
46
|
+
[/\bIn the event that\b/i, 'If'],
|
|
47
|
+
[/\bWith regard to\b/i, 'About'],
|
|
48
|
+
[/\bIt is important to note that\b/i, 'Note:'],
|
|
49
|
+
[/\bAs (?:mentioned|noted|stated) (?:earlier|above|previously),?\s*/i, ''],
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
// Lines that are pure filler (entire line is fluff)
|
|
53
|
+
const PURE_FILLER = /^(?:Sure(?:,| thing).*|I(?:'d| would) (?:be happy|love) to.*|Let me (?:know|explain|walk you through).*|(?:Here(?:'s| is) (?:a|the) (?:summary|breakdown|overview|explanation).*)|(?:To (?:summarize|recap|sum up):?.*)|(?:In (?:summary|conclusion):?.*))$/i;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Apply response distillation to LLM output text.
|
|
57
|
+
*
|
|
58
|
+
* @param {string} text - Raw LLM response text
|
|
59
|
+
* @param {Object} [options]
|
|
60
|
+
* @param {'lite'|'full'|'ultra'} [options.mode='full'] - Compression intensity
|
|
61
|
+
* @returns {{ text: string, originalTokens: number, compressedTokens: number }}
|
|
62
|
+
*/
|
|
63
|
+
function distillResponse(text, options = {}) {
|
|
64
|
+
const mode = options.mode || 'full';
|
|
65
|
+
|
|
66
|
+
if (!text || text.length < 50) {
|
|
67
|
+
const count = text ? text.split(/\s+/).filter(Boolean).length : 0;
|
|
68
|
+
return { text: text || '', originalTokens: count, compressedTokens: count };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const originalTokens = text.split(/\s+/).filter(Boolean).length;
|
|
72
|
+
|
|
73
|
+
// Split into code blocks and prose — NEVER touch code blocks
|
|
74
|
+
const parts = text.split(/(```[\s\S]*?```)/);
|
|
75
|
+
const compressed = parts.map(part => {
|
|
76
|
+
if (part.startsWith('```')) return part;
|
|
77
|
+
return _compressProse(part, mode);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
let result = compressed.join('');
|
|
81
|
+
result = result.replace(/\n{3,}/g, '\n\n').trim();
|
|
82
|
+
|
|
83
|
+
const compressedTokens = result ? result.split(/\s+/).filter(Boolean).length : 0;
|
|
84
|
+
|
|
85
|
+
return { text: result, originalTokens, compressedTokens };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Compress a prose section (not inside a code block).
|
|
90
|
+
* @private
|
|
91
|
+
*/
|
|
92
|
+
function _compressProse(text, mode) {
|
|
93
|
+
const lines = text.split('\n');
|
|
94
|
+
const output = [];
|
|
95
|
+
|
|
96
|
+
for (const line of lines) {
|
|
97
|
+
const stripped = line.trim();
|
|
98
|
+
|
|
99
|
+
// Skip pure filler lines
|
|
100
|
+
if (stripped && PURE_FILLER.test(stripped)) continue;
|
|
101
|
+
|
|
102
|
+
let compressed = stripped;
|
|
103
|
+
|
|
104
|
+
if (mode === 'full' || mode === 'ultra') {
|
|
105
|
+
for (const [pattern, replacement] of FILLER_PATTERNS) {
|
|
106
|
+
compressed = compressed.replace(pattern, replacement);
|
|
107
|
+
}
|
|
108
|
+
} else if (mode === 'lite') {
|
|
109
|
+
// Lite: only first 4 patterns (pleasantries + preamble)
|
|
110
|
+
for (const [pattern, replacement] of FILLER_PATTERNS.slice(0, 4)) {
|
|
111
|
+
compressed = compressed.replace(pattern, replacement);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Ultra: also strip articles and filler words
|
|
116
|
+
if (mode === 'ultra') {
|
|
117
|
+
compressed = compressed.replace(/\b(?:the|a|an|just|simply|basically|essentially)\s+/g, '');
|
|
118
|
+
compressed = compressed.replace(/\s{2,}/g, ' ');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (compressed.trim()) {
|
|
122
|
+
const leading = line.length - line.trimStart().length;
|
|
123
|
+
output.push(' '.repeat(leading) + compressed.trim());
|
|
124
|
+
} else if (!stripped) {
|
|
125
|
+
output.push('');
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return output.join('\n');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Distill a single SSE content delta for streaming responses.
|
|
134
|
+
*
|
|
135
|
+
* @param {string} chunk - SSE content delta text
|
|
136
|
+
* @param {Object} [options]
|
|
137
|
+
* @param {'lite'|'full'|'ultra'} [options.mode='full']
|
|
138
|
+
* @returns {string} Distilled chunk
|
|
139
|
+
*/
|
|
140
|
+
function distillSSEChunk(chunk, options = {}) {
|
|
141
|
+
const mode = options.mode || 'full';
|
|
142
|
+
|
|
143
|
+
if (!chunk || chunk.length < 10) return chunk;
|
|
144
|
+
if (chunk.includes('```') || chunk.startsWith(' ')) return chunk;
|
|
145
|
+
|
|
146
|
+
let result = chunk;
|
|
147
|
+
const patternsToApply = FILLER_PATTERNS.slice(0, 8);
|
|
148
|
+
for (const [pattern, replacement] of patternsToApply) {
|
|
149
|
+
result = result.replace(pattern, replacement);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return result;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
module.exports = { distillResponse, distillSSEChunk };
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Evolution Daemon — Zero-Token Autonomous Self-Improvement (JavaScript)
|
|
3
|
+
* ======================================================================
|
|
4
|
+
*
|
|
5
|
+
* Background daemon that orchestrates:
|
|
6
|
+
* 1. Dreaming Loop — idle-time weight optimization via self-play
|
|
7
|
+
* 2. Federation — contribute + merge global weights via GitHub
|
|
8
|
+
*
|
|
9
|
+
* Mirrors the Python EvolutionDaemon but adapted for Node.js event loop.
|
|
10
|
+
*
|
|
11
|
+
* @module evolution_daemon
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
'use strict';
|
|
15
|
+
|
|
16
|
+
const { FeedbackJournal, optimize, WEIGHT_KEYS, DEFAULTS } = require('./autotune');
|
|
17
|
+
|
|
18
|
+
// ── Dreaming Loop ──
|
|
19
|
+
|
|
20
|
+
const IDLE_THRESHOLD_MS = 60000; // 60s idle before dreaming
|
|
21
|
+
const DREAM_PERTURBATION = 0.08; // Weight perturbation magnitude
|
|
22
|
+
const DREAM_MAX_TRIALS = 5; // Trials per dream cycle
|
|
23
|
+
|
|
24
|
+
class DreamingLoop {
|
|
25
|
+
/**
|
|
26
|
+
* @param {FeedbackJournal} journal
|
|
27
|
+
* @param {Object} [options]
|
|
28
|
+
* @param {number} [options.idleThresholdMs]
|
|
29
|
+
*/
|
|
30
|
+
constructor(journal, options = {}) {
|
|
31
|
+
this._journal = journal;
|
|
32
|
+
this._idleThreshold = options.idleThresholdMs || IDLE_THRESHOLD_MS;
|
|
33
|
+
this._lastActivity = Date.now();
|
|
34
|
+
this._stats = { cycles: 0, improvements: 0, totalTrials: 0 };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Record user activity — resets idle timer. */
|
|
38
|
+
recordActivity() {
|
|
39
|
+
this._lastActivity = Date.now();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Check if system has been idle long enough to dream. */
|
|
43
|
+
shouldDream() {
|
|
44
|
+
return (Date.now() - this._lastActivity) >= this._idleThreshold;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Run one dream cycle: perturb weights, evaluate, keep improvements.
|
|
49
|
+
*
|
|
50
|
+
* Uses counterfactual self-play:
|
|
51
|
+
* 1. Load best weights from journal history
|
|
52
|
+
* 2. Generate random perturbations
|
|
53
|
+
* 3. Score each variant against journal episodes
|
|
54
|
+
* 4. If any beats current → adopt it
|
|
55
|
+
*
|
|
56
|
+
* @param {Object} currentWeights - Current {w_r, w_f, w_s, w_e}
|
|
57
|
+
* @returns {{ status: string, improvements: number, bestScore: number }}
|
|
58
|
+
*/
|
|
59
|
+
runDreamCycle(currentWeights) {
|
|
60
|
+
const episodes = this._journal.load();
|
|
61
|
+
if (episodes.length < 5) {
|
|
62
|
+
return { status: 'insufficient_data', improvements: 0, bestScore: 0 };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
this._stats.cycles++;
|
|
66
|
+
let bestWeights = { ...currentWeights };
|
|
67
|
+
let bestScore = this._scoreWeights(currentWeights, episodes);
|
|
68
|
+
let improvements = 0;
|
|
69
|
+
|
|
70
|
+
for (let trial = 0; trial < DREAM_MAX_TRIALS; trial++) {
|
|
71
|
+
this._stats.totalTrials++;
|
|
72
|
+
|
|
73
|
+
// Generate perturbation (Gaussian)
|
|
74
|
+
const candidate = {};
|
|
75
|
+
for (const k of WEIGHT_KEYS) {
|
|
76
|
+
const noise = (Math.random() + Math.random() + Math.random() - 1.5) * DREAM_PERTURBATION;
|
|
77
|
+
candidate[k] = Math.max(0.05, Math.min(0.80, (bestWeights[k] || 0.25) + noise));
|
|
78
|
+
}
|
|
79
|
+
// Normalize to sum=1
|
|
80
|
+
const sum = WEIGHT_KEYS.reduce((s, k) => s + candidate[k], 0);
|
|
81
|
+
for (const k of WEIGHT_KEYS) candidate[k] = Math.round(candidate[k] / sum * 10000) / 10000;
|
|
82
|
+
|
|
83
|
+
const score = this._scoreWeights(candidate, episodes);
|
|
84
|
+
if (score > bestScore) {
|
|
85
|
+
bestScore = score;
|
|
86
|
+
bestWeights = { ...candidate };
|
|
87
|
+
improvements++;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (improvements > 0) {
|
|
92
|
+
this._stats.improvements += improvements;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
status: 'completed',
|
|
97
|
+
improvements,
|
|
98
|
+
bestScore: Math.round(bestScore * 1000) / 1000,
|
|
99
|
+
bestWeights,
|
|
100
|
+
trials: DREAM_MAX_TRIALS,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Score a weight configuration against historical episodes.
|
|
106
|
+
* Simulates reward: higher reward episodes → higher agreement with weights.
|
|
107
|
+
* @private
|
|
108
|
+
*/
|
|
109
|
+
_scoreWeights(weights, episodes) {
|
|
110
|
+
let totalScore = 0;
|
|
111
|
+
let count = 0;
|
|
112
|
+
|
|
113
|
+
for (const ep of episodes) {
|
|
114
|
+
if (!ep.w || ep.r === undefined) continue;
|
|
115
|
+
|
|
116
|
+
// Cosine similarity between candidate weights and episode's weights
|
|
117
|
+
const epW = ep.w;
|
|
118
|
+
let dot = 0, normA = 0, normB = 0;
|
|
119
|
+
for (const k of WEIGHT_KEYS) {
|
|
120
|
+
const a = weights[k] || 0.25;
|
|
121
|
+
const b = epW[k] || epW[k.replace('w_', 'w_')] || 0.25;
|
|
122
|
+
dot += a * b;
|
|
123
|
+
normA += a * a;
|
|
124
|
+
normB += b * b;
|
|
125
|
+
}
|
|
126
|
+
const similarity = dot / (Math.sqrt(normA) * Math.sqrt(normB) + 1e-8);
|
|
127
|
+
|
|
128
|
+
// Reward-weighted: positive episodes attract, negative repel
|
|
129
|
+
totalScore += ep.r * similarity;
|
|
130
|
+
count++;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return count > 0 ? totalScore / count : 0;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
get stats() { return { ...this._stats }; }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
// ── Evolution Daemon ──
|
|
141
|
+
|
|
142
|
+
class EvolutionDaemon {
|
|
143
|
+
/**
|
|
144
|
+
* @param {Object} options
|
|
145
|
+
* @param {string} options.checkpointDir - Directory for state
|
|
146
|
+
* @param {Object} [options.engine] - WASM engine instance
|
|
147
|
+
* @param {number} [options.pollIntervalMs=30000]
|
|
148
|
+
*/
|
|
149
|
+
constructor(options = {}) {
|
|
150
|
+
this._checkpointDir = options.checkpointDir || '.entroly';
|
|
151
|
+
this._engine = options.engine || null;
|
|
152
|
+
this._pollInterval = options.pollIntervalMs || 30000;
|
|
153
|
+
|
|
154
|
+
// Journal
|
|
155
|
+
this._journal = new FeedbackJournal(this._checkpointDir);
|
|
156
|
+
|
|
157
|
+
// Dreaming
|
|
158
|
+
this._dreaming = new DreamingLoop(this._journal);
|
|
159
|
+
|
|
160
|
+
// Federation (lazy loaded)
|
|
161
|
+
this._federation = null;
|
|
162
|
+
this._transport = null;
|
|
163
|
+
this._federationCycleCounter = 0;
|
|
164
|
+
|
|
165
|
+
// State
|
|
166
|
+
this._timer = null;
|
|
167
|
+
this._stats = {
|
|
168
|
+
dreamCycles: 0,
|
|
169
|
+
federationContributions: 0,
|
|
170
|
+
federationMerges: 0,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Initialize federation (call after construction).
|
|
176
|
+
*/
|
|
177
|
+
async initFederation() {
|
|
178
|
+
try {
|
|
179
|
+
const { FederationClient, GitHubTransport } = require('./federation');
|
|
180
|
+
this._federation = new FederationClient({ dataDir: this._checkpointDir });
|
|
181
|
+
|
|
182
|
+
if (this._federation.enabled) {
|
|
183
|
+
this._transport = new GitHubTransport();
|
|
184
|
+
|
|
185
|
+
// Sync on startup
|
|
186
|
+
try {
|
|
187
|
+
const packets = await this._transport.pull(48);
|
|
188
|
+
let saved = 0;
|
|
189
|
+
for (const packet of packets) {
|
|
190
|
+
if (this._federation.saveContribution(packet)) saved++;
|
|
191
|
+
}
|
|
192
|
+
if (saved > 0) {
|
|
193
|
+
this._federation.mergeGlobal();
|
|
194
|
+
this._stats.federationMerges++;
|
|
195
|
+
}
|
|
196
|
+
} catch {}
|
|
197
|
+
}
|
|
198
|
+
} catch {}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Record user activity — resets dreaming idle timer. */
|
|
202
|
+
recordActivity() {
|
|
203
|
+
this._dreaming.recordActivity();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Start the daemon (non-blocking, uses setInterval).
|
|
208
|
+
*/
|
|
209
|
+
start() {
|
|
210
|
+
if (this._timer) return;
|
|
211
|
+
|
|
212
|
+
this._timer = setInterval(() => {
|
|
213
|
+
try {
|
|
214
|
+
this.runOnce();
|
|
215
|
+
} catch (e) {
|
|
216
|
+
console.error(`[evolution] Error: ${e.message}`);
|
|
217
|
+
}
|
|
218
|
+
}, this._pollInterval);
|
|
219
|
+
|
|
220
|
+
// Make timer non-blocking (don't prevent process exit)
|
|
221
|
+
if (this._timer.unref) this._timer.unref();
|
|
222
|
+
|
|
223
|
+
console.error('[evolution] Daemon started');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Stop the daemon. */
|
|
227
|
+
stop() {
|
|
228
|
+
if (this._timer) {
|
|
229
|
+
clearInterval(this._timer);
|
|
230
|
+
this._timer = null;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Execute one daemon cycle.
|
|
236
|
+
*/
|
|
237
|
+
runOnce() {
|
|
238
|
+
const result = { dream: null, federation: null };
|
|
239
|
+
|
|
240
|
+
// ── Phase 1: Dreaming ──
|
|
241
|
+
if (this._dreaming.shouldDream()) {
|
|
242
|
+
const currentWeights = this._getCurrentWeights();
|
|
243
|
+
const dreamResult = this._dreaming.runDreamCycle(currentWeights);
|
|
244
|
+
result.dream = dreamResult;
|
|
245
|
+
|
|
246
|
+
if (dreamResult.status === 'completed') {
|
|
247
|
+
this._stats.dreamCycles++;
|
|
248
|
+
|
|
249
|
+
// Apply improved weights to engine
|
|
250
|
+
if (dreamResult.improvements > 0 && dreamResult.bestWeights && this._engine) {
|
|
251
|
+
const w = dreamResult.bestWeights;
|
|
252
|
+
try {
|
|
253
|
+
this._engine.set_weights(w.w_r, w.w_f, w.w_s, w.w_e);
|
|
254
|
+
} catch {}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ── Phase 2: Federation ──
|
|
258
|
+
if (dreamResult.improvements > 0 && this._federation && this._federation.enabled) {
|
|
259
|
+
this._federationCycleCounter++;
|
|
260
|
+
this._handleFederation(dreamResult);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return result;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** @private */
|
|
269
|
+
_getCurrentWeights() {
|
|
270
|
+
// Try to read from engine, fall back to defaults
|
|
271
|
+
if (this._engine && typeof this._engine.get_weights === 'function') {
|
|
272
|
+
try {
|
|
273
|
+
const w = this._engine.get_weights();
|
|
274
|
+
return {
|
|
275
|
+
w_r: w.recency ?? DEFAULTS.w_r,
|
|
276
|
+
w_f: w.frequency ?? DEFAULTS.w_f,
|
|
277
|
+
w_s: w.semantic_sim ?? DEFAULTS.w_s,
|
|
278
|
+
w_e: w.entropy ?? DEFAULTS.w_e,
|
|
279
|
+
};
|
|
280
|
+
} catch {}
|
|
281
|
+
}
|
|
282
|
+
return { ...DEFAULTS };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** @private */
|
|
286
|
+
_handleFederation(dreamResult) {
|
|
287
|
+
if (!this._federation || !this._transport) return;
|
|
288
|
+
|
|
289
|
+
try {
|
|
290
|
+
// Contribute improved weights
|
|
291
|
+
const w = dreamResult.bestWeights;
|
|
292
|
+
const packet = this._federation.contribute(
|
|
293
|
+
'js_project', // archetype
|
|
294
|
+
w,
|
|
295
|
+
this._journal.count(),
|
|
296
|
+
Math.min(1.0, this._journal.count() / 20),
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
if (packet && this._transport.canWrite) {
|
|
300
|
+
// Async push — fire and forget
|
|
301
|
+
this._transport.push(packet).then(ok => {
|
|
302
|
+
if (ok) this._stats.federationContributions++;
|
|
303
|
+
}).catch(() => {});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Merge global every 10 cycles
|
|
307
|
+
if (this._federationCycleCounter % 10 === 0) {
|
|
308
|
+
this._transport.pull(48).then(packets => {
|
|
309
|
+
let saved = 0;
|
|
310
|
+
for (const p of packets) {
|
|
311
|
+
if (this._federation.saveContribution(p)) saved++;
|
|
312
|
+
}
|
|
313
|
+
if (saved > 0) {
|
|
314
|
+
this._federation.mergeGlobal();
|
|
315
|
+
this._stats.federationMerges++;
|
|
316
|
+
|
|
317
|
+
// Apply merged weights
|
|
318
|
+
const global = this._federation.getGlobalWeights('js_project');
|
|
319
|
+
if (global && this._engine) {
|
|
320
|
+
const gw = global.weights;
|
|
321
|
+
try {
|
|
322
|
+
this._engine.set_weights(gw.w_r, gw.w_f, gw.w_s, gw.w_e);
|
|
323
|
+
} catch {}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}).catch(() => {});
|
|
327
|
+
}
|
|
328
|
+
} catch {}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
get stats() { return { ...this._stats, dreaming: this._dreaming.stats }; }
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
module.exports = { EvolutionDaemon, DreamingLoop };
|