@evomap/evolver 1.28.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.
- package/LICENSE +22 -0
- package/README.md +290 -0
- package/README.zh-CN.md +236 -0
- package/SKILL.md +132 -0
- package/assets/gep/capsules.json +79 -0
- package/assets/gep/events.jsonl +7 -0
- package/assets/gep/genes.json +108 -0
- package/index.js +530 -0
- package/package.json +38 -0
- package/src/canary.js +13 -0
- package/src/evolve.js +1704 -0
- package/src/gep/a2a.js +173 -0
- package/src/gep/a2aProtocol.js +736 -0
- package/src/gep/analyzer.js +35 -0
- package/src/gep/assetCallLog.js +130 -0
- package/src/gep/assetStore.js +297 -0
- package/src/gep/assets.js +36 -0
- package/src/gep/bridge.js +71 -0
- package/src/gep/candidates.js +142 -0
- package/src/gep/contentHash.js +65 -0
- package/src/gep/deviceId.js +209 -0
- package/src/gep/envFingerprint.js +83 -0
- package/src/gep/hubReview.js +206 -0
- package/src/gep/hubSearch.js +237 -0
- package/src/gep/issueReporter.js +262 -0
- package/src/gep/llmReview.js +92 -0
- package/src/gep/memoryGraph.js +771 -0
- package/src/gep/memoryGraphAdapter.js +203 -0
- package/src/gep/mutation.js +186 -0
- package/src/gep/narrativeMemory.js +108 -0
- package/src/gep/paths.js +113 -0
- package/src/gep/personality.js +355 -0
- package/src/gep/prompt.js +566 -0
- package/src/gep/questionGenerator.js +212 -0
- package/src/gep/reflection.js +127 -0
- package/src/gep/sanitize.js +67 -0
- package/src/gep/selector.js +250 -0
- package/src/gep/signals.js +417 -0
- package/src/gep/skillDistiller.js +499 -0
- package/src/gep/solidify.js +1681 -0
- package/src/gep/strategy.js +126 -0
- package/src/gep/taskReceiver.js +528 -0
- package/src/gep/validationReport.js +55 -0
- package/src/ops/cleanup.js +80 -0
- package/src/ops/commentary.js +60 -0
- package/src/ops/health_check.js +106 -0
- package/src/ops/index.js +11 -0
- package/src/ops/innovation.js +67 -0
- package/src/ops/lifecycle.js +168 -0
- package/src/ops/self_repair.js +72 -0
- package/src/ops/skills_monitor.js +143 -0
- package/src/ops/trigger.js +33 -0
package/src/evolve.js
ADDED
|
@@ -0,0 +1,1704 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const { execSync } = require('child_process');
|
|
5
|
+
const { getRepoRoot, getMemoryDir, getSessionScope } = require('./gep/paths');
|
|
6
|
+
const { extractSignals } = require('./gep/signals');
|
|
7
|
+
const {
|
|
8
|
+
loadGenes,
|
|
9
|
+
loadCapsules,
|
|
10
|
+
readAllEvents,
|
|
11
|
+
getLastEventId,
|
|
12
|
+
appendCandidateJsonl,
|
|
13
|
+
readRecentCandidates,
|
|
14
|
+
readRecentExternalCandidates,
|
|
15
|
+
readRecentFailedCapsules,
|
|
16
|
+
ensureAssetFiles,
|
|
17
|
+
} = require('./gep/assetStore');
|
|
18
|
+
const { selectGeneAndCapsule, matchPatternToSignals } = require('./gep/selector');
|
|
19
|
+
const { buildGepPrompt, buildReusePrompt, buildHubMatchedBlock } = require('./gep/prompt');
|
|
20
|
+
const { hubSearch } = require('./gep/hubSearch');
|
|
21
|
+
const { logAssetCall } = require('./gep/assetCallLog');
|
|
22
|
+
const { extractCapabilityCandidates, renderCandidatesPreview } = require('./gep/candidates');
|
|
23
|
+
const memoryAdapter = require('./gep/memoryGraphAdapter');
|
|
24
|
+
const {
|
|
25
|
+
getAdvice: getMemoryAdvice,
|
|
26
|
+
recordSignalSnapshot,
|
|
27
|
+
recordHypothesis,
|
|
28
|
+
recordAttempt,
|
|
29
|
+
recordOutcome: recordOutcomeFromState,
|
|
30
|
+
memoryGraphPath,
|
|
31
|
+
} = memoryAdapter;
|
|
32
|
+
const { readStateForSolidify, writeStateForSolidify } = require('./gep/solidify');
|
|
33
|
+
const { fetchTasks, selectBestTask, claimTask, taskToSignals, claimWorkerTask, estimateCommitmentDeadline } = require('./gep/taskReceiver');
|
|
34
|
+
const { generateQuestions } = require('./gep/questionGenerator');
|
|
35
|
+
const { buildMutation, isHighRiskMutationAllowed } = require('./gep/mutation');
|
|
36
|
+
const { selectPersonalityForRun } = require('./gep/personality');
|
|
37
|
+
const { clip, writePromptArtifact, renderSessionsSpawnCall } = require('./gep/bridge');
|
|
38
|
+
const { getEvolutionDir } = require('./gep/paths');
|
|
39
|
+
const { shouldReflect, buildReflectionContext, recordReflection } = require('./gep/reflection');
|
|
40
|
+
const { loadNarrativeSummary } = require('./gep/narrativeMemory');
|
|
41
|
+
const { maybeReportIssue } = require('./gep/issueReporter');
|
|
42
|
+
|
|
43
|
+
const REPO_ROOT = getRepoRoot();
|
|
44
|
+
|
|
45
|
+
// Load environment variables from repo root
|
|
46
|
+
try {
|
|
47
|
+
require('dotenv').config({ path: path.join(REPO_ROOT, '.env'), quiet: true });
|
|
48
|
+
} catch (e) {
|
|
49
|
+
// dotenv might not be installed or .env missing, proceed gracefully
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Configuration from CLI flags or Env
|
|
53
|
+
const ARGS = process.argv.slice(2);
|
|
54
|
+
const IS_REVIEW_MODE = ARGS.includes('--review');
|
|
55
|
+
const IS_DRY_RUN = ARGS.includes('--dry-run');
|
|
56
|
+
const IS_RANDOM_DRIFT = ARGS.includes('--drift') || String(process.env.RANDOM_DRIFT || '').toLowerCase() === 'true';
|
|
57
|
+
|
|
58
|
+
// Default Configuration
|
|
59
|
+
const MEMORY_DIR = getMemoryDir();
|
|
60
|
+
const AGENT_NAME = process.env.AGENT_NAME || 'main';
|
|
61
|
+
const AGENT_SESSIONS_DIR = path.join(os.homedir(), `.openclaw/agents/${AGENT_NAME}/sessions`);
|
|
62
|
+
const TODAY_LOG = path.join(MEMORY_DIR, new Date().toISOString().split('T')[0] + '.md');
|
|
63
|
+
|
|
64
|
+
// Ensure memory directory exists so state/cache writes work.
|
|
65
|
+
try {
|
|
66
|
+
if (!fs.existsSync(MEMORY_DIR)) fs.mkdirSync(MEMORY_DIR, { recursive: true });
|
|
67
|
+
} catch (e) {}
|
|
68
|
+
|
|
69
|
+
function formatSessionLog(jsonlContent) {
|
|
70
|
+
const result = [];
|
|
71
|
+
const lines = jsonlContent.split('\n');
|
|
72
|
+
let lastLine = '';
|
|
73
|
+
let repeatCount = 0;
|
|
74
|
+
|
|
75
|
+
const flushRepeats = () => {
|
|
76
|
+
if (repeatCount > 0) {
|
|
77
|
+
result.push(` ... [Repeated ${repeatCount} times] ...`);
|
|
78
|
+
repeatCount = 0;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
for (const line of lines) {
|
|
83
|
+
if (!line.trim()) continue;
|
|
84
|
+
try {
|
|
85
|
+
const data = JSON.parse(line);
|
|
86
|
+
let entry = '';
|
|
87
|
+
|
|
88
|
+
if (data.type === 'message' && data.message) {
|
|
89
|
+
const role = (data.message.role || 'unknown').toUpperCase();
|
|
90
|
+
let content = '';
|
|
91
|
+
if (Array.isArray(data.message.content)) {
|
|
92
|
+
content = data.message.content
|
|
93
|
+
.map(c => {
|
|
94
|
+
if (c.type === 'text') return c.text;
|
|
95
|
+
if (c.type === 'toolCall') return `[TOOL: ${c.name}]`;
|
|
96
|
+
return '';
|
|
97
|
+
})
|
|
98
|
+
.join(' ');
|
|
99
|
+
} else if (typeof data.message.content === 'string') {
|
|
100
|
+
content = data.message.content;
|
|
101
|
+
} else {
|
|
102
|
+
content = JSON.stringify(data.message.content);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Capture LLM errors from errorMessage field (e.g. "Unsupported MIME type: image/gif")
|
|
106
|
+
if (data.message.errorMessage) {
|
|
107
|
+
const errMsg = typeof data.message.errorMessage === 'string'
|
|
108
|
+
? data.message.errorMessage
|
|
109
|
+
: JSON.stringify(data.message.errorMessage);
|
|
110
|
+
content = `[LLM ERROR] ${errMsg.replace(/\n+/g, ' ').slice(0, 300)}`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Filter: Skip Heartbeats to save noise
|
|
114
|
+
if (content.trim() === 'HEARTBEAT_OK') continue;
|
|
115
|
+
if (content.includes('NO_REPLY') && !data.message.errorMessage) continue;
|
|
116
|
+
|
|
117
|
+
// Clean up newlines for compact reading
|
|
118
|
+
content = content.replace(/\n+/g, ' ').slice(0, 300);
|
|
119
|
+
entry = `**${role}**: ${content}`;
|
|
120
|
+
} else if (data.type === 'tool_result' || (data.message && data.message.role === 'toolResult')) {
|
|
121
|
+
// Filter: Skip generic success results or short uninformative ones
|
|
122
|
+
// Only show error or significant output
|
|
123
|
+
let resContent = '';
|
|
124
|
+
|
|
125
|
+
// Robust extraction: Handle structured tool results (e.g. sessions_spawn) that lack 'output'
|
|
126
|
+
if (data.tool_result) {
|
|
127
|
+
if (data.tool_result.output) {
|
|
128
|
+
resContent = data.tool_result.output;
|
|
129
|
+
} else {
|
|
130
|
+
resContent = JSON.stringify(data.tool_result);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (data.content) resContent = typeof data.content === 'string' ? data.content : JSON.stringify(data.content);
|
|
135
|
+
|
|
136
|
+
if (resContent.length < 50 && (resContent.includes('success') || resContent.includes('done'))) continue;
|
|
137
|
+
if (resContent.trim() === '' || resContent === '{}') continue;
|
|
138
|
+
|
|
139
|
+
// Improvement: Show snippet of result (especially errors) instead of hiding it
|
|
140
|
+
const preview = resContent.replace(/\n+/g, ' ').slice(0, 200);
|
|
141
|
+
entry = `[TOOL RESULT] ${preview}${resContent.length > 200 ? '...' : ''}`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (entry) {
|
|
145
|
+
if (entry === lastLine) {
|
|
146
|
+
repeatCount++;
|
|
147
|
+
} else {
|
|
148
|
+
flushRepeats();
|
|
149
|
+
result.push(entry);
|
|
150
|
+
lastLine = entry;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
} catch (e) {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
flushRepeats();
|
|
158
|
+
return result.join('\n');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function readRealSessionLog() {
|
|
162
|
+
try {
|
|
163
|
+
if (!fs.existsSync(AGENT_SESSIONS_DIR)) return '[NO SESSION LOGS FOUND]';
|
|
164
|
+
|
|
165
|
+
const now = Date.now();
|
|
166
|
+
const ACTIVE_WINDOW_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
167
|
+
const TARGET_BYTES = 120000;
|
|
168
|
+
const PER_SESSION_BYTES = 20000; // Read tail of each active session
|
|
169
|
+
|
|
170
|
+
// Session scope isolation: when EVOLVER_SESSION_SCOPE is set,
|
|
171
|
+
// only read sessions whose filenames contain the scope identifier.
|
|
172
|
+
// This prevents cross-channel/cross-project memory contamination.
|
|
173
|
+
const sessionScope = getSessionScope();
|
|
174
|
+
|
|
175
|
+
// Find ALL active sessions (modified in last 24h), sorted newest first
|
|
176
|
+
let files = fs
|
|
177
|
+
.readdirSync(AGENT_SESSIONS_DIR)
|
|
178
|
+
.filter(f => f.endsWith('.jsonl') && !f.includes('.lock'))
|
|
179
|
+
.map(f => {
|
|
180
|
+
try {
|
|
181
|
+
const st = fs.statSync(path.join(AGENT_SESSIONS_DIR, f));
|
|
182
|
+
return { name: f, time: st.mtime.getTime(), size: st.size };
|
|
183
|
+
} catch (e) {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
})
|
|
187
|
+
.filter(f => f && (now - f.time) < ACTIVE_WINDOW_MS)
|
|
188
|
+
.sort((a, b) => b.time - a.time); // Newest first
|
|
189
|
+
|
|
190
|
+
if (files.length === 0) return '[NO JSONL FILES]';
|
|
191
|
+
|
|
192
|
+
// Skip evolver's own sessions to avoid self-reference loops
|
|
193
|
+
let nonEvolverFiles = files.filter(f => !f.name.startsWith('evolver_hand_'));
|
|
194
|
+
|
|
195
|
+
// Session scope filter: when scope is active, only include sessions
|
|
196
|
+
// whose filename contains the scope string (e.g., channel_123456.jsonl).
|
|
197
|
+
// If no sessions match the scope, fall back to all non-evolver sessions
|
|
198
|
+
// (graceful degradation -- better to evolve with global context than not at all).
|
|
199
|
+
if (sessionScope && nonEvolverFiles.length > 0) {
|
|
200
|
+
const scopeLower = sessionScope.toLowerCase();
|
|
201
|
+
const scopedFiles = nonEvolverFiles.filter(f => f.name.toLowerCase().includes(scopeLower));
|
|
202
|
+
if (scopedFiles.length > 0) {
|
|
203
|
+
nonEvolverFiles = scopedFiles;
|
|
204
|
+
console.log(`[SessionScope] Filtered to ${scopedFiles.length} session(s) matching scope "${sessionScope}".`);
|
|
205
|
+
} else {
|
|
206
|
+
console.log(`[SessionScope] No sessions match scope "${sessionScope}". Using all ${nonEvolverFiles.length} session(s) (fallback).`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const activeFiles = nonEvolverFiles.length > 0 ? nonEvolverFiles : files.slice(0, 1);
|
|
211
|
+
|
|
212
|
+
// Read from multiple active sessions (up to 6) to get a full picture
|
|
213
|
+
const maxSessions = Math.min(activeFiles.length, 6);
|
|
214
|
+
const sections = [];
|
|
215
|
+
let totalBytes = 0;
|
|
216
|
+
|
|
217
|
+
for (let i = 0; i < maxSessions && totalBytes < TARGET_BYTES; i++) {
|
|
218
|
+
const f = activeFiles[i];
|
|
219
|
+
const bytesLeft = TARGET_BYTES - totalBytes;
|
|
220
|
+
const readSize = Math.min(PER_SESSION_BYTES, bytesLeft);
|
|
221
|
+
const raw = readRecentLog(path.join(AGENT_SESSIONS_DIR, f.name), readSize);
|
|
222
|
+
const formatted = formatSessionLog(raw);
|
|
223
|
+
if (formatted.trim()) {
|
|
224
|
+
sections.push(`--- SESSION (${f.name}) ---\n${formatted}`);
|
|
225
|
+
totalBytes += formatted.length;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
let content = sections.join('\n\n');
|
|
230
|
+
|
|
231
|
+
return content;
|
|
232
|
+
} catch (e) {
|
|
233
|
+
return `[ERROR READING SESSION LOGS: ${e.message}]`;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function readRecentLog(filePath, size = 10000) {
|
|
238
|
+
try {
|
|
239
|
+
if (!fs.existsSync(filePath)) return `[MISSING] ${filePath}`;
|
|
240
|
+
const stats = fs.statSync(filePath);
|
|
241
|
+
const start = Math.max(0, stats.size - size);
|
|
242
|
+
const buffer = Buffer.alloc(stats.size - start);
|
|
243
|
+
const fd = fs.openSync(filePath, 'r');
|
|
244
|
+
fs.readSync(fd, buffer, 0, buffer.length, start);
|
|
245
|
+
fs.closeSync(fd);
|
|
246
|
+
return buffer.toString('utf8');
|
|
247
|
+
} catch (e) {
|
|
248
|
+
return `[ERROR READING ${filePath}: ${e.message}]`;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function checkSystemHealth() {
|
|
253
|
+
const report = [];
|
|
254
|
+
try {
|
|
255
|
+
// Uptime & Node Version
|
|
256
|
+
const uptime = (os.uptime() / 3600).toFixed(1);
|
|
257
|
+
report.push(`Uptime: ${uptime}h`);
|
|
258
|
+
report.push(`Node: ${process.version}`);
|
|
259
|
+
|
|
260
|
+
// Memory Usage (RSS)
|
|
261
|
+
const mem = process.memoryUsage();
|
|
262
|
+
const rssMb = (mem.rss / 1024 / 1024).toFixed(1);
|
|
263
|
+
report.push(`Agent RSS: ${rssMb}MB`);
|
|
264
|
+
|
|
265
|
+
// Optimization: Use native Node.js fs.statfsSync instead of spawning 'df'
|
|
266
|
+
if (fs.statfsSync) {
|
|
267
|
+
const stats = fs.statfsSync('/');
|
|
268
|
+
const total = stats.blocks * stats.bsize;
|
|
269
|
+
const free = stats.bfree * stats.bsize;
|
|
270
|
+
const used = total - free;
|
|
271
|
+
const freeGb = (free / 1024 / 1024 / 1024).toFixed(1);
|
|
272
|
+
const usedPercent = Math.round((used / total) * 100);
|
|
273
|
+
report.push(`Disk: ${usedPercent}% (${freeGb}G free)`);
|
|
274
|
+
}
|
|
275
|
+
} catch (e) {}
|
|
276
|
+
|
|
277
|
+
try {
|
|
278
|
+
if (process.platform === 'win32') {
|
|
279
|
+
const wmic = execSync('tasklist /FI "IMAGENAME eq node.exe" /NH', {
|
|
280
|
+
encoding: 'utf8',
|
|
281
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
282
|
+
timeout: 3000,
|
|
283
|
+
windowsHide: true,
|
|
284
|
+
});
|
|
285
|
+
const count = wmic.split('\n').filter(l => l.trim() && !l.includes('INFO:')).length;
|
|
286
|
+
report.push(`Node Processes: ${count}`);
|
|
287
|
+
} else {
|
|
288
|
+
try {
|
|
289
|
+
const pgrep = execSync('pgrep -c node', {
|
|
290
|
+
encoding: 'utf8',
|
|
291
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
292
|
+
timeout: 2000,
|
|
293
|
+
});
|
|
294
|
+
report.push(`Node Processes: ${pgrep.trim()}`);
|
|
295
|
+
} catch (e) {
|
|
296
|
+
const ps = execSync('ps aux | grep node | grep -v grep | wc -l', {
|
|
297
|
+
encoding: 'utf8',
|
|
298
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
299
|
+
timeout: 2000,
|
|
300
|
+
});
|
|
301
|
+
report.push(`Node Processes: ${ps.trim()}`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
} catch (e) {}
|
|
305
|
+
|
|
306
|
+
// Integration Health Checks (Env Vars)
|
|
307
|
+
try {
|
|
308
|
+
const issues = [];
|
|
309
|
+
|
|
310
|
+
// Generic Integration Status Check (Decoupled)
|
|
311
|
+
if (process.env.INTEGRATION_STATUS_CMD) {
|
|
312
|
+
try {
|
|
313
|
+
const status = execSync(process.env.INTEGRATION_STATUS_CMD, {
|
|
314
|
+
encoding: 'utf8',
|
|
315
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
316
|
+
timeout: 2000,
|
|
317
|
+
windowsHide: true,
|
|
318
|
+
});
|
|
319
|
+
if (status.trim()) issues.push(status.trim());
|
|
320
|
+
} catch (e) {}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (issues.length > 0) {
|
|
324
|
+
report.push(`Integrations: ${issues.join(', ')}`);
|
|
325
|
+
} else {
|
|
326
|
+
report.push('Integrations: Nominal');
|
|
327
|
+
}
|
|
328
|
+
} catch (e) {}
|
|
329
|
+
|
|
330
|
+
return report.length ? report.join(' | ') : 'Health Check Unavailable';
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function getMutationDirective(logContent) {
|
|
334
|
+
// Signal hints derived from recent logs.
|
|
335
|
+
const errorMatches = logContent.match(/\[ERROR|Error:|Exception:|FAIL|Failed|"isError":true/gi) || [];
|
|
336
|
+
const errorCount = errorMatches.length;
|
|
337
|
+
const isUnstable = errorCount > 2;
|
|
338
|
+
const recommendedIntent = isUnstable ? 'repair' : 'optimize';
|
|
339
|
+
|
|
340
|
+
return `
|
|
341
|
+
[Signal Hints]
|
|
342
|
+
- recent_error_count: ${errorCount}
|
|
343
|
+
- stability: ${isUnstable ? 'unstable' : 'stable'}
|
|
344
|
+
- recommended_intent: ${recommendedIntent}
|
|
345
|
+
`;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const STATE_FILE = path.join(getEvolutionDir(), 'evolution_state.json');
|
|
349
|
+
const DORMANT_HYPOTHESIS_FILE = path.join(getEvolutionDir(), 'dormant_hypothesis.json');
|
|
350
|
+
var DORMANT_TTL_MS = 3600 * 1000;
|
|
351
|
+
|
|
352
|
+
function writeDormantHypothesis(data) {
|
|
353
|
+
try {
|
|
354
|
+
var dir = getEvolutionDir();
|
|
355
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
356
|
+
var obj = Object.assign({}, data, { created_at: new Date().toISOString(), ttl_ms: DORMANT_TTL_MS });
|
|
357
|
+
var tmp = DORMANT_HYPOTHESIS_FILE + '.tmp';
|
|
358
|
+
fs.writeFileSync(tmp, JSON.stringify(obj, null, 2) + '\n', 'utf8');
|
|
359
|
+
fs.renameSync(tmp, DORMANT_HYPOTHESIS_FILE);
|
|
360
|
+
console.log('[DormantHypothesis] Saved partial state before backoff: ' + (data.backoff_reason || 'unknown'));
|
|
361
|
+
} catch (e) {
|
|
362
|
+
console.log('[DormantHypothesis] Write failed (non-fatal): ' + (e && e.message ? e.message : e));
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function readDormantHypothesis() {
|
|
367
|
+
try {
|
|
368
|
+
if (!fs.existsSync(DORMANT_HYPOTHESIS_FILE)) return null;
|
|
369
|
+
var raw = fs.readFileSync(DORMANT_HYPOTHESIS_FILE, 'utf8');
|
|
370
|
+
if (!raw.trim()) return null;
|
|
371
|
+
var obj = JSON.parse(raw);
|
|
372
|
+
var createdAt = obj.created_at ? new Date(obj.created_at).getTime() : 0;
|
|
373
|
+
var ttl = Number.isFinite(Number(obj.ttl_ms)) ? Number(obj.ttl_ms) : DORMANT_TTL_MS;
|
|
374
|
+
if (Date.now() - createdAt > ttl) {
|
|
375
|
+
clearDormantHypothesis();
|
|
376
|
+
console.log('[DormantHypothesis] Expired (age: ' + Math.round((Date.now() - createdAt) / 1000) + 's). Discarded.');
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
return obj;
|
|
380
|
+
} catch (e) {
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function clearDormantHypothesis() {
|
|
386
|
+
try {
|
|
387
|
+
if (fs.existsSync(DORMANT_HYPOTHESIS_FILE)) fs.unlinkSync(DORMANT_HYPOTHESIS_FILE);
|
|
388
|
+
} catch (e) {}
|
|
389
|
+
}
|
|
390
|
+
// Read MEMORY.md and USER.md from the WORKSPACE root (not the evolver plugin dir).
|
|
391
|
+
// This avoids symlink breakage if the target file is temporarily deleted.
|
|
392
|
+
const WORKSPACE_ROOT = process.env.OPENCLAW_WORKSPACE || path.resolve(REPO_ROOT, '../..');
|
|
393
|
+
const ROOT_MEMORY = path.join(WORKSPACE_ROOT, 'MEMORY.md');
|
|
394
|
+
const DIR_MEMORY = path.join(MEMORY_DIR, 'MEMORY.md');
|
|
395
|
+
const MEMORY_FILE = fs.existsSync(ROOT_MEMORY) ? ROOT_MEMORY : (fs.existsSync(DIR_MEMORY) ? DIR_MEMORY : ROOT_MEMORY);
|
|
396
|
+
const USER_FILE = path.join(WORKSPACE_ROOT, 'USER.md');
|
|
397
|
+
|
|
398
|
+
function readMemorySnippet() {
|
|
399
|
+
try {
|
|
400
|
+
// Session scope isolation: when a scope is active, prefer scoped MEMORY.md
|
|
401
|
+
// at memory/scopes/<scope>/MEMORY.md. Falls back to global MEMORY.md if
|
|
402
|
+
// scoped file doesn't exist (common: scoped MEMORY.md created on first evolution).
|
|
403
|
+
const scope = getSessionScope();
|
|
404
|
+
let memFile = MEMORY_FILE;
|
|
405
|
+
if (scope) {
|
|
406
|
+
const scopedMemory = path.join(MEMORY_DIR, 'scopes', scope, 'MEMORY.md');
|
|
407
|
+
if (fs.existsSync(scopedMemory)) {
|
|
408
|
+
memFile = scopedMemory;
|
|
409
|
+
console.log(`[SessionScope] Reading scoped MEMORY.md for "${scope}".`);
|
|
410
|
+
} else {
|
|
411
|
+
// First run with scope: global MEMORY.md will be used, but note it.
|
|
412
|
+
console.log(`[SessionScope] No scoped MEMORY.md for "${scope}". Using global MEMORY.md.`);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
if (!fs.existsSync(memFile)) return '[MEMORY.md MISSING]';
|
|
416
|
+
const content = fs.readFileSync(memFile, 'utf8');
|
|
417
|
+
// Optimization: Increased limit from 2000 to 50000 for modern context windows
|
|
418
|
+
return content.length > 50000
|
|
419
|
+
? content.slice(0, 50000) + `\n... [TRUNCATED: ${content.length - 50000} chars remaining]`
|
|
420
|
+
: content;
|
|
421
|
+
} catch (e) {
|
|
422
|
+
return '[ERROR READING MEMORY.md]';
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function readUserSnippet() {
|
|
427
|
+
try {
|
|
428
|
+
if (!fs.existsSync(USER_FILE)) return '[USER.md MISSING]';
|
|
429
|
+
return fs.readFileSync(USER_FILE, 'utf8');
|
|
430
|
+
} catch (e) {
|
|
431
|
+
return '[ERROR READING USER.md]';
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function getNextCycleId() {
|
|
436
|
+
let state = { cycleCount: 0, lastRun: 0 };
|
|
437
|
+
try {
|
|
438
|
+
if (fs.existsSync(STATE_FILE)) {
|
|
439
|
+
state = JSON.parse(fs.readFileSync(STATE_FILE, 'utf8'));
|
|
440
|
+
}
|
|
441
|
+
} catch (e) {}
|
|
442
|
+
|
|
443
|
+
state.cycleCount = (state.cycleCount || 0) + 1;
|
|
444
|
+
state.lastRun = Date.now();
|
|
445
|
+
|
|
446
|
+
try {
|
|
447
|
+
fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
|
|
448
|
+
} catch (e) {}
|
|
449
|
+
|
|
450
|
+
return String(state.cycleCount).padStart(4, '0');
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function performMaintenance() {
|
|
454
|
+
// Auto-update check (rate-limited, non-fatal).
|
|
455
|
+
checkAndAutoUpdate();
|
|
456
|
+
|
|
457
|
+
try {
|
|
458
|
+
if (!fs.existsSync(AGENT_SESSIONS_DIR)) return;
|
|
459
|
+
|
|
460
|
+
const files = fs.readdirSync(AGENT_SESSIONS_DIR).filter(f => f.endsWith('.jsonl'));
|
|
461
|
+
|
|
462
|
+
// Clean up evolver's own hand sessions immediately.
|
|
463
|
+
// These are single-use executor sessions that must not accumulate,
|
|
464
|
+
// otherwise they pollute the agent's context and starve user conversations.
|
|
465
|
+
const evolverFiles = files.filter(f => f.startsWith('evolver_hand_'));
|
|
466
|
+
for (const f of evolverFiles) {
|
|
467
|
+
try {
|
|
468
|
+
fs.unlinkSync(path.join(AGENT_SESSIONS_DIR, f));
|
|
469
|
+
} catch (_) {}
|
|
470
|
+
}
|
|
471
|
+
if (evolverFiles.length > 0) {
|
|
472
|
+
console.log(`[Maintenance] Cleaned ${evolverFiles.length} evolver hand session(s).`);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Archive old non-evolver sessions when count exceeds threshold.
|
|
476
|
+
const remaining = files.length - evolverFiles.length;
|
|
477
|
+
if (remaining < 100) return;
|
|
478
|
+
|
|
479
|
+
console.log(`[Maintenance] Found ${remaining} session logs. Archiving old ones...`);
|
|
480
|
+
|
|
481
|
+
const ARCHIVE_DIR = path.join(AGENT_SESSIONS_DIR, 'archive');
|
|
482
|
+
if (!fs.existsSync(ARCHIVE_DIR)) fs.mkdirSync(ARCHIVE_DIR, { recursive: true });
|
|
483
|
+
|
|
484
|
+
const fileStats = files
|
|
485
|
+
.filter(f => !f.startsWith('evolver_hand_'))
|
|
486
|
+
.map(f => {
|
|
487
|
+
try {
|
|
488
|
+
return { name: f, time: fs.statSync(path.join(AGENT_SESSIONS_DIR, f)).mtime.getTime() };
|
|
489
|
+
} catch (e) {
|
|
490
|
+
return null;
|
|
491
|
+
}
|
|
492
|
+
})
|
|
493
|
+
.filter(Boolean)
|
|
494
|
+
.sort((a, b) => a.time - b.time);
|
|
495
|
+
|
|
496
|
+
const toArchive = fileStats.slice(0, fileStats.length - 50);
|
|
497
|
+
|
|
498
|
+
for (const file of toArchive) {
|
|
499
|
+
const oldPath = path.join(AGENT_SESSIONS_DIR, file.name);
|
|
500
|
+
const newPath = path.join(ARCHIVE_DIR, file.name);
|
|
501
|
+
fs.renameSync(oldPath, newPath);
|
|
502
|
+
}
|
|
503
|
+
if (toArchive.length > 0) {
|
|
504
|
+
console.log(`[Maintenance] Archived ${toArchive.length} logs to ${ARCHIVE_DIR}`);
|
|
505
|
+
}
|
|
506
|
+
} catch (e) {
|
|
507
|
+
console.error(`[Maintenance] Error: ${e.message}`);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// --- Auto-update: check for newer versions of evolver and wrapper on ClawHub ---
|
|
512
|
+
function checkAndAutoUpdate() {
|
|
513
|
+
try {
|
|
514
|
+
// Read config: default autoUpdate = true
|
|
515
|
+
const configPath = path.join(os.homedir(), '.openclaw', 'openclaw.json');
|
|
516
|
+
let autoUpdate = true;
|
|
517
|
+
let intervalHours = 6;
|
|
518
|
+
try {
|
|
519
|
+
if (fs.existsSync(configPath)) {
|
|
520
|
+
const cfg = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
521
|
+
if (cfg.evolver && cfg.evolver.autoUpdate === false) autoUpdate = false;
|
|
522
|
+
if (cfg.evolver && Number.isFinite(Number(cfg.evolver.autoUpdateIntervalHours))) {
|
|
523
|
+
intervalHours = Number(cfg.evolver.autoUpdateIntervalHours);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
} catch (_) {}
|
|
527
|
+
|
|
528
|
+
if (!autoUpdate) return;
|
|
529
|
+
|
|
530
|
+
// Rate limit: only check once per interval
|
|
531
|
+
const stateFile = path.join(MEMORY_DIR, 'evolver_update_check.json');
|
|
532
|
+
const now = Date.now();
|
|
533
|
+
const intervalMs = intervalHours * 60 * 60 * 1000;
|
|
534
|
+
try {
|
|
535
|
+
if (fs.existsSync(stateFile)) {
|
|
536
|
+
const state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
|
|
537
|
+
if (state.lastCheckedAt && (now - new Date(state.lastCheckedAt).getTime()) < intervalMs) {
|
|
538
|
+
return; // Too soon, skip
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
} catch (_) {}
|
|
542
|
+
|
|
543
|
+
let clawhubBin = null;
|
|
544
|
+
const whichCmd = process.platform === 'win32' ? 'where clawhub' : 'which clawhub';
|
|
545
|
+
const candidates = ['clawhub', path.join(os.homedir(), '.npm-global/bin/clawhub'), '/usr/local/bin/clawhub'];
|
|
546
|
+
for (const c of candidates) {
|
|
547
|
+
try {
|
|
548
|
+
if (c === 'clawhub') {
|
|
549
|
+
execSync(whichCmd, { stdio: 'ignore', timeout: 3000, windowsHide: true });
|
|
550
|
+
clawhubBin = 'clawhub';
|
|
551
|
+
break;
|
|
552
|
+
}
|
|
553
|
+
if (fs.existsSync(c)) { clawhubBin = c; break; }
|
|
554
|
+
} catch (_) {}
|
|
555
|
+
}
|
|
556
|
+
if (!clawhubBin) return; // No clawhub CLI available
|
|
557
|
+
|
|
558
|
+
// Update evolver and feishu-evolver-wrapper
|
|
559
|
+
const slugs = ['evolver', 'feishu-evolver-wrapper'];
|
|
560
|
+
let updated = false;
|
|
561
|
+
for (const slug of slugs) {
|
|
562
|
+
try {
|
|
563
|
+
const out = execSync(`${clawhubBin} update ${slug} --force`, {
|
|
564
|
+
encoding: 'utf8',
|
|
565
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
566
|
+
timeout: 30000,
|
|
567
|
+
cwd: path.resolve(REPO_ROOT, '..'),
|
|
568
|
+
windowsHide: true,
|
|
569
|
+
});
|
|
570
|
+
if (out && !out.includes('already up to date') && !out.includes('not installed')) {
|
|
571
|
+
console.log(`[AutoUpdate] ${slug}: ${out.trim().split('\n').pop()}`);
|
|
572
|
+
updated = true;
|
|
573
|
+
}
|
|
574
|
+
} catch (e) {
|
|
575
|
+
// Non-fatal: update failure should never block evolution
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Write state
|
|
580
|
+
try {
|
|
581
|
+
const stateData = {
|
|
582
|
+
lastCheckedAt: new Date(now).toISOString(),
|
|
583
|
+
updated,
|
|
584
|
+
};
|
|
585
|
+
fs.writeFileSync(stateFile, JSON.stringify(stateData, null, 2) + '\n');
|
|
586
|
+
} catch (_) {}
|
|
587
|
+
|
|
588
|
+
if (updated) {
|
|
589
|
+
console.log('[AutoUpdate] Skills updated. Changes will take effect on next wrapper restart.');
|
|
590
|
+
}
|
|
591
|
+
} catch (e) {
|
|
592
|
+
// Entire auto-update is non-fatal
|
|
593
|
+
console.log(`[AutoUpdate] Check failed (non-fatal): ${e.message}`);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function sleepMs(ms) {
|
|
598
|
+
const t = Number(ms);
|
|
599
|
+
const n = Number.isFinite(t) ? Math.max(0, t) : 0;
|
|
600
|
+
return new Promise(resolve => setTimeout(resolve, n));
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// Check system load average via os.loadavg().
|
|
604
|
+
// Returns { load1m, load5m, load15m }. Used for load-aware throttling.
|
|
605
|
+
function getSystemLoad() {
|
|
606
|
+
try {
|
|
607
|
+
const loadavg = os.loadavg();
|
|
608
|
+
return { load1m: loadavg[0], load5m: loadavg[1], load15m: loadavg[2] };
|
|
609
|
+
} catch (e) {
|
|
610
|
+
return { load1m: 0, load5m: 0, load15m: 0 };
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// Calculate intelligent default load threshold based on CPU cores
|
|
615
|
+
// Rule of thumb:
|
|
616
|
+
// - Single-core: 0.8-1.0 (use 0.9)
|
|
617
|
+
// - Multi-core: cores x 0.8-1.0 (use 0.9)
|
|
618
|
+
// - Production: reserve 20% headroom for burst traffic
|
|
619
|
+
function getDefaultLoadMax() {
|
|
620
|
+
const cpuCount = os.cpus().length;
|
|
621
|
+
if (cpuCount === 1) {
|
|
622
|
+
return 0.9;
|
|
623
|
+
} else {
|
|
624
|
+
return cpuCount * 0.9;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// Check how many agent sessions are actively being processed (modified in the last N minutes).
|
|
629
|
+
// If the agent is busy with user conversations, evolver should back off.
|
|
630
|
+
function getRecentActiveSessionCount(windowMs) {
|
|
631
|
+
try {
|
|
632
|
+
if (!fs.existsSync(AGENT_SESSIONS_DIR)) return 0;
|
|
633
|
+
const now = Date.now();
|
|
634
|
+
const w = Number.isFinite(windowMs) ? windowMs : 10 * 60 * 1000;
|
|
635
|
+
return fs.readdirSync(AGENT_SESSIONS_DIR)
|
|
636
|
+
.filter(f => f.endsWith('.jsonl') && !f.includes('.lock') && !f.startsWith('evolver_hand_'))
|
|
637
|
+
.filter(f => {
|
|
638
|
+
try { return (now - fs.statSync(path.join(AGENT_SESSIONS_DIR, f)).mtimeMs) < w; } catch (_) { return false; }
|
|
639
|
+
}).length;
|
|
640
|
+
} catch (_) { return 0; }
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
async function run() {
|
|
644
|
+
const bridgeEnabled = String(process.env.EVOLVE_BRIDGE || '').toLowerCase() !== 'false';
|
|
645
|
+
const loopMode = ARGS.includes('--loop') || ARGS.includes('--mad-dog') || String(process.env.EVOLVE_LOOP || '').toLowerCase() === 'true';
|
|
646
|
+
|
|
647
|
+
// SAFEGUARD: If another evolver Hand Agent is already running, back off.
|
|
648
|
+
// Prevents race conditions when a wrapper restarts while the old Hand Agent
|
|
649
|
+
// is still executing. The Core yields instead of starting a competing cycle.
|
|
650
|
+
if (process.platform !== 'win32') {
|
|
651
|
+
try {
|
|
652
|
+
const _psRace = require('child_process').execSync(
|
|
653
|
+
'ps aux | grep "evolver_hand_" | grep "openclaw.*agent" | grep -v grep',
|
|
654
|
+
{ encoding: 'utf8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'] }
|
|
655
|
+
).trim();
|
|
656
|
+
if (_psRace && _psRace.length > 0) {
|
|
657
|
+
console.log('[Evolver] Another evolver Hand Agent is already running. Yielding this cycle.');
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
} catch (_) {
|
|
661
|
+
// grep exit 1 = no match = no conflict, safe to proceed
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// SAFEGUARD: If the agent has too many active user sessions, back off.
|
|
666
|
+
// Evolver must not starve user conversations by consuming model concurrency.
|
|
667
|
+
const QUEUE_MAX = Number.parseInt(process.env.EVOLVE_AGENT_QUEUE_MAX || '10', 10);
|
|
668
|
+
const QUEUE_BACKOFF_MS = Number.parseInt(process.env.EVOLVE_AGENT_QUEUE_BACKOFF_MS || '60000', 10);
|
|
669
|
+
const activeUserSessions = getRecentActiveSessionCount(10 * 60 * 1000);
|
|
670
|
+
if (activeUserSessions > QUEUE_MAX) {
|
|
671
|
+
console.log(`[Evolver] Agent has ${activeUserSessions} active user sessions (max ${QUEUE_MAX}). Backing off ${QUEUE_BACKOFF_MS}ms to avoid starving user conversations.`);
|
|
672
|
+
writeDormantHypothesis({
|
|
673
|
+
backoff_reason: 'active_sessions_exceeded',
|
|
674
|
+
active_sessions: activeUserSessions,
|
|
675
|
+
queue_max: QUEUE_MAX,
|
|
676
|
+
});
|
|
677
|
+
await sleepMs(QUEUE_BACKOFF_MS);
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// SAFEGUARD: System load awareness.
|
|
682
|
+
// When system load is too high (e.g. too many concurrent processes, heavy I/O),
|
|
683
|
+
// back off to prevent the evolver from contributing to load spikes.
|
|
684
|
+
// Echo-MingXuan's Cycle #55 saw load spike from 0.02-0.50 to 1.30 before crash.
|
|
685
|
+
const LOAD_MAX = parseFloat(process.env.EVOLVE_LOAD_MAX || String(getDefaultLoadMax()));
|
|
686
|
+
const sysLoad = getSystemLoad();
|
|
687
|
+
if (sysLoad.load1m > LOAD_MAX) {
|
|
688
|
+
console.log(`[Evolver] System load ${sysLoad.load1m.toFixed(2)} exceeds max ${LOAD_MAX.toFixed(1)} (auto-calculated for ${os.cpus().length} cores). Backing off ${QUEUE_BACKOFF_MS}ms.`);
|
|
689
|
+
writeDormantHypothesis({
|
|
690
|
+
backoff_reason: 'system_load_exceeded',
|
|
691
|
+
system_load: { load1m: sysLoad.load1m, load5m: sysLoad.load5m, load15m: sysLoad.load15m },
|
|
692
|
+
load_max: LOAD_MAX,
|
|
693
|
+
cpu_cores: os.cpus().length,
|
|
694
|
+
});
|
|
695
|
+
await sleepMs(QUEUE_BACKOFF_MS);
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// Loop gating: do not start a new cycle until the previous one is solidified.
|
|
700
|
+
// This prevents wrappers from "fast-cycling" the Brain without waiting for the Hand to finish.
|
|
701
|
+
if (bridgeEnabled && loopMode) {
|
|
702
|
+
try {
|
|
703
|
+
const st = readStateForSolidify();
|
|
704
|
+
const lastRun = st && st.last_run ? st.last_run : null;
|
|
705
|
+
const lastSolid = st && st.last_solidify ? st.last_solidify : null;
|
|
706
|
+
if (lastRun && lastRun.run_id) {
|
|
707
|
+
const pending = !lastSolid || !lastSolid.run_id || String(lastSolid.run_id) !== String(lastRun.run_id);
|
|
708
|
+
if (pending) {
|
|
709
|
+
writeDormantHypothesis({
|
|
710
|
+
backoff_reason: 'loop_gating_pending_solidify',
|
|
711
|
+
signals: lastRun && Array.isArray(lastRun.signals) ? lastRun.signals : [],
|
|
712
|
+
selected_gene_id: lastRun && lastRun.selected_gene_id ? lastRun.selected_gene_id : null,
|
|
713
|
+
mutation: lastRun && lastRun.mutation ? lastRun.mutation : null,
|
|
714
|
+
personality_state: lastRun && lastRun.personality_state ? lastRun.personality_state : null,
|
|
715
|
+
run_id: lastRun.run_id,
|
|
716
|
+
});
|
|
717
|
+
const raw = process.env.EVOLVE_PENDING_SLEEP_MS || process.env.EVOLVE_MIN_INTERVAL || '120000';
|
|
718
|
+
const n = parseInt(String(raw), 10);
|
|
719
|
+
const waitMs = Number.isFinite(n) ? Math.max(0, n) : 120000;
|
|
720
|
+
await sleepMs(waitMs);
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
} catch (e) {
|
|
725
|
+
// If we cannot read state, proceed (fail open) to avoid deadlock.
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// Reset per-cycle env flags to prevent state leaking between cycles.
|
|
730
|
+
// In --loop mode, process.env persists across cycles. The circuit breaker
|
|
731
|
+
// below will re-set FORCE_INNOVATION if the condition still holds.
|
|
732
|
+
// CWD Recovery: If the working directory was deleted during a previous cycle
|
|
733
|
+
// (e.g., by git reset/restore or directory removal), process.cwd() throws
|
|
734
|
+
// ENOENT and ALL subsequent operations fail. Recover by chdir to REPO_ROOT.
|
|
735
|
+
try {
|
|
736
|
+
process.cwd();
|
|
737
|
+
} catch (e) {
|
|
738
|
+
if (e && e.code === 'ENOENT') {
|
|
739
|
+
console.warn('[Evolver] CWD lost (ENOENT). Recovering to REPO_ROOT: ' + REPO_ROOT);
|
|
740
|
+
try { process.chdir(REPO_ROOT); } catch (e2) {
|
|
741
|
+
console.error('[Evolver] CWD recovery failed: ' + (e2 && e2.message ? e2.message : e2));
|
|
742
|
+
throw e;
|
|
743
|
+
}
|
|
744
|
+
} else {
|
|
745
|
+
throw e;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
delete process.env.FORCE_INNOVATION;
|
|
750
|
+
|
|
751
|
+
// SAFEGUARD: Git repository check.
|
|
752
|
+
// Solidify, rollback, and blast radius all depend on git. Without a git repo
|
|
753
|
+
// these operations silently produce empty results, leading to data loss.
|
|
754
|
+
try {
|
|
755
|
+
execSync('git rev-parse --git-dir', { cwd: REPO_ROOT, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 5000 });
|
|
756
|
+
} catch (_) {
|
|
757
|
+
console.error('[Evolver] FATAL: Not a git repository (' + REPO_ROOT + ').');
|
|
758
|
+
console.error('[Evolver] Evolver requires git for rollback, blast radius calculation, and solidify.');
|
|
759
|
+
console.error('[Evolver] Run "git init && git add -A && git commit -m init" in your project root, then try again.');
|
|
760
|
+
process.exitCode = 1;
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
var dormantHypothesis = readDormantHypothesis();
|
|
765
|
+
if (dormantHypothesis) {
|
|
766
|
+
console.log('[DormantHypothesis] Recovered partial state from previous backoff: ' + (dormantHypothesis.backoff_reason || 'unknown'));
|
|
767
|
+
clearDormantHypothesis();
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
const startTime = Date.now();
|
|
771
|
+
console.log('Scanning session logs...');
|
|
772
|
+
|
|
773
|
+
// Ensure all GEP asset files exist before any operation.
|
|
774
|
+
// This prevents "No such file or directory" errors when external tools
|
|
775
|
+
// (grep, cat, etc.) reference optional append-only files like genes.jsonl.
|
|
776
|
+
try { ensureAssetFiles(); } catch (e) {
|
|
777
|
+
console.error(`[AssetInit] ensureAssetFiles failed (non-fatal): ${e.message}`);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// Maintenance: Clean up old logs to keep directory scan fast
|
|
781
|
+
if (!IS_DRY_RUN) {
|
|
782
|
+
performMaintenance();
|
|
783
|
+
} else {
|
|
784
|
+
console.log('[Maintenance] Skipped (dry-run mode).');
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// --- Repair Loop Circuit Breaker ---
|
|
788
|
+
// Detect when the evolver is stuck in a "repair -> fail -> repair" cycle.
|
|
789
|
+
// If the last N events are all failed repairs with the same gene, force
|
|
790
|
+
// innovation intent to break out of the loop instead of retrying the same fix.
|
|
791
|
+
const REPAIR_LOOP_THRESHOLD = 3;
|
|
792
|
+
try {
|
|
793
|
+
const allEvents = readAllEvents();
|
|
794
|
+
const recent = Array.isArray(allEvents) ? allEvents.slice(-REPAIR_LOOP_THRESHOLD) : [];
|
|
795
|
+
if (recent.length >= REPAIR_LOOP_THRESHOLD) {
|
|
796
|
+
const allRepairFailed = recent.every(e =>
|
|
797
|
+
e && e.intent === 'repair' &&
|
|
798
|
+
e.outcome && e.outcome.status === 'failed'
|
|
799
|
+
);
|
|
800
|
+
if (allRepairFailed) {
|
|
801
|
+
const geneIds = recent.map(e => (e.genes_used && e.genes_used[0]) || 'unknown');
|
|
802
|
+
const sameGene = geneIds.every(id => id === geneIds[0]);
|
|
803
|
+
console.warn(`[CircuitBreaker] Detected ${REPAIR_LOOP_THRESHOLD} consecutive failed repairs${sameGene ? ` (gene: ${geneIds[0]})` : ''}. Forcing innovation intent to break the loop.`);
|
|
804
|
+
// Set env flag that downstream code reads to force innovation
|
|
805
|
+
process.env.FORCE_INNOVATION = 'true';
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
} catch (e) {
|
|
809
|
+
// Non-fatal: if we can't read events, proceed normally
|
|
810
|
+
console.error(`[CircuitBreaker] Check failed (non-fatal): ${e.message}`);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
const recentMasterLog = readRealSessionLog();
|
|
814
|
+
const todayLog = readRecentLog(TODAY_LOG);
|
|
815
|
+
const memorySnippet = readMemorySnippet();
|
|
816
|
+
const userSnippet = readUserSnippet();
|
|
817
|
+
|
|
818
|
+
const cycleNum = getNextCycleId();
|
|
819
|
+
const cycleId = `Cycle #${cycleNum}`;
|
|
820
|
+
|
|
821
|
+
// 2. Detect Workspace State & Local Overrides
|
|
822
|
+
// Logic: Default to generic reporting (message)
|
|
823
|
+
let fileList = '';
|
|
824
|
+
const skillsDir = path.join(REPO_ROOT, 'skills');
|
|
825
|
+
|
|
826
|
+
// Default Reporting: Use generic `message` tool or `process.env.EVOLVE_REPORT_CMD` if set.
|
|
827
|
+
// This removes the hardcoded dependency on 'feishu-card' from the core logic.
|
|
828
|
+
let reportingDirective = `Report requirement:
|
|
829
|
+
- Use \`message\` tool.
|
|
830
|
+
- Title: Evolution ${cycleId}
|
|
831
|
+
- Status: [SUCCESS]
|
|
832
|
+
- Changes: Detail exactly what was improved.`;
|
|
833
|
+
|
|
834
|
+
// Wrapper Injection Point: The wrapper can inject a custom reporting directive via ENV.
|
|
835
|
+
if (process.env.EVOLVE_REPORT_DIRECTIVE) {
|
|
836
|
+
reportingDirective = process.env.EVOLVE_REPORT_DIRECTIVE.replace('__CYCLE_ID__', cycleId);
|
|
837
|
+
} else if (process.env.EVOLVE_REPORT_CMD) {
|
|
838
|
+
reportingDirective = `Report requirement (custom):
|
|
839
|
+
- Execute the custom report command:
|
|
840
|
+
\`\`\`
|
|
841
|
+
${process.env.EVOLVE_REPORT_CMD.replace('__CYCLE_ID__', cycleId)}
|
|
842
|
+
\`\`\`
|
|
843
|
+
- Ensure you pass the status and action details.`;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// Handle Review Mode Flag (--review)
|
|
847
|
+
if (IS_REVIEW_MODE) {
|
|
848
|
+
reportingDirective +=
|
|
849
|
+
'\n - REVIEW PAUSE: After generating the fix but BEFORE applying significant edits, ask the user for confirmation.';
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
const SKILLS_CACHE_FILE = path.join(MEMORY_DIR, 'skills_list_cache.json');
|
|
853
|
+
|
|
854
|
+
try {
|
|
855
|
+
if (fs.existsSync(skillsDir)) {
|
|
856
|
+
// Check cache validity (mtime of skills folder vs cache file)
|
|
857
|
+
let useCache = false;
|
|
858
|
+
const dirStats = fs.statSync(skillsDir);
|
|
859
|
+
if (fs.existsSync(SKILLS_CACHE_FILE)) {
|
|
860
|
+
const cacheStats = fs.statSync(SKILLS_CACHE_FILE);
|
|
861
|
+
const CACHE_TTL = 1000 * 60 * 60 * 6; // 6 Hours
|
|
862
|
+
const isFresh = Date.now() - cacheStats.mtimeMs < CACHE_TTL;
|
|
863
|
+
|
|
864
|
+
// Use cache if it's fresh AND newer than the directory (structure change)
|
|
865
|
+
if (isFresh && cacheStats.mtimeMs > dirStats.mtimeMs) {
|
|
866
|
+
try {
|
|
867
|
+
const cached = JSON.parse(fs.readFileSync(SKILLS_CACHE_FILE, 'utf8'));
|
|
868
|
+
fileList = cached.list;
|
|
869
|
+
useCache = true;
|
|
870
|
+
} catch (e) {}
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
if (!useCache) {
|
|
875
|
+
const skills = fs
|
|
876
|
+
.readdirSync(skillsDir, { withFileTypes: true })
|
|
877
|
+
.filter(dirent => dirent.isDirectory())
|
|
878
|
+
.map(dirent => {
|
|
879
|
+
const name = dirent.name;
|
|
880
|
+
let desc = 'No description';
|
|
881
|
+
try {
|
|
882
|
+
const pkg = require(path.join(skillsDir, name, 'package.json'));
|
|
883
|
+
if (pkg.description) desc = pkg.description.slice(0, 100) + (pkg.description.length > 100 ? '...' : '');
|
|
884
|
+
} catch (e) {
|
|
885
|
+
try {
|
|
886
|
+
const skillMdPath = path.join(skillsDir, name, 'SKILL.md');
|
|
887
|
+
if (fs.existsSync(skillMdPath)) {
|
|
888
|
+
const skillMd = fs.readFileSync(skillMdPath, 'utf8');
|
|
889
|
+
// Strategy 1: YAML Frontmatter (description: ...)
|
|
890
|
+
const yamlMatch = skillMd.match(/^description:\s*(.*)$/m);
|
|
891
|
+
if (yamlMatch) {
|
|
892
|
+
desc = yamlMatch[1].trim();
|
|
893
|
+
} else {
|
|
894
|
+
// Strategy 2: First non-header, non-empty line
|
|
895
|
+
const lines = skillMd.split('\n');
|
|
896
|
+
for (const line of lines) {
|
|
897
|
+
const trimmed = line.trim();
|
|
898
|
+
if (
|
|
899
|
+
trimmed &&
|
|
900
|
+
!trimmed.startsWith('#') &&
|
|
901
|
+
!trimmed.startsWith('---') &&
|
|
902
|
+
!trimmed.startsWith('```')
|
|
903
|
+
) {
|
|
904
|
+
desc = trimmed;
|
|
905
|
+
break;
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
if (desc.length > 100) desc = desc.slice(0, 100) + '...';
|
|
910
|
+
}
|
|
911
|
+
} catch (e2) {}
|
|
912
|
+
}
|
|
913
|
+
return `- **${name}**: ${desc}`;
|
|
914
|
+
});
|
|
915
|
+
fileList = skills.join('\n');
|
|
916
|
+
|
|
917
|
+
// Write cache
|
|
918
|
+
try {
|
|
919
|
+
fs.writeFileSync(SKILLS_CACHE_FILE, JSON.stringify({ list: fileList }, null, 2));
|
|
920
|
+
} catch (e) {}
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
} catch (e) {
|
|
924
|
+
fileList = `Error listing skills: ${e.message}`;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
const mutationDirective = getMutationDirective(recentMasterLog);
|
|
928
|
+
const healthReport = checkSystemHealth();
|
|
929
|
+
|
|
930
|
+
// Feature: Mood Awareness (Mode E - Personalization)
|
|
931
|
+
let moodStatus = 'Mood: Unknown';
|
|
932
|
+
try {
|
|
933
|
+
const moodFile = path.join(MEMORY_DIR, 'mood.json');
|
|
934
|
+
if (fs.existsSync(moodFile)) {
|
|
935
|
+
const moodData = JSON.parse(fs.readFileSync(moodFile, 'utf8'));
|
|
936
|
+
moodStatus = `Mood: ${moodData.current_mood || 'Neutral'} (Intensity: ${moodData.intensity || 0})`;
|
|
937
|
+
}
|
|
938
|
+
} catch (e) {}
|
|
939
|
+
|
|
940
|
+
const scanTime = Date.now() - startTime;
|
|
941
|
+
const memorySize = fs.existsSync(MEMORY_FILE) ? fs.statSync(MEMORY_FILE).size : 0;
|
|
942
|
+
|
|
943
|
+
let syncDirective = 'Workspace sync: optional/disabled in this environment.';
|
|
944
|
+
|
|
945
|
+
// Check for git-sync skill availability
|
|
946
|
+
const hasGitSync = fs.existsSync(path.join(skillsDir, 'git-sync'));
|
|
947
|
+
if (hasGitSync) {
|
|
948
|
+
syncDirective = 'Workspace sync: run skills/git-sync/sync.sh "Evolution: Workspace Sync"';
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
const genes = loadGenes();
|
|
952
|
+
const capsules = loadCapsules();
|
|
953
|
+
const recentEvents = (() => {
|
|
954
|
+
try {
|
|
955
|
+
const all = readAllEvents();
|
|
956
|
+
return Array.isArray(all) ? all.filter(e => e && e.type === 'EvolutionEvent').slice(-80) : [];
|
|
957
|
+
} catch (e) {
|
|
958
|
+
return [];
|
|
959
|
+
}
|
|
960
|
+
})();
|
|
961
|
+
const signals = extractSignals({
|
|
962
|
+
recentSessionTranscript: recentMasterLog,
|
|
963
|
+
todayLog,
|
|
964
|
+
memorySnippet,
|
|
965
|
+
userSnippet,
|
|
966
|
+
recentEvents,
|
|
967
|
+
});
|
|
968
|
+
|
|
969
|
+
if (dormantHypothesis && Array.isArray(dormantHypothesis.signals) && dormantHypothesis.signals.length > 0) {
|
|
970
|
+
var dormantSignals = dormantHypothesis.signals;
|
|
971
|
+
var injected = 0;
|
|
972
|
+
for (var dsi = 0; dsi < dormantSignals.length; dsi++) {
|
|
973
|
+
if (!signals.includes(dormantSignals[dsi])) {
|
|
974
|
+
signals.push(dormantSignals[dsi]);
|
|
975
|
+
injected++;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
if (injected > 0) {
|
|
979
|
+
console.log('[DormantHypothesis] Injected ' + injected + ' signal(s) from previous interrupted cycle.');
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// --- Hub Task Auto-Claim (with proactive questions) ---
|
|
984
|
+
// Generate questions from current context, piggyback them on the fetch call,
|
|
985
|
+
// then pick the best task and auto-claim it.
|
|
986
|
+
let activeTask = null;
|
|
987
|
+
let proactiveQuestions = [];
|
|
988
|
+
try {
|
|
989
|
+
proactiveQuestions = generateQuestions({
|
|
990
|
+
signals,
|
|
991
|
+
recentEvents,
|
|
992
|
+
sessionTranscript: recentMasterLog,
|
|
993
|
+
memorySnippet: memorySnippet,
|
|
994
|
+
});
|
|
995
|
+
if (proactiveQuestions.length > 0) {
|
|
996
|
+
console.log(`[QuestionGenerator] Generated ${proactiveQuestions.length} proactive question(s).`);
|
|
997
|
+
}
|
|
998
|
+
} catch (e) {
|
|
999
|
+
console.log(`[QuestionGenerator] Generation failed (non-fatal): ${e.message}`);
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// --- Auto GitHub Issue Reporter ---
|
|
1003
|
+
// When persistent failures are detected, file an issue to the upstream repo
|
|
1004
|
+
// with sanitized logs and environment info.
|
|
1005
|
+
try {
|
|
1006
|
+
await maybeReportIssue({
|
|
1007
|
+
signals,
|
|
1008
|
+
recentEvents,
|
|
1009
|
+
sessionLog: recentMasterLog,
|
|
1010
|
+
});
|
|
1011
|
+
} catch (e) {
|
|
1012
|
+
console.log(`[IssueReporter] Check failed (non-fatal): ${e.message}`);
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
// LessonL: lessons received from Hub during fetch
|
|
1016
|
+
let hubLessons = [];
|
|
1017
|
+
|
|
1018
|
+
try {
|
|
1019
|
+
const fetchResult = await fetchTasks({ questions: proactiveQuestions });
|
|
1020
|
+
const hubTasks = fetchResult.tasks || [];
|
|
1021
|
+
|
|
1022
|
+
if (fetchResult.questions_created && fetchResult.questions_created.length > 0) {
|
|
1023
|
+
const created = fetchResult.questions_created.filter(function(q) { return !q.error; });
|
|
1024
|
+
const failed = fetchResult.questions_created.filter(function(q) { return q.error; });
|
|
1025
|
+
if (created.length > 0) {
|
|
1026
|
+
console.log(`[QuestionGenerator] Hub accepted ${created.length} question(s) as bounties.`);
|
|
1027
|
+
}
|
|
1028
|
+
if (failed.length > 0) {
|
|
1029
|
+
console.log(`[QuestionGenerator] Hub rejected ${failed.length} question(s): ${failed.map(function(q) { return q.error; }).join(', ')}`);
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
// LessonL: capture relevant lessons from Hub
|
|
1034
|
+
if (Array.isArray(fetchResult.relevant_lessons) && fetchResult.relevant_lessons.length > 0) {
|
|
1035
|
+
hubLessons = fetchResult.relevant_lessons;
|
|
1036
|
+
console.log(`[LessonBank] Received ${hubLessons.length} lesson(s) from ecosystem.`);
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
if (hubTasks.length > 0) {
|
|
1040
|
+
let taskMemoryEvents = [];
|
|
1041
|
+
try {
|
|
1042
|
+
const { tryReadMemoryGraphEvents } = require('./gep/memoryGraph');
|
|
1043
|
+
taskMemoryEvents = tryReadMemoryGraphEvents(1000);
|
|
1044
|
+
} catch {}
|
|
1045
|
+
const best = selectBestTask(hubTasks, taskMemoryEvents);
|
|
1046
|
+
if (best) {
|
|
1047
|
+
const alreadyClaimed = best.status === 'claimed';
|
|
1048
|
+
let claimed = alreadyClaimed;
|
|
1049
|
+
if (!alreadyClaimed) {
|
|
1050
|
+
const commitDeadline = estimateCommitmentDeadline(best);
|
|
1051
|
+
claimed = await claimTask(best.id || best.task_id, commitDeadline ? { commitment_deadline: commitDeadline } : undefined);
|
|
1052
|
+
if (claimed && commitDeadline) {
|
|
1053
|
+
best._commitment_deadline = commitDeadline;
|
|
1054
|
+
console.log(`[Commitment] Deadline set: ${commitDeadline}`);
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
if (claimed) {
|
|
1058
|
+
activeTask = best;
|
|
1059
|
+
const taskSignals = taskToSignals(best);
|
|
1060
|
+
for (const sig of taskSignals) {
|
|
1061
|
+
if (!signals.includes(sig)) signals.unshift(sig);
|
|
1062
|
+
}
|
|
1063
|
+
console.log(`[TaskReceiver] ${alreadyClaimed ? 'Resuming' : 'Claimed'} task: "${best.title || best.id}" (${taskSignals.length} signals injected)`);
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
} catch (e) {
|
|
1068
|
+
console.log(`[TaskReceiver] Fetch/claim failed (non-fatal): ${e.message}`);
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
// --- Commitment: check for overdue tasks from heartbeat ---
|
|
1072
|
+
// If Hub reported overdue tasks, prioritize resuming them by injecting their
|
|
1073
|
+
// signals at the front. This does not change activeTask selection (the overdue
|
|
1074
|
+
// task should already be claimed/active from a previous cycle).
|
|
1075
|
+
try {
|
|
1076
|
+
const { consumeOverdueTasks } = require('./gep/a2aProtocol');
|
|
1077
|
+
const overdueTasks = consumeOverdueTasks();
|
|
1078
|
+
if (overdueTasks.length > 0) {
|
|
1079
|
+
for (const ot of overdueTasks) {
|
|
1080
|
+
const otId = ot.task_id || ot.id;
|
|
1081
|
+
if (activeTask && (activeTask.id === otId || activeTask.task_id === otId)) {
|
|
1082
|
+
console.warn(`[Commitment] Active task "${activeTask.title || otId}" is OVERDUE -- prioritizing completion.`);
|
|
1083
|
+
signals.unshift('overdue_task', 'urgent');
|
|
1084
|
+
break;
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
} catch {}
|
|
1089
|
+
|
|
1090
|
+
// --- Worker Pool: select task from heartbeat available_work (deferred claim) ---
|
|
1091
|
+
// Only remember the best task and inject its signals; actual claim+complete
|
|
1092
|
+
// happens atomically in solidify.js after a successful evolution cycle.
|
|
1093
|
+
if (!activeTask && process.env.WORKER_ENABLED === '1') {
|
|
1094
|
+
try {
|
|
1095
|
+
const { consumeAvailableWork } = require('./gep/a2aProtocol');
|
|
1096
|
+
const workerTasks = consumeAvailableWork();
|
|
1097
|
+
if (workerTasks.length > 0) {
|
|
1098
|
+
let taskMemoryEvents = [];
|
|
1099
|
+
try {
|
|
1100
|
+
const { tryReadMemoryGraphEvents } = require('./gep/memoryGraph');
|
|
1101
|
+
taskMemoryEvents = tryReadMemoryGraphEvents(1000);
|
|
1102
|
+
} catch {}
|
|
1103
|
+
const best = selectBestTask(workerTasks, taskMemoryEvents);
|
|
1104
|
+
if (best) {
|
|
1105
|
+
activeTask = best;
|
|
1106
|
+
activeTask._worker_pending = true;
|
|
1107
|
+
const taskSignals = taskToSignals(best);
|
|
1108
|
+
for (const sig of taskSignals) {
|
|
1109
|
+
if (!signals.includes(sig)) signals.unshift(sig);
|
|
1110
|
+
}
|
|
1111
|
+
console.log(`[WorkerPool] Selected worker task (deferred claim): "${best.title || best.id}" (${taskSignals.length} signals injected)`);
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
} catch (e) {
|
|
1115
|
+
console.log(`[WorkerPool] Task selection failed (non-fatal): ${e.message}`);
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
const recentErrorMatches = recentMasterLog.match(/\[ERROR|Error:|Exception:|FAIL|Failed|"isError":true/gi) || [];
|
|
1120
|
+
const recentErrorCount = recentErrorMatches.length;
|
|
1121
|
+
|
|
1122
|
+
const evidence = {
|
|
1123
|
+
// Keep short; do not store full transcripts in the graph.
|
|
1124
|
+
recent_session_tail: String(recentMasterLog || '').slice(-6000),
|
|
1125
|
+
today_log_tail: String(todayLog || '').slice(-2500),
|
|
1126
|
+
};
|
|
1127
|
+
|
|
1128
|
+
const sessionScope = getSessionScope();
|
|
1129
|
+
const observations = {
|
|
1130
|
+
agent: AGENT_NAME,
|
|
1131
|
+
session_scope: sessionScope || null,
|
|
1132
|
+
drift_enabled: IS_RANDOM_DRIFT,
|
|
1133
|
+
review_mode: IS_REVIEW_MODE,
|
|
1134
|
+
dry_run: IS_DRY_RUN,
|
|
1135
|
+
system_health: healthReport,
|
|
1136
|
+
mood: moodStatus,
|
|
1137
|
+
scan_ms: scanTime,
|
|
1138
|
+
memory_size_bytes: memorySize,
|
|
1139
|
+
recent_error_count: recentErrorCount,
|
|
1140
|
+
node: process.version,
|
|
1141
|
+
platform: process.platform,
|
|
1142
|
+
cwd: process.cwd(),
|
|
1143
|
+
evidence,
|
|
1144
|
+
};
|
|
1145
|
+
|
|
1146
|
+
if (sessionScope) {
|
|
1147
|
+
console.log(`[SessionScope] Active scope: "${sessionScope}". Evolution state and memory graph are isolated.`);
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
// Memory Graph: close last action with an inferred outcome (append-only graph, mutable state).
|
|
1151
|
+
try {
|
|
1152
|
+
recordOutcomeFromState({ signals, observations });
|
|
1153
|
+
} catch (e) {
|
|
1154
|
+
// If we can't read/write memory graph, refuse to evolve (no "memoryless evolution").
|
|
1155
|
+
console.error(`[MemoryGraph] Outcome write failed: ${e.message}`);
|
|
1156
|
+
console.error(`[MemoryGraph] Refusing to evolve without causal memory. Target: ${memoryGraphPath()}`);
|
|
1157
|
+
throw new Error(`MemoryGraph Outcome write failed: ${e.message}`);
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
// Memory Graph: record current signals as a first-class node. If this fails, refuse to evolve.
|
|
1161
|
+
try {
|
|
1162
|
+
recordSignalSnapshot({ signals, observations });
|
|
1163
|
+
} catch (e) {
|
|
1164
|
+
console.error(`[MemoryGraph] Signal snapshot write failed: ${e.message}`);
|
|
1165
|
+
console.error(`[MemoryGraph] Refusing to evolve without causal memory. Target: ${memoryGraphPath()}`);
|
|
1166
|
+
throw new Error(`MemoryGraph Signal snapshot write failed: ${e.message}`);
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
// Capability candidates (structured, short): persist and preview.
|
|
1170
|
+
const newCandidates = extractCapabilityCandidates({
|
|
1171
|
+
recentSessionTranscript: recentMasterLog,
|
|
1172
|
+
signals,
|
|
1173
|
+
});
|
|
1174
|
+
for (const c of newCandidates) {
|
|
1175
|
+
try {
|
|
1176
|
+
appendCandidateJsonl(c);
|
|
1177
|
+
} catch (e) {}
|
|
1178
|
+
}
|
|
1179
|
+
const recentCandidates = readRecentCandidates(20);
|
|
1180
|
+
const capabilityCandidatesPreview = renderCandidatesPreview(recentCandidates.slice(-8), 1600);
|
|
1181
|
+
|
|
1182
|
+
// External candidate zone (A2A receive): only surface candidates when local signals trigger them.
|
|
1183
|
+
// External candidates are NEVER executed directly; they must be validated and promoted first.
|
|
1184
|
+
let externalCandidatesPreview = '(none)';
|
|
1185
|
+
try {
|
|
1186
|
+
const external = readRecentExternalCandidates(50);
|
|
1187
|
+
const list = Array.isArray(external) ? external : [];
|
|
1188
|
+
const capsulesOnly = list.filter(x => x && x.type === 'Capsule');
|
|
1189
|
+
const genesOnly = list.filter(x => x && x.type === 'Gene');
|
|
1190
|
+
|
|
1191
|
+
const matchedExternalGenes = genesOnly
|
|
1192
|
+
.map(g => {
|
|
1193
|
+
const pats = Array.isArray(g.signals_match) ? g.signals_match : [];
|
|
1194
|
+
const hit = pats.reduce((acc, p) => (matchPatternToSignals(p, signals) ? acc + 1 : acc), 0);
|
|
1195
|
+
return { gene: g, hit };
|
|
1196
|
+
})
|
|
1197
|
+
.filter(x => x.hit > 0)
|
|
1198
|
+
.sort((a, b) => b.hit - a.hit)
|
|
1199
|
+
.slice(0, 3)
|
|
1200
|
+
.map(x => x.gene);
|
|
1201
|
+
|
|
1202
|
+
const matchedExternalCapsules = capsulesOnly
|
|
1203
|
+
.map(c => {
|
|
1204
|
+
const triggers = Array.isArray(c.trigger) ? c.trigger : [];
|
|
1205
|
+
const score = triggers.reduce((acc, t) => (matchPatternToSignals(t, signals) ? acc + 1 : acc), 0);
|
|
1206
|
+
return { capsule: c, score };
|
|
1207
|
+
})
|
|
1208
|
+
.filter(x => x.score > 0)
|
|
1209
|
+
.sort((a, b) => b.score - a.score)
|
|
1210
|
+
.slice(0, 3)
|
|
1211
|
+
.map(x => x.capsule);
|
|
1212
|
+
|
|
1213
|
+
if (matchedExternalGenes.length || matchedExternalCapsules.length) {
|
|
1214
|
+
externalCandidatesPreview = `\`\`\`json\n${JSON.stringify(
|
|
1215
|
+
[
|
|
1216
|
+
...matchedExternalGenes.map(g => ({
|
|
1217
|
+
type: g.type,
|
|
1218
|
+
id: g.id,
|
|
1219
|
+
category: g.category || null,
|
|
1220
|
+
signals_match: g.signals_match || [],
|
|
1221
|
+
a2a: g.a2a || null,
|
|
1222
|
+
})),
|
|
1223
|
+
...matchedExternalCapsules.map(c => ({
|
|
1224
|
+
type: c.type,
|
|
1225
|
+
id: c.id,
|
|
1226
|
+
trigger: c.trigger,
|
|
1227
|
+
gene: c.gene,
|
|
1228
|
+
summary: c.summary,
|
|
1229
|
+
confidence: c.confidence,
|
|
1230
|
+
blast_radius: c.blast_radius || null,
|
|
1231
|
+
outcome: c.outcome || null,
|
|
1232
|
+
success_streak: c.success_streak || null,
|
|
1233
|
+
a2a: c.a2a || null,
|
|
1234
|
+
})),
|
|
1235
|
+
],
|
|
1236
|
+
null,
|
|
1237
|
+
2
|
|
1238
|
+
)}\n\`\`\``;
|
|
1239
|
+
}
|
|
1240
|
+
} catch (e) {}
|
|
1241
|
+
|
|
1242
|
+
// Search-First Evolution: query Hub for reusable solutions before local reasoning.
|
|
1243
|
+
let hubHit = null;
|
|
1244
|
+
try {
|
|
1245
|
+
hubHit = await hubSearch(signals, { timeoutMs: 8000 });
|
|
1246
|
+
if (hubHit && hubHit.hit) {
|
|
1247
|
+
console.log(`[SearchFirst] Hub hit: asset=${hubHit.asset_id}, score=${hubHit.score}, mode=${hubHit.mode}`);
|
|
1248
|
+
} else {
|
|
1249
|
+
console.log(`[SearchFirst] No hub match (reason: ${hubHit && hubHit.reason ? hubHit.reason : 'unknown'}). Proceeding with local evolution.`);
|
|
1250
|
+
}
|
|
1251
|
+
} catch (e) {
|
|
1252
|
+
console.log(`[SearchFirst] Hub search failed (non-fatal): ${e.message}`);
|
|
1253
|
+
hubHit = { hit: false, reason: 'exception' };
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
// Memory Graph reasoning: prefer high-confidence paths, suppress known low-success paths (unless drift is explicit).
|
|
1257
|
+
let memoryAdvice = null;
|
|
1258
|
+
try {
|
|
1259
|
+
memoryAdvice = getMemoryAdvice({ signals, genes, driftEnabled: IS_RANDOM_DRIFT });
|
|
1260
|
+
} catch (e) {
|
|
1261
|
+
console.error(`[MemoryGraph] Read failed: ${e.message}`);
|
|
1262
|
+
console.error(`[MemoryGraph] Refusing to evolve without causal memory. Target: ${memoryGraphPath()}`);
|
|
1263
|
+
throw new Error(`MemoryGraph Read failed: ${e.message}`);
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
// Reflection Phase: periodically pause to assess evolution strategy.
|
|
1267
|
+
try {
|
|
1268
|
+
const cycleState = fs.existsSync(STATE_FILE) ? JSON.parse(fs.readFileSync(STATE_FILE, 'utf8')) : {};
|
|
1269
|
+
const cycleCount = cycleState.cycleCount || 0;
|
|
1270
|
+
if (shouldReflect({ cycleCount, recentEvents })) {
|
|
1271
|
+
const narrativeSummary = loadNarrativeSummary(3000);
|
|
1272
|
+
const reflectionCtx = buildReflectionContext({
|
|
1273
|
+
recentEvents,
|
|
1274
|
+
signals,
|
|
1275
|
+
memoryAdvice,
|
|
1276
|
+
narrative: narrativeSummary,
|
|
1277
|
+
});
|
|
1278
|
+
recordReflection({
|
|
1279
|
+
cycle_count: cycleCount,
|
|
1280
|
+
signals_snapshot: signals.slice(0, 20),
|
|
1281
|
+
preferred_gene: memoryAdvice && memoryAdvice.preferredGeneId ? memoryAdvice.preferredGeneId : null,
|
|
1282
|
+
banned_genes: memoryAdvice && Array.isArray(memoryAdvice.bannedGeneIds) ? memoryAdvice.bannedGeneIds : [],
|
|
1283
|
+
context_preview: reflectionCtx.slice(0, 1000),
|
|
1284
|
+
});
|
|
1285
|
+
console.log(`[Reflection] Strategic reflection recorded at cycle ${cycleCount}.`);
|
|
1286
|
+
}
|
|
1287
|
+
} catch (e) {
|
|
1288
|
+
console.log('[Reflection] Failed (non-fatal): ' + (e && e.message ? e.message : e));
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
var recentFailedCapsules = [];
|
|
1292
|
+
try {
|
|
1293
|
+
recentFailedCapsules = readRecentFailedCapsules(50);
|
|
1294
|
+
} catch (e) {
|
|
1295
|
+
console.log('[FailedCapsules] Read failed (non-fatal): ' + e.message);
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
const { selectedGene, capsuleCandidates, selector } = selectGeneAndCapsule({
|
|
1299
|
+
genes,
|
|
1300
|
+
capsules,
|
|
1301
|
+
signals,
|
|
1302
|
+
memoryAdvice,
|
|
1303
|
+
driftEnabled: IS_RANDOM_DRIFT,
|
|
1304
|
+
failedCapsules: recentFailedCapsules,
|
|
1305
|
+
});
|
|
1306
|
+
|
|
1307
|
+
const selectedBy = memoryAdvice && memoryAdvice.preferredGeneId ? 'memory_graph+selector' : 'selector';
|
|
1308
|
+
const capsulesUsed = Array.isArray(capsuleCandidates)
|
|
1309
|
+
? capsuleCandidates.map(c => (c && c.id ? String(c.id) : null)).filter(Boolean)
|
|
1310
|
+
: [];
|
|
1311
|
+
const selectedCapsuleId = capsulesUsed.length ? capsulesUsed[0] : null;
|
|
1312
|
+
|
|
1313
|
+
// Personality selection (natural selection + small mutation when triggered).
|
|
1314
|
+
// This state is persisted in MEMORY_DIR and is treated as an evolution control surface (not role-play).
|
|
1315
|
+
const personalitySelection = selectPersonalityForRun({
|
|
1316
|
+
driftEnabled: IS_RANDOM_DRIFT,
|
|
1317
|
+
signals,
|
|
1318
|
+
recentEvents,
|
|
1319
|
+
});
|
|
1320
|
+
const personalityState = personalitySelection && personalitySelection.personality_state ? personalitySelection.personality_state : null;
|
|
1321
|
+
|
|
1322
|
+
// Mutation object is mandatory for every evolution run.
|
|
1323
|
+
const tail = Array.isArray(recentEvents) ? recentEvents.slice(-6) : [];
|
|
1324
|
+
const tailOutcomes = tail
|
|
1325
|
+
.map(e => (e && e.outcome && e.outcome.status ? String(e.outcome.status) : null))
|
|
1326
|
+
.filter(Boolean);
|
|
1327
|
+
const stableSuccess = tailOutcomes.length >= 6 && tailOutcomes.every(s => s === 'success');
|
|
1328
|
+
const tailAvgScore =
|
|
1329
|
+
tail.length > 0
|
|
1330
|
+
? tail.reduce((acc, e) => acc + (e && e.outcome && Number.isFinite(Number(e.outcome.score)) ? Number(e.outcome.score) : 0), 0) /
|
|
1331
|
+
tail.length
|
|
1332
|
+
: 0;
|
|
1333
|
+
const innovationPressure =
|
|
1334
|
+
!IS_RANDOM_DRIFT &&
|
|
1335
|
+
personalityState &&
|
|
1336
|
+
Number.isFinite(Number(personalityState.creativity)) &&
|
|
1337
|
+
Number(personalityState.creativity) >= 0.75 &&
|
|
1338
|
+
stableSuccess &&
|
|
1339
|
+
tailAvgScore >= 0.7;
|
|
1340
|
+
const forceInnovation =
|
|
1341
|
+
String(process.env.FORCE_INNOVATION || process.env.EVOLVE_FORCE_INNOVATION || '').toLowerCase() === 'true';
|
|
1342
|
+
const mutationInnovateMode = !!IS_RANDOM_DRIFT || !!innovationPressure || !!forceInnovation;
|
|
1343
|
+
const mutationSignals = innovationPressure ? [...(Array.isArray(signals) ? signals : []), 'stable_success_plateau'] : signals;
|
|
1344
|
+
const mutationSignalsEffective = forceInnovation
|
|
1345
|
+
? [...(Array.isArray(mutationSignals) ? mutationSignals : []), 'force_innovation']
|
|
1346
|
+
: mutationSignals;
|
|
1347
|
+
|
|
1348
|
+
const allowHighRisk =
|
|
1349
|
+
!!IS_RANDOM_DRIFT &&
|
|
1350
|
+
!!personalitySelection &&
|
|
1351
|
+
!!personalitySelection.personality_known &&
|
|
1352
|
+
personalityState &&
|
|
1353
|
+
isHighRiskMutationAllowed(personalityState) &&
|
|
1354
|
+
Number(personalityState.rigor) >= 0.8 &&
|
|
1355
|
+
Number(personalityState.risk_tolerance) <= 0.3 &&
|
|
1356
|
+
!(Array.isArray(signals) && signals.includes('log_error'));
|
|
1357
|
+
const mutation = buildMutation({
|
|
1358
|
+
signals: mutationSignalsEffective,
|
|
1359
|
+
selectedGene,
|
|
1360
|
+
driftEnabled: mutationInnovateMode,
|
|
1361
|
+
personalityState,
|
|
1362
|
+
allowHighRisk,
|
|
1363
|
+
});
|
|
1364
|
+
|
|
1365
|
+
// Memory Graph: record hypothesis bridging Signal -> Action. If this fails, refuse to evolve.
|
|
1366
|
+
let hypothesisId = null;
|
|
1367
|
+
try {
|
|
1368
|
+
const hyp = recordHypothesis({
|
|
1369
|
+
signals,
|
|
1370
|
+
mutation,
|
|
1371
|
+
personality_state: personalityState,
|
|
1372
|
+
selectedGene,
|
|
1373
|
+
selector,
|
|
1374
|
+
driftEnabled: mutationInnovateMode,
|
|
1375
|
+
selectedBy,
|
|
1376
|
+
capsulesUsed,
|
|
1377
|
+
observations,
|
|
1378
|
+
});
|
|
1379
|
+
hypothesisId = hyp && hyp.hypothesisId ? hyp.hypothesisId : null;
|
|
1380
|
+
} catch (e) {
|
|
1381
|
+
console.error(`[MemoryGraph] Hypothesis write failed: ${e.message}`);
|
|
1382
|
+
console.error(`[MemoryGraph] Refusing to evolve without causal memory. Target: ${memoryGraphPath()}`);
|
|
1383
|
+
throw new Error(`MemoryGraph Hypothesis write failed: ${e.message}`);
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
// Memory Graph: record the chosen causal path for this run. If this fails, refuse to output a mutation prompt.
|
|
1387
|
+
try {
|
|
1388
|
+
recordAttempt({
|
|
1389
|
+
signals,
|
|
1390
|
+
mutation,
|
|
1391
|
+
personality_state: personalityState,
|
|
1392
|
+
selectedGene,
|
|
1393
|
+
selector,
|
|
1394
|
+
driftEnabled: mutationInnovateMode,
|
|
1395
|
+
selectedBy,
|
|
1396
|
+
hypothesisId,
|
|
1397
|
+
capsulesUsed,
|
|
1398
|
+
observations,
|
|
1399
|
+
});
|
|
1400
|
+
} catch (e) {
|
|
1401
|
+
console.error(`[MemoryGraph] Attempt write failed: ${e.message}`);
|
|
1402
|
+
console.error(`[MemoryGraph] Refusing to evolve without causal memory. Target: ${memoryGraphPath()}`);
|
|
1403
|
+
throw new Error(`MemoryGraph Attempt write failed: ${e.message}`);
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
// Solidify state: capture minimal, auditable context for post-patch validation + asset write.
|
|
1407
|
+
// This enforces strict protocol closure after patch application.
|
|
1408
|
+
try {
|
|
1409
|
+
const runId = `run_${Date.now()}`;
|
|
1410
|
+
const parentEventId = getLastEventId();
|
|
1411
|
+
const selectedBy = memoryAdvice && memoryAdvice.preferredGeneId ? 'memory_graph+selector' : 'selector';
|
|
1412
|
+
|
|
1413
|
+
// Baseline snapshot (before any edits).
|
|
1414
|
+
let baselineUntracked = [];
|
|
1415
|
+
let baselineHead = null;
|
|
1416
|
+
try {
|
|
1417
|
+
const out = execSync('git ls-files --others --exclude-standard', {
|
|
1418
|
+
cwd: REPO_ROOT,
|
|
1419
|
+
encoding: 'utf8',
|
|
1420
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
1421
|
+
timeout: 4000,
|
|
1422
|
+
windowsHide: true,
|
|
1423
|
+
});
|
|
1424
|
+
baselineUntracked = String(out)
|
|
1425
|
+
.split('\n')
|
|
1426
|
+
.map(l => l.trim())
|
|
1427
|
+
.filter(Boolean);
|
|
1428
|
+
} catch (e) {}
|
|
1429
|
+
|
|
1430
|
+
try {
|
|
1431
|
+
const out = execSync('git rev-parse HEAD', {
|
|
1432
|
+
cwd: REPO_ROOT,
|
|
1433
|
+
encoding: 'utf8',
|
|
1434
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
1435
|
+
timeout: 4000,
|
|
1436
|
+
windowsHide: true,
|
|
1437
|
+
});
|
|
1438
|
+
baselineHead = String(out || '').trim() || null;
|
|
1439
|
+
} catch (e) {}
|
|
1440
|
+
|
|
1441
|
+
const maxFiles =
|
|
1442
|
+
selectedGene && selectedGene.constraints && Number.isFinite(Number(selectedGene.constraints.max_files))
|
|
1443
|
+
? Number(selectedGene.constraints.max_files)
|
|
1444
|
+
: 12;
|
|
1445
|
+
const blastRadiusEstimate = {
|
|
1446
|
+
files: Number.isFinite(maxFiles) && maxFiles > 0 ? maxFiles : 0,
|
|
1447
|
+
lines: Number.isFinite(maxFiles) && maxFiles > 0 ? Math.round(maxFiles * 80) : 0,
|
|
1448
|
+
};
|
|
1449
|
+
|
|
1450
|
+
// Merge into existing state to preserve last_solidify (do not wipe it).
|
|
1451
|
+
const prevState = readStateForSolidify();
|
|
1452
|
+
prevState.last_run = {
|
|
1453
|
+
run_id: runId,
|
|
1454
|
+
created_at: new Date().toISOString(),
|
|
1455
|
+
parent_event_id: parentEventId || null,
|
|
1456
|
+
selected_gene_id: selectedGene && selectedGene.id ? selectedGene.id : null,
|
|
1457
|
+
selected_capsule_id: selectedCapsuleId,
|
|
1458
|
+
selector: selector || null,
|
|
1459
|
+
signals: Array.isArray(signals) ? signals : [],
|
|
1460
|
+
mutation: mutation || null,
|
|
1461
|
+
mutation_id: mutation && mutation.id ? mutation.id : null,
|
|
1462
|
+
personality_state: personalityState || null,
|
|
1463
|
+
personality_key: personalitySelection && personalitySelection.personality_key ? personalitySelection.personality_key : null,
|
|
1464
|
+
personality_known: !!(personalitySelection && personalitySelection.personality_known),
|
|
1465
|
+
personality_mutations:
|
|
1466
|
+
personalitySelection && Array.isArray(personalitySelection.personality_mutations)
|
|
1467
|
+
? personalitySelection.personality_mutations
|
|
1468
|
+
: [],
|
|
1469
|
+
drift: !!IS_RANDOM_DRIFT,
|
|
1470
|
+
selected_by: selectedBy,
|
|
1471
|
+
source_type: hubHit && hubHit.hit ? (hubHit.mode === 'direct' ? 'reused' : 'reference') : 'generated',
|
|
1472
|
+
reused_asset_id: hubHit && hubHit.hit ? (hubHit.asset_id || null) : null,
|
|
1473
|
+
reused_source_node: hubHit && hubHit.hit ? (hubHit.source_node_id || null) : null,
|
|
1474
|
+
reused_chain_id: hubHit && hubHit.hit ? (hubHit.chain_id || null) : null,
|
|
1475
|
+
baseline_untracked: baselineUntracked,
|
|
1476
|
+
baseline_git_head: baselineHead,
|
|
1477
|
+
blast_radius_estimate: blastRadiusEstimate,
|
|
1478
|
+
active_task_id: activeTask ? (activeTask.id || activeTask.task_id || null) : null,
|
|
1479
|
+
active_task_title: activeTask ? (activeTask.title || null) : null,
|
|
1480
|
+
worker_assignment_id: activeTask ? (activeTask._worker_assignment_id || null) : null,
|
|
1481
|
+
worker_pending: activeTask ? (activeTask._worker_pending || false) : false,
|
|
1482
|
+
commitment_deadline: activeTask ? (activeTask._commitment_deadline || null) : null,
|
|
1483
|
+
applied_lessons: hubLessons.map(function(l) { return l.lesson_id; }).filter(Boolean),
|
|
1484
|
+
hub_lessons: hubLessons,
|
|
1485
|
+
};
|
|
1486
|
+
writeStateForSolidify(prevState);
|
|
1487
|
+
|
|
1488
|
+
if (hubHit && hubHit.hit) {
|
|
1489
|
+
const assetAction = hubHit.mode === 'direct' ? 'asset_reuse' : 'asset_reference';
|
|
1490
|
+
logAssetCall({
|
|
1491
|
+
run_id: runId,
|
|
1492
|
+
action: assetAction,
|
|
1493
|
+
asset_id: hubHit.asset_id || null,
|
|
1494
|
+
asset_type: hubHit.match && hubHit.match.type ? hubHit.match.type : null,
|
|
1495
|
+
source_node_id: hubHit.source_node_id || null,
|
|
1496
|
+
chain_id: hubHit.chain_id || null,
|
|
1497
|
+
score: hubHit.score || null,
|
|
1498
|
+
mode: hubHit.mode,
|
|
1499
|
+
signals: Array.isArray(signals) ? signals : [],
|
|
1500
|
+
extra: {
|
|
1501
|
+
selected_gene_id: selectedGene && selectedGene.id ? selectedGene.id : null,
|
|
1502
|
+
task_id: activeTask ? (activeTask.id || activeTask.task_id || null) : null,
|
|
1503
|
+
},
|
|
1504
|
+
});
|
|
1505
|
+
}
|
|
1506
|
+
} catch (e) {
|
|
1507
|
+
console.error(`[SolidifyState] Write failed: ${e.message}`);
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
const genesPreview = `\`\`\`json\n${JSON.stringify(genes.slice(0, 6), null, 2)}\n\`\`\``;
|
|
1511
|
+
const capsulesPreview = `\`\`\`json\n${JSON.stringify(capsules.slice(-3), null, 2)}\n\`\`\``;
|
|
1512
|
+
|
|
1513
|
+
const reviewNote = IS_REVIEW_MODE
|
|
1514
|
+
? 'Review mode: before significant edits, pause and ask the user for confirmation.'
|
|
1515
|
+
: 'Review mode: disabled.';
|
|
1516
|
+
|
|
1517
|
+
// Build recent evolution history summary for context injection
|
|
1518
|
+
const recentHistorySummary = (() => {
|
|
1519
|
+
if (!recentEvents || recentEvents.length === 0) return '(no prior evolution events)';
|
|
1520
|
+
const last8 = recentEvents.slice(-8);
|
|
1521
|
+
const lines = last8.map((evt, idx) => {
|
|
1522
|
+
const sigs = Array.isArray(evt.signals) ? evt.signals.slice(0, 3).join(', ') : '?';
|
|
1523
|
+
const gene = Array.isArray(evt.genes_used) && evt.genes_used.length ? evt.genes_used[0] : 'none';
|
|
1524
|
+
const outcome = evt.outcome && evt.outcome.status ? evt.outcome.status : '?';
|
|
1525
|
+
const ts = evt.meta && evt.meta.at ? evt.meta.at : (evt.id || '');
|
|
1526
|
+
return ` ${idx + 1}. [${evt.intent || '?'}] signals=[${sigs}] gene=${gene} outcome=${outcome} @${ts}`;
|
|
1527
|
+
});
|
|
1528
|
+
return lines.join('\n');
|
|
1529
|
+
})();
|
|
1530
|
+
|
|
1531
|
+
const context = `
|
|
1532
|
+
Runtime state:
|
|
1533
|
+
- System health: ${healthReport}
|
|
1534
|
+
- Agent state: ${moodStatus}
|
|
1535
|
+
- Scan duration: ${scanTime}ms
|
|
1536
|
+
- Memory size: ${memorySize} bytes
|
|
1537
|
+
- Skills available (if any):
|
|
1538
|
+
${fileList || '[skills directory not found]'}
|
|
1539
|
+
|
|
1540
|
+
Notes:
|
|
1541
|
+
- ${reviewNote}
|
|
1542
|
+
- ${reportingDirective}
|
|
1543
|
+
- ${syncDirective}
|
|
1544
|
+
|
|
1545
|
+
Recent Evolution History (last 8 cycles -- DO NOT repeat the same intent+signal+gene):
|
|
1546
|
+
${recentHistorySummary}
|
|
1547
|
+
IMPORTANT: If you see 3+ consecutive "repair" cycles with the same gene, you MUST switch to "innovate" intent.
|
|
1548
|
+
${(() => {
|
|
1549
|
+
// Compute consecutive failure count from recent events for context injection
|
|
1550
|
+
let cfc = 0;
|
|
1551
|
+
const evts = Array.isArray(recentEvents) ? recentEvents : [];
|
|
1552
|
+
for (let i = evts.length - 1; i >= 0; i--) {
|
|
1553
|
+
if (evts[i] && evts[i].outcome && evts[i].outcome.status === 'failed') cfc++;
|
|
1554
|
+
else break;
|
|
1555
|
+
}
|
|
1556
|
+
if (cfc >= 3) {
|
|
1557
|
+
return `\nFAILURE STREAK WARNING: The last ${cfc} cycles ALL FAILED. You MUST change your approach.\n- Do NOT repeat the same gene/strategy. Pick a completely different approach.\n- If the error is external (API down, binary missing), mark as FAILED and move on.\n- Prefer a minimal safe innovate cycle over yet another failing repair.`;
|
|
1558
|
+
}
|
|
1559
|
+
return '';
|
|
1560
|
+
})()}
|
|
1561
|
+
|
|
1562
|
+
External candidates (A2A receive zone; staged only, never execute directly):
|
|
1563
|
+
${externalCandidatesPreview}
|
|
1564
|
+
|
|
1565
|
+
Global memory (MEMORY.md):
|
|
1566
|
+
\`\`\`
|
|
1567
|
+
${memorySnippet}
|
|
1568
|
+
\`\`\`
|
|
1569
|
+
|
|
1570
|
+
User registry (USER.md):
|
|
1571
|
+
\`\`\`
|
|
1572
|
+
${userSnippet}
|
|
1573
|
+
\`\`\`
|
|
1574
|
+
|
|
1575
|
+
Recent memory snippet:
|
|
1576
|
+
\`\`\`
|
|
1577
|
+
${todayLog.slice(-3000)}
|
|
1578
|
+
\`\`\`
|
|
1579
|
+
|
|
1580
|
+
Recent session transcript:
|
|
1581
|
+
\`\`\`
|
|
1582
|
+
${recentMasterLog}
|
|
1583
|
+
\`\`\`
|
|
1584
|
+
|
|
1585
|
+
Mutation directive:
|
|
1586
|
+
${mutationDirective}
|
|
1587
|
+
`.trim();
|
|
1588
|
+
|
|
1589
|
+
// Build the prompt: in direct-reuse mode, use a minimal reuse prompt.
|
|
1590
|
+
// In reference mode (or no hit), use the full GEP prompt with hub match injected.
|
|
1591
|
+
const isDirectReuse = hubHit && hubHit.hit && hubHit.mode === 'direct';
|
|
1592
|
+
const hubMatchedBlock = hubHit && hubHit.hit && hubHit.mode === 'reference'
|
|
1593
|
+
? buildHubMatchedBlock({ capsule: hubHit.match })
|
|
1594
|
+
: null;
|
|
1595
|
+
|
|
1596
|
+
const prompt = isDirectReuse
|
|
1597
|
+
? buildReusePrompt({
|
|
1598
|
+
capsule: hubHit.match,
|
|
1599
|
+
signals,
|
|
1600
|
+
nowIso: new Date().toISOString(),
|
|
1601
|
+
})
|
|
1602
|
+
: buildGepPrompt({
|
|
1603
|
+
nowIso: new Date().toISOString(),
|
|
1604
|
+
context,
|
|
1605
|
+
signals,
|
|
1606
|
+
selector,
|
|
1607
|
+
parentEventId: getLastEventId(),
|
|
1608
|
+
selectedGene,
|
|
1609
|
+
capsuleCandidates,
|
|
1610
|
+
genesPreview,
|
|
1611
|
+
capsulesPreview,
|
|
1612
|
+
capabilityCandidatesPreview,
|
|
1613
|
+
externalCandidatesPreview,
|
|
1614
|
+
hubMatchedBlock,
|
|
1615
|
+
failedCapsules: recentFailedCapsules,
|
|
1616
|
+
hubLessons,
|
|
1617
|
+
});
|
|
1618
|
+
|
|
1619
|
+
// Optional: emit a compact thought process block for wrappers (noise-controlled).
|
|
1620
|
+
const emitThought = String(process.env.EVOLVE_EMIT_THOUGHT_PROCESS || '').toLowerCase() === 'true';
|
|
1621
|
+
if (emitThought) {
|
|
1622
|
+
const s = Array.isArray(signals) ? signals : [];
|
|
1623
|
+
const thought = [
|
|
1624
|
+
`cycle_id: ${cycleId}`,
|
|
1625
|
+
`signals_count: ${s.length}`,
|
|
1626
|
+
`signals: ${s.slice(0, 12).join(', ')}${s.length > 12 ? ' ...' : ''}`,
|
|
1627
|
+
`selected_gene: ${selectedGene && selectedGene.id ? String(selectedGene.id) : '(none)'}`,
|
|
1628
|
+
`selected_capsule: ${selectedCapsuleId ? String(selectedCapsuleId) : '(none)'}`,
|
|
1629
|
+
`mutation_category: ${mutation && mutation.category ? String(mutation.category) : '(none)'}`,
|
|
1630
|
+
`force_innovation: ${forceInnovation ? 'true' : 'false'}`,
|
|
1631
|
+
`source_type: ${hubHit && hubHit.hit ? (isDirectReuse ? 'reused' : 'reference') : 'generated'}`,
|
|
1632
|
+
`hub_reuse_mode: ${isDirectReuse ? 'direct' : hubMatchedBlock ? 'reference' : 'none'}`,
|
|
1633
|
+
].join('\n');
|
|
1634
|
+
console.log(`[THOUGHT_PROCESS]\n${thought}\n[/THOUGHT_PROCESS]`);
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
const printPrompt = String(process.env.EVOLVE_PRINT_PROMPT || '').toLowerCase() === 'true';
|
|
1638
|
+
|
|
1639
|
+
// Default behavior (v1.4.1+): "execute-by-default" by bridging prompt -> sub-agent via sessions_spawn.
|
|
1640
|
+
// This project is the Brain; the Hand is a spawned executor agent. Wrappers can disable bridging with EVOLVE_BRIDGE=false.
|
|
1641
|
+
if (bridgeEnabled) {
|
|
1642
|
+
// Reuse the run_id stored in the solidify state when possible.
|
|
1643
|
+
let runId = `run_${Date.now()}`;
|
|
1644
|
+
try {
|
|
1645
|
+
const st = readStateForSolidify();
|
|
1646
|
+
if (st && st.last_run && st.last_run.run_id) runId = String(st.last_run.run_id);
|
|
1647
|
+
} catch (e) {}
|
|
1648
|
+
let artifact = null;
|
|
1649
|
+
try {
|
|
1650
|
+
artifact = writePromptArtifact({
|
|
1651
|
+
memoryDir: getEvolutionDir(),
|
|
1652
|
+
cycleId,
|
|
1653
|
+
runId,
|
|
1654
|
+
prompt,
|
|
1655
|
+
meta: {
|
|
1656
|
+
agent: AGENT_NAME,
|
|
1657
|
+
drift_enabled: IS_RANDOM_DRIFT,
|
|
1658
|
+
review_mode: IS_REVIEW_MODE,
|
|
1659
|
+
dry_run: IS_DRY_RUN,
|
|
1660
|
+
mutation_id: mutation && mutation.id ? mutation.id : null,
|
|
1661
|
+
personality_key: personalitySelection && personalitySelection.personality_key ? personalitySelection.personality_key : null,
|
|
1662
|
+
},
|
|
1663
|
+
});
|
|
1664
|
+
} catch (e) {
|
|
1665
|
+
artifact = null;
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
const executorTask = [
|
|
1669
|
+
'You are the executor (the Hand).',
|
|
1670
|
+
'Your job is to apply a safe, minimal patch in this repo following the attached GEP protocol prompt.',
|
|
1671
|
+
artifact && artifact.promptPath ? `Prompt file: ${artifact.promptPath}` : 'Prompt file: (unavailable)',
|
|
1672
|
+
'',
|
|
1673
|
+
'After applying changes and validations, you MUST run:',
|
|
1674
|
+
' node index.js solidify',
|
|
1675
|
+
'',
|
|
1676
|
+
'Loop chaining (only if you are running in loop mode): after solidify succeeds, print a sessions_spawn call to start the next loop run with a short delay.',
|
|
1677
|
+
'Example:',
|
|
1678
|
+
'sessions_spawn({ task: "exec: node skills/feishu-evolver-wrapper/lifecycle.js ensure", agentId: "main", cleanup: "delete", label: "gep_loop_next" })',
|
|
1679
|
+
'',
|
|
1680
|
+
'GEP protocol prompt (may be truncated here; prefer the prompt file if provided):',
|
|
1681
|
+
clip(prompt, 24000),
|
|
1682
|
+
].join('\n');
|
|
1683
|
+
|
|
1684
|
+
const spawn = renderSessionsSpawnCall({
|
|
1685
|
+
task: executorTask,
|
|
1686
|
+
agentId: AGENT_NAME,
|
|
1687
|
+
cleanup: 'delete',
|
|
1688
|
+
label: `gep_bridge_${cycleNum}`,
|
|
1689
|
+
});
|
|
1690
|
+
|
|
1691
|
+
console.log('\n[BRIDGE ENABLED] Spawning executor agent via sessions_spawn.');
|
|
1692
|
+
console.log(spawn);
|
|
1693
|
+
if (printPrompt) {
|
|
1694
|
+
console.log('\n[PROMPT OUTPUT] (EVOLVE_PRINT_PROMPT=true)');
|
|
1695
|
+
console.log(prompt);
|
|
1696
|
+
}
|
|
1697
|
+
} else {
|
|
1698
|
+
console.log(prompt);
|
|
1699
|
+
console.log('\n[SOLIDIFY REQUIRED] After applying the patch and validations, run: node index.js solidify');
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
module.exports = { run };
|
|
1704
|
+
|