0878535cb8
主要变更: - Capability Evolver 技能大幅升级 - 新增 LLM 审查、叙事记忆、反思机制模块 - 改进 Agent 间通信协议和技能搜索 - 整合 Git 备份到 evolver-run.sh - Agent 配置与日志更新 - dev-assist/haonan/main 认证和会话配置 - 5 个 agent 的 AGENTS.md 文档更新 - haonan 的 MEMORY.md 和 SOUL.md 更新 - 新增功能目录 - hooks/self-improvement 自我改进钩子 - memory/evolution 进化记忆状态 - workspace/.learnings/ 学习记录 - 清理 - 删除 scripts/git-auto-sync.sh(已整合到 evolver) - 删除旧的 _meta.json 配置 - 忽略临时工作空间目录
1632 lines
62 KiB
JavaScript
1632 lines
62 KiB
JavaScript
const fs = require('fs');
|
|
const path = require('path');
|
|
const os = require('os');
|
|
const { execSync } = require('child_process');
|
|
const { getRepoRoot, getMemoryDir, getSessionScope } = require('./gep/paths');
|
|
const { extractSignals } = require('./gep/signals');
|
|
const {
|
|
loadGenes,
|
|
loadCapsules,
|
|
readAllEvents,
|
|
getLastEventId,
|
|
appendCandidateJsonl,
|
|
readRecentCandidates,
|
|
readRecentExternalCandidates,
|
|
readRecentFailedCapsules,
|
|
ensureAssetFiles,
|
|
} = require('./gep/assetStore');
|
|
const { selectGeneAndCapsule, matchPatternToSignals } = require('./gep/selector');
|
|
const { buildGepPrompt, buildReusePrompt, buildHubMatchedBlock } = require('./gep/prompt');
|
|
const { hubSearch } = require('./gep/hubSearch');
|
|
const { logAssetCall } = require('./gep/assetCallLog');
|
|
const { extractCapabilityCandidates, renderCandidatesPreview } = require('./gep/candidates');
|
|
const memoryAdapter = require('./gep/memoryGraphAdapter');
|
|
const {
|
|
getAdvice: getMemoryAdvice,
|
|
recordSignalSnapshot,
|
|
recordHypothesis,
|
|
recordAttempt,
|
|
recordOutcome: recordOutcomeFromState,
|
|
memoryGraphPath,
|
|
} = memoryAdapter;
|
|
const { readStateForSolidify, writeStateForSolidify } = require('./gep/solidify');
|
|
const { fetchTasks, selectBestTask, claimTask, taskToSignals } = require('./gep/taskReceiver');
|
|
const { generateQuestions } = require('./gep/questionGenerator');
|
|
const { buildMutation, isHighRiskMutationAllowed } = require('./gep/mutation');
|
|
const { selectPersonalityForRun } = require('./gep/personality');
|
|
const { clip, writePromptArtifact, renderSessionsSpawnCall } = require('./gep/bridge');
|
|
const { getEvolutionDir } = require('./gep/paths');
|
|
const { shouldReflect, buildReflectionContext, recordReflection } = require('./gep/reflection');
|
|
const { loadNarrativeSummary } = require('./gep/narrativeMemory');
|
|
|
|
const REPO_ROOT = getRepoRoot();
|
|
|
|
// Load environment variables from repo root
|
|
try {
|
|
require('dotenv').config({ path: path.join(REPO_ROOT, '.env'), quiet: true });
|
|
} catch (e) {
|
|
// dotenv might not be installed or .env missing, proceed gracefully
|
|
}
|
|
|
|
// Configuration from CLI flags or Env
|
|
const ARGS = process.argv.slice(2);
|
|
const IS_REVIEW_MODE = ARGS.includes('--review');
|
|
const IS_DRY_RUN = ARGS.includes('--dry-run');
|
|
const IS_RANDOM_DRIFT = ARGS.includes('--drift') || String(process.env.RANDOM_DRIFT || '').toLowerCase() === 'true';
|
|
|
|
// Default Configuration
|
|
const MEMORY_DIR = getMemoryDir();
|
|
const AGENT_NAME = process.env.AGENT_NAME || 'main';
|
|
const AGENT_SESSIONS_DIR = path.join(os.homedir(), `.openclaw/agents/${AGENT_NAME}/sessions`);
|
|
const TODAY_LOG = path.join(MEMORY_DIR, new Date().toISOString().split('T')[0] + '.md');
|
|
|
|
// Ensure memory directory exists so state/cache writes work.
|
|
try {
|
|
if (!fs.existsSync(MEMORY_DIR)) fs.mkdirSync(MEMORY_DIR, { recursive: true });
|
|
} catch (e) {}
|
|
|
|
function formatSessionLog(jsonlContent) {
|
|
const result = [];
|
|
const lines = jsonlContent.split('\n');
|
|
let lastLine = '';
|
|
let repeatCount = 0;
|
|
|
|
const flushRepeats = () => {
|
|
if (repeatCount > 0) {
|
|
result.push(` ... [Repeated ${repeatCount} times] ...`);
|
|
repeatCount = 0;
|
|
}
|
|
};
|
|
|
|
for (const line of lines) {
|
|
if (!line.trim()) continue;
|
|
try {
|
|
const data = JSON.parse(line);
|
|
let entry = '';
|
|
|
|
if (data.type === 'message' && data.message) {
|
|
const role = (data.message.role || 'unknown').toUpperCase();
|
|
let content = '';
|
|
if (Array.isArray(data.message.content)) {
|
|
content = data.message.content
|
|
.map(c => {
|
|
if (c.type === 'text') return c.text;
|
|
if (c.type === 'toolCall') return `[TOOL: ${c.name}]`;
|
|
return '';
|
|
})
|
|
.join(' ');
|
|
} else if (typeof data.message.content === 'string') {
|
|
content = data.message.content;
|
|
} else {
|
|
content = JSON.stringify(data.message.content);
|
|
}
|
|
|
|
// Capture LLM errors from errorMessage field (e.g. "Unsupported MIME type: image/gif")
|
|
if (data.message.errorMessage) {
|
|
const errMsg = typeof data.message.errorMessage === 'string'
|
|
? data.message.errorMessage
|
|
: JSON.stringify(data.message.errorMessage);
|
|
content = `[LLM ERROR] ${errMsg.replace(/\n+/g, ' ').slice(0, 300)}`;
|
|
}
|
|
|
|
// Filter: Skip Heartbeats to save noise
|
|
if (content.trim() === 'HEARTBEAT_OK') continue;
|
|
if (content.includes('NO_REPLY') && !data.message.errorMessage) continue;
|
|
|
|
// Clean up newlines for compact reading
|
|
content = content.replace(/\n+/g, ' ').slice(0, 300);
|
|
entry = `**${role}**: ${content}`;
|
|
} else if (data.type === 'tool_result' || (data.message && data.message.role === 'toolResult')) {
|
|
// Filter: Skip generic success results or short uninformative ones
|
|
// Only show error or significant output
|
|
let resContent = '';
|
|
|
|
// Robust extraction: Handle structured tool results (e.g. sessions_spawn) that lack 'output'
|
|
if (data.tool_result) {
|
|
if (data.tool_result.output) {
|
|
resContent = data.tool_result.output;
|
|
} else {
|
|
resContent = JSON.stringify(data.tool_result);
|
|
}
|
|
}
|
|
|
|
if (data.content) resContent = typeof data.content === 'string' ? data.content : JSON.stringify(data.content);
|
|
|
|
if (resContent.length < 50 && (resContent.includes('success') || resContent.includes('done'))) continue;
|
|
if (resContent.trim() === '' || resContent === '{}') continue;
|
|
|
|
// Improvement: Show snippet of result (especially errors) instead of hiding it
|
|
const preview = resContent.replace(/\n+/g, ' ').slice(0, 200);
|
|
entry = `[TOOL RESULT] ${preview}${resContent.length > 200 ? '...' : ''}`;
|
|
}
|
|
|
|
if (entry) {
|
|
if (entry === lastLine) {
|
|
repeatCount++;
|
|
} else {
|
|
flushRepeats();
|
|
result.push(entry);
|
|
lastLine = entry;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
continue;
|
|
}
|
|
}
|
|
flushRepeats();
|
|
return result.join('\n');
|
|
}
|
|
|
|
function readRealSessionLog() {
|
|
try {
|
|
if (!fs.existsSync(AGENT_SESSIONS_DIR)) return '[NO SESSION LOGS FOUND]';
|
|
|
|
const now = Date.now();
|
|
const ACTIVE_WINDOW_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
const TARGET_BYTES = 120000;
|
|
const PER_SESSION_BYTES = 20000; // Read tail of each active session
|
|
|
|
// Session scope isolation: when EVOLVER_SESSION_SCOPE is set,
|
|
// only read sessions whose filenames contain the scope identifier.
|
|
// This prevents cross-channel/cross-project memory contamination.
|
|
const sessionScope = getSessionScope();
|
|
|
|
// Find ALL active sessions (modified in last 24h), sorted newest first
|
|
let files = fs
|
|
.readdirSync(AGENT_SESSIONS_DIR)
|
|
.filter(f => f.endsWith('.jsonl') && !f.includes('.lock'))
|
|
.map(f => {
|
|
try {
|
|
const st = fs.statSync(path.join(AGENT_SESSIONS_DIR, f));
|
|
return { name: f, time: st.mtime.getTime(), size: st.size };
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
})
|
|
.filter(f => f && (now - f.time) < ACTIVE_WINDOW_MS)
|
|
.sort((a, b) => b.time - a.time); // Newest first
|
|
|
|
if (files.length === 0) return '[NO JSONL FILES]';
|
|
|
|
// Skip evolver's own sessions to avoid self-reference loops
|
|
let nonEvolverFiles = files.filter(f => !f.name.startsWith('evolver_hand_'));
|
|
|
|
// Session scope filter: when scope is active, only include sessions
|
|
// whose filename contains the scope string (e.g., channel_123456.jsonl).
|
|
// If no sessions match the scope, fall back to all non-evolver sessions
|
|
// (graceful degradation -- better to evolve with global context than not at all).
|
|
if (sessionScope && nonEvolverFiles.length > 0) {
|
|
const scopeLower = sessionScope.toLowerCase();
|
|
const scopedFiles = nonEvolverFiles.filter(f => f.name.toLowerCase().includes(scopeLower));
|
|
if (scopedFiles.length > 0) {
|
|
nonEvolverFiles = scopedFiles;
|
|
console.log(`[SessionScope] Filtered to ${scopedFiles.length} session(s) matching scope "${sessionScope}".`);
|
|
} else {
|
|
console.log(`[SessionScope] No sessions match scope "${sessionScope}". Using all ${nonEvolverFiles.length} session(s) (fallback).`);
|
|
}
|
|
}
|
|
|
|
const activeFiles = nonEvolverFiles.length > 0 ? nonEvolverFiles : files.slice(0, 1);
|
|
|
|
// Read from multiple active sessions (up to 6) to get a full picture
|
|
const maxSessions = Math.min(activeFiles.length, 6);
|
|
const sections = [];
|
|
let totalBytes = 0;
|
|
|
|
for (let i = 0; i < maxSessions && totalBytes < TARGET_BYTES; i++) {
|
|
const f = activeFiles[i];
|
|
const bytesLeft = TARGET_BYTES - totalBytes;
|
|
const readSize = Math.min(PER_SESSION_BYTES, bytesLeft);
|
|
const raw = readRecentLog(path.join(AGENT_SESSIONS_DIR, f.name), readSize);
|
|
const formatted = formatSessionLog(raw);
|
|
if (formatted.trim()) {
|
|
sections.push(`--- SESSION (${f.name}) ---\n${formatted}`);
|
|
totalBytes += formatted.length;
|
|
}
|
|
}
|
|
|
|
let content = sections.join('\n\n');
|
|
|
|
return content;
|
|
} catch (e) {
|
|
return `[ERROR READING SESSION LOGS: ${e.message}]`;
|
|
}
|
|
}
|
|
|
|
function readRecentLog(filePath, size = 10000) {
|
|
try {
|
|
if (!fs.existsSync(filePath)) return `[MISSING] ${filePath}`;
|
|
const stats = fs.statSync(filePath);
|
|
const start = Math.max(0, stats.size - size);
|
|
const buffer = Buffer.alloc(stats.size - start);
|
|
const fd = fs.openSync(filePath, 'r');
|
|
fs.readSync(fd, buffer, 0, buffer.length, start);
|
|
fs.closeSync(fd);
|
|
return buffer.toString('utf8');
|
|
} catch (e) {
|
|
return `[ERROR READING ${filePath}: ${e.message}]`;
|
|
}
|
|
}
|
|
|
|
function checkSystemHealth() {
|
|
const report = [];
|
|
try {
|
|
// Uptime & Node Version
|
|
const uptime = (os.uptime() / 3600).toFixed(1);
|
|
report.push(`Uptime: ${uptime}h`);
|
|
report.push(`Node: ${process.version}`);
|
|
|
|
// Memory Usage (RSS)
|
|
const mem = process.memoryUsage();
|
|
const rssMb = (mem.rss / 1024 / 1024).toFixed(1);
|
|
report.push(`Agent RSS: ${rssMb}MB`);
|
|
|
|
// Optimization: Use native Node.js fs.statfsSync instead of spawning 'df'
|
|
if (fs.statfsSync) {
|
|
const stats = fs.statfsSync('/');
|
|
const total = stats.blocks * stats.bsize;
|
|
const free = stats.bfree * stats.bsize;
|
|
const used = total - free;
|
|
const freeGb = (free / 1024 / 1024 / 1024).toFixed(1);
|
|
const usedPercent = Math.round((used / total) * 100);
|
|
report.push(`Disk: ${usedPercent}% (${freeGb}G free)`);
|
|
}
|
|
} catch (e) {}
|
|
|
|
try {
|
|
if (process.platform === 'win32') {
|
|
const wmic = execSync('tasklist /FI "IMAGENAME eq node.exe" /NH', {
|
|
encoding: 'utf8',
|
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
timeout: 3000,
|
|
windowsHide: true,
|
|
});
|
|
const count = wmic.split('\n').filter(l => l.trim() && !l.includes('INFO:')).length;
|
|
report.push(`Node Processes: ${count}`);
|
|
} else {
|
|
try {
|
|
const pgrep = execSync('pgrep -c node', {
|
|
encoding: 'utf8',
|
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
timeout: 2000,
|
|
});
|
|
report.push(`Node Processes: ${pgrep.trim()}`);
|
|
} catch (e) {
|
|
const ps = execSync('ps aux | grep node | grep -v grep | wc -l', {
|
|
encoding: 'utf8',
|
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
timeout: 2000,
|
|
});
|
|
report.push(`Node Processes: ${ps.trim()}`);
|
|
}
|
|
}
|
|
} catch (e) {}
|
|
|
|
// Integration Health Checks (Env Vars)
|
|
try {
|
|
const issues = [];
|
|
|
|
// Generic Integration Status Check (Decoupled)
|
|
if (process.env.INTEGRATION_STATUS_CMD) {
|
|
try {
|
|
const status = execSync(process.env.INTEGRATION_STATUS_CMD, {
|
|
encoding: 'utf8',
|
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
timeout: 2000,
|
|
windowsHide: true,
|
|
});
|
|
if (status.trim()) issues.push(status.trim());
|
|
} catch (e) {}
|
|
}
|
|
|
|
if (issues.length > 0) {
|
|
report.push(`Integrations: ${issues.join(', ')}`);
|
|
} else {
|
|
report.push('Integrations: Nominal');
|
|
}
|
|
} catch (e) {}
|
|
|
|
return report.length ? report.join(' | ') : 'Health Check Unavailable';
|
|
}
|
|
|
|
function getMutationDirective(logContent) {
|
|
// Signal hints derived from recent logs.
|
|
const errorMatches = logContent.match(/\[ERROR|Error:|Exception:|FAIL|Failed|"isError":true/gi) || [];
|
|
const errorCount = errorMatches.length;
|
|
const isUnstable = errorCount > 2;
|
|
const recommendedIntent = isUnstable ? 'repair' : 'optimize';
|
|
|
|
return `
|
|
[Signal Hints]
|
|
- recent_error_count: ${errorCount}
|
|
- stability: ${isUnstable ? 'unstable' : 'stable'}
|
|
- recommended_intent: ${recommendedIntent}
|
|
`;
|
|
}
|
|
|
|
const STATE_FILE = path.join(getEvolutionDir(), 'evolution_state.json');
|
|
const DORMANT_HYPOTHESIS_FILE = path.join(getEvolutionDir(), 'dormant_hypothesis.json');
|
|
var DORMANT_TTL_MS = 3600 * 1000;
|
|
|
|
function writeDormantHypothesis(data) {
|
|
try {
|
|
var dir = getEvolutionDir();
|
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
var obj = Object.assign({}, data, { created_at: new Date().toISOString(), ttl_ms: DORMANT_TTL_MS });
|
|
var tmp = DORMANT_HYPOTHESIS_FILE + '.tmp';
|
|
fs.writeFileSync(tmp, JSON.stringify(obj, null, 2) + '\n', 'utf8');
|
|
fs.renameSync(tmp, DORMANT_HYPOTHESIS_FILE);
|
|
console.log('[DormantHypothesis] Saved partial state before backoff: ' + (data.backoff_reason || 'unknown'));
|
|
} catch (e) {
|
|
console.log('[DormantHypothesis] Write failed (non-fatal): ' + (e && e.message ? e.message : e));
|
|
}
|
|
}
|
|
|
|
function readDormantHypothesis() {
|
|
try {
|
|
if (!fs.existsSync(DORMANT_HYPOTHESIS_FILE)) return null;
|
|
var raw = fs.readFileSync(DORMANT_HYPOTHESIS_FILE, 'utf8');
|
|
if (!raw.trim()) return null;
|
|
var obj = JSON.parse(raw);
|
|
var createdAt = obj.created_at ? new Date(obj.created_at).getTime() : 0;
|
|
var ttl = Number.isFinite(Number(obj.ttl_ms)) ? Number(obj.ttl_ms) : DORMANT_TTL_MS;
|
|
if (Date.now() - createdAt > ttl) {
|
|
clearDormantHypothesis();
|
|
console.log('[DormantHypothesis] Expired (age: ' + Math.round((Date.now() - createdAt) / 1000) + 's). Discarded.');
|
|
return null;
|
|
}
|
|
return obj;
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function clearDormantHypothesis() {
|
|
try {
|
|
if (fs.existsSync(DORMANT_HYPOTHESIS_FILE)) fs.unlinkSync(DORMANT_HYPOTHESIS_FILE);
|
|
} catch (e) {}
|
|
}
|
|
// Read MEMORY.md and USER.md from the WORKSPACE root (not the evolver plugin dir).
|
|
// This avoids symlink breakage if the target file is temporarily deleted.
|
|
const WORKSPACE_ROOT = process.env.OPENCLAW_WORKSPACE || path.resolve(REPO_ROOT, '../..');
|
|
const ROOT_MEMORY = path.join(WORKSPACE_ROOT, 'MEMORY.md');
|
|
const DIR_MEMORY = path.join(MEMORY_DIR, 'MEMORY.md');
|
|
const MEMORY_FILE = fs.existsSync(ROOT_MEMORY) ? ROOT_MEMORY : (fs.existsSync(DIR_MEMORY) ? DIR_MEMORY : ROOT_MEMORY);
|
|
const USER_FILE = path.join(WORKSPACE_ROOT, 'USER.md');
|
|
|
|
function readMemorySnippet() {
|
|
try {
|
|
// Session scope isolation: when a scope is active, prefer scoped MEMORY.md
|
|
// at memory/scopes/<scope>/MEMORY.md. Falls back to global MEMORY.md if
|
|
// scoped file doesn't exist (common: scoped MEMORY.md created on first evolution).
|
|
const scope = getSessionScope();
|
|
let memFile = MEMORY_FILE;
|
|
if (scope) {
|
|
const scopedMemory = path.join(MEMORY_DIR, 'scopes', scope, 'MEMORY.md');
|
|
if (fs.existsSync(scopedMemory)) {
|
|
memFile = scopedMemory;
|
|
console.log(`[SessionScope] Reading scoped MEMORY.md for "${scope}".`);
|
|
} else {
|
|
// First run with scope: global MEMORY.md will be used, but note it.
|
|
console.log(`[SessionScope] No scoped MEMORY.md for "${scope}". Using global MEMORY.md.`);
|
|
}
|
|
}
|
|
if (!fs.existsSync(memFile)) return '[MEMORY.md MISSING]';
|
|
const content = fs.readFileSync(memFile, 'utf8');
|
|
// Optimization: Increased limit from 2000 to 50000 for modern context windows
|
|
return content.length > 50000
|
|
? content.slice(0, 50000) + `\n... [TRUNCATED: ${content.length - 50000} chars remaining]`
|
|
: content;
|
|
} catch (e) {
|
|
return '[ERROR READING MEMORY.md]';
|
|
}
|
|
}
|
|
|
|
function readUserSnippet() {
|
|
try {
|
|
if (!fs.existsSync(USER_FILE)) return '[USER.md MISSING]';
|
|
return fs.readFileSync(USER_FILE, 'utf8');
|
|
} catch (e) {
|
|
return '[ERROR READING USER.md]';
|
|
}
|
|
}
|
|
|
|
function getNextCycleId() {
|
|
let state = { cycleCount: 0, lastRun: 0 };
|
|
try {
|
|
if (fs.existsSync(STATE_FILE)) {
|
|
state = JSON.parse(fs.readFileSync(STATE_FILE, 'utf8'));
|
|
}
|
|
} catch (e) {}
|
|
|
|
state.cycleCount = (state.cycleCount || 0) + 1;
|
|
state.lastRun = Date.now();
|
|
|
|
try {
|
|
fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
|
|
} catch (e) {}
|
|
|
|
return String(state.cycleCount).padStart(4, '0');
|
|
}
|
|
|
|
function performMaintenance() {
|
|
// Auto-update check (rate-limited, non-fatal).
|
|
checkAndAutoUpdate();
|
|
|
|
try {
|
|
if (!fs.existsSync(AGENT_SESSIONS_DIR)) return;
|
|
|
|
const files = fs.readdirSync(AGENT_SESSIONS_DIR).filter(f => f.endsWith('.jsonl'));
|
|
|
|
// Clean up evolver's own hand sessions immediately.
|
|
// These are single-use executor sessions that must not accumulate,
|
|
// otherwise they pollute the agent's context and starve user conversations.
|
|
const evolverFiles = files.filter(f => f.startsWith('evolver_hand_'));
|
|
for (const f of evolverFiles) {
|
|
try {
|
|
fs.unlinkSync(path.join(AGENT_SESSIONS_DIR, f));
|
|
} catch (_) {}
|
|
}
|
|
if (evolverFiles.length > 0) {
|
|
console.log(`[Maintenance] Cleaned ${evolverFiles.length} evolver hand session(s).`);
|
|
}
|
|
|
|
// Archive old non-evolver sessions when count exceeds threshold.
|
|
const remaining = files.length - evolverFiles.length;
|
|
if (remaining < 100) return;
|
|
|
|
console.log(`[Maintenance] Found ${remaining} session logs. Archiving old ones...`);
|
|
|
|
const ARCHIVE_DIR = path.join(AGENT_SESSIONS_DIR, 'archive');
|
|
if (!fs.existsSync(ARCHIVE_DIR)) fs.mkdirSync(ARCHIVE_DIR, { recursive: true });
|
|
|
|
const fileStats = files
|
|
.filter(f => !f.startsWith('evolver_hand_'))
|
|
.map(f => {
|
|
try {
|
|
return { name: f, time: fs.statSync(path.join(AGENT_SESSIONS_DIR, f)).mtime.getTime() };
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
})
|
|
.filter(Boolean)
|
|
.sort((a, b) => a.time - b.time);
|
|
|
|
const toArchive = fileStats.slice(0, fileStats.length - 50);
|
|
|
|
for (const file of toArchive) {
|
|
const oldPath = path.join(AGENT_SESSIONS_DIR, file.name);
|
|
const newPath = path.join(ARCHIVE_DIR, file.name);
|
|
fs.renameSync(oldPath, newPath);
|
|
}
|
|
if (toArchive.length > 0) {
|
|
console.log(`[Maintenance] Archived ${toArchive.length} logs to ${ARCHIVE_DIR}`);
|
|
}
|
|
} catch (e) {
|
|
console.error(`[Maintenance] Error: ${e.message}`);
|
|
}
|
|
}
|
|
|
|
// --- Auto-update: check for newer versions of evolver and wrapper on ClawHub ---
|
|
function checkAndAutoUpdate() {
|
|
try {
|
|
// Read config: default autoUpdate = true
|
|
const configPath = path.join(os.homedir(), '.openclaw', 'openclaw.json');
|
|
let autoUpdate = true;
|
|
let intervalHours = 6;
|
|
try {
|
|
if (fs.existsSync(configPath)) {
|
|
const cfg = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
if (cfg.evolver && cfg.evolver.autoUpdate === false) autoUpdate = false;
|
|
if (cfg.evolver && Number.isFinite(Number(cfg.evolver.autoUpdateIntervalHours))) {
|
|
intervalHours = Number(cfg.evolver.autoUpdateIntervalHours);
|
|
}
|
|
}
|
|
} catch (_) {}
|
|
|
|
if (!autoUpdate) return;
|
|
|
|
// Rate limit: only check once per interval
|
|
const stateFile = path.join(MEMORY_DIR, 'evolver_update_check.json');
|
|
const now = Date.now();
|
|
const intervalMs = intervalHours * 60 * 60 * 1000;
|
|
try {
|
|
if (fs.existsSync(stateFile)) {
|
|
const state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
|
|
if (state.lastCheckedAt && (now - new Date(state.lastCheckedAt).getTime()) < intervalMs) {
|
|
return; // Too soon, skip
|
|
}
|
|
}
|
|
} catch (_) {}
|
|
|
|
let clawhubBin = null;
|
|
const whichCmd = process.platform === 'win32' ? 'where clawhub' : 'which clawhub';
|
|
const candidates = ['clawhub', path.join(os.homedir(), '.npm-global/bin/clawhub'), '/usr/local/bin/clawhub'];
|
|
for (const c of candidates) {
|
|
try {
|
|
if (c === 'clawhub') {
|
|
execSync(whichCmd, { stdio: 'ignore', timeout: 3000, windowsHide: true });
|
|
clawhubBin = 'clawhub';
|
|
break;
|
|
}
|
|
if (fs.existsSync(c)) { clawhubBin = c; break; }
|
|
} catch (_) {}
|
|
}
|
|
if (!clawhubBin) return; // No clawhub CLI available
|
|
|
|
// Update evolver and feishu-evolver-wrapper
|
|
const slugs = ['evolver', 'feishu-evolver-wrapper'];
|
|
let updated = false;
|
|
for (const slug of slugs) {
|
|
try {
|
|
const out = execSync(`${clawhubBin} update ${slug} --force`, {
|
|
encoding: 'utf8',
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
timeout: 30000,
|
|
cwd: path.resolve(REPO_ROOT, '..'),
|
|
windowsHide: true,
|
|
});
|
|
if (out && !out.includes('already up to date') && !out.includes('not installed')) {
|
|
console.log(`[AutoUpdate] ${slug}: ${out.trim().split('\n').pop()}`);
|
|
updated = true;
|
|
}
|
|
} catch (e) {
|
|
// Non-fatal: update failure should never block evolution
|
|
}
|
|
}
|
|
|
|
// Write state
|
|
try {
|
|
const stateData = {
|
|
lastCheckedAt: new Date(now).toISOString(),
|
|
updated,
|
|
};
|
|
fs.writeFileSync(stateFile, JSON.stringify(stateData, null, 2) + '\n');
|
|
} catch (_) {}
|
|
|
|
if (updated) {
|
|
console.log('[AutoUpdate] Skills updated. Changes will take effect on next wrapper restart.');
|
|
}
|
|
} catch (e) {
|
|
// Entire auto-update is non-fatal
|
|
console.log(`[AutoUpdate] Check failed (non-fatal): ${e.message}`);
|
|
}
|
|
}
|
|
|
|
function sleepMs(ms) {
|
|
const t = Number(ms);
|
|
const n = Number.isFinite(t) ? Math.max(0, t) : 0;
|
|
return new Promise(resolve => setTimeout(resolve, n));
|
|
}
|
|
|
|
// Check system load average via os.loadavg().
|
|
// Returns { load1m, load5m, load15m }. Used for load-aware throttling.
|
|
function getSystemLoad() {
|
|
try {
|
|
const loadavg = os.loadavg();
|
|
return { load1m: loadavg[0], load5m: loadavg[1], load15m: loadavg[2] };
|
|
} catch (e) {
|
|
return { load1m: 0, load5m: 0, load15m: 0 };
|
|
}
|
|
}
|
|
|
|
// Calculate intelligent default load threshold based on CPU cores
|
|
// Rule of thumb:
|
|
// - Single-core: 0.8-1.0 (use 0.9)
|
|
// - Multi-core: cores x 0.8-1.0 (use 0.9)
|
|
// - Production: reserve 20% headroom for burst traffic
|
|
function getDefaultLoadMax() {
|
|
const cpuCount = os.cpus().length;
|
|
if (cpuCount === 1) {
|
|
return 0.9;
|
|
} else {
|
|
return cpuCount * 0.9;
|
|
}
|
|
}
|
|
|
|
// Check how many agent sessions are actively being processed (modified in the last N minutes).
|
|
// If the agent is busy with user conversations, evolver should back off.
|
|
function getRecentActiveSessionCount(windowMs) {
|
|
try {
|
|
if (!fs.existsSync(AGENT_SESSIONS_DIR)) return 0;
|
|
const now = Date.now();
|
|
const w = Number.isFinite(windowMs) ? windowMs : 10 * 60 * 1000;
|
|
return fs.readdirSync(AGENT_SESSIONS_DIR)
|
|
.filter(f => f.endsWith('.jsonl') && !f.includes('.lock') && !f.startsWith('evolver_hand_'))
|
|
.filter(f => {
|
|
try { return (now - fs.statSync(path.join(AGENT_SESSIONS_DIR, f)).mtimeMs) < w; } catch (_) { return false; }
|
|
}).length;
|
|
} catch (_) { return 0; }
|
|
}
|
|
|
|
async function run() {
|
|
const bridgeEnabled = String(process.env.EVOLVE_BRIDGE || '').toLowerCase() !== 'false';
|
|
const loopMode = ARGS.includes('--loop') || ARGS.includes('--mad-dog') || String(process.env.EVOLVE_LOOP || '').toLowerCase() === 'true';
|
|
|
|
// SAFEGUARD: If another evolver Hand Agent is already running, back off.
|
|
// Prevents race conditions when a wrapper restarts while the old Hand Agent
|
|
// is still executing. The Core yields instead of starting a competing cycle.
|
|
if (process.platform !== 'win32') {
|
|
try {
|
|
const _psRace = require('child_process').execSync(
|
|
'ps aux | grep "evolver_hand_" | grep "openclaw.*agent" | grep -v grep',
|
|
{ encoding: 'utf8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'] }
|
|
).trim();
|
|
if (_psRace && _psRace.length > 0) {
|
|
console.log('[Evolver] Another evolver Hand Agent is already running. Yielding this cycle.');
|
|
return;
|
|
}
|
|
} catch (_) {
|
|
// grep exit 1 = no match = no conflict, safe to proceed
|
|
}
|
|
}
|
|
|
|
// SAFEGUARD: If the agent has too many active user sessions, back off.
|
|
// Evolver must not starve user conversations by consuming model concurrency.
|
|
const QUEUE_MAX = Number.parseInt(process.env.EVOLVE_AGENT_QUEUE_MAX || '10', 10);
|
|
const QUEUE_BACKOFF_MS = Number.parseInt(process.env.EVOLVE_AGENT_QUEUE_BACKOFF_MS || '60000', 10);
|
|
const activeUserSessions = getRecentActiveSessionCount(10 * 60 * 1000);
|
|
if (activeUserSessions > QUEUE_MAX) {
|
|
console.log(`[Evolver] Agent has ${activeUserSessions} active user sessions (max ${QUEUE_MAX}). Backing off ${QUEUE_BACKOFF_MS}ms to avoid starving user conversations.`);
|
|
writeDormantHypothesis({
|
|
backoff_reason: 'active_sessions_exceeded',
|
|
active_sessions: activeUserSessions,
|
|
queue_max: QUEUE_MAX,
|
|
});
|
|
await sleepMs(QUEUE_BACKOFF_MS);
|
|
return;
|
|
}
|
|
|
|
// SAFEGUARD: System load awareness.
|
|
// When system load is too high (e.g. too many concurrent processes, heavy I/O),
|
|
// back off to prevent the evolver from contributing to load spikes.
|
|
// Echo-MingXuan's Cycle #55 saw load spike from 0.02-0.50 to 1.30 before crash.
|
|
const LOAD_MAX = parseFloat(process.env.EVOLVE_LOAD_MAX || String(getDefaultLoadMax()));
|
|
const sysLoad = getSystemLoad();
|
|
if (sysLoad.load1m > LOAD_MAX) {
|
|
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.`);
|
|
writeDormantHypothesis({
|
|
backoff_reason: 'system_load_exceeded',
|
|
system_load: { load1m: sysLoad.load1m, load5m: sysLoad.load5m, load15m: sysLoad.load15m },
|
|
load_max: LOAD_MAX,
|
|
cpu_cores: os.cpus().length,
|
|
});
|
|
await sleepMs(QUEUE_BACKOFF_MS);
|
|
return;
|
|
}
|
|
|
|
// Loop gating: do not start a new cycle until the previous one is solidified.
|
|
// This prevents wrappers from "fast-cycling" the Brain without waiting for the Hand to finish.
|
|
if (bridgeEnabled && loopMode) {
|
|
try {
|
|
const st = readStateForSolidify();
|
|
const lastRun = st && st.last_run ? st.last_run : null;
|
|
const lastSolid = st && st.last_solidify ? st.last_solidify : null;
|
|
if (lastRun && lastRun.run_id) {
|
|
const pending = !lastSolid || !lastSolid.run_id || String(lastSolid.run_id) !== String(lastRun.run_id);
|
|
if (pending) {
|
|
writeDormantHypothesis({
|
|
backoff_reason: 'loop_gating_pending_solidify',
|
|
signals: lastRun && Array.isArray(lastRun.signals) ? lastRun.signals : [],
|
|
selected_gene_id: lastRun && lastRun.selected_gene_id ? lastRun.selected_gene_id : null,
|
|
mutation: lastRun && lastRun.mutation ? lastRun.mutation : null,
|
|
personality_state: lastRun && lastRun.personality_state ? lastRun.personality_state : null,
|
|
run_id: lastRun.run_id,
|
|
});
|
|
const raw = process.env.EVOLVE_PENDING_SLEEP_MS || process.env.EVOLVE_MIN_INTERVAL || '120000';
|
|
const n = parseInt(String(raw), 10);
|
|
const waitMs = Number.isFinite(n) ? Math.max(0, n) : 120000;
|
|
await sleepMs(waitMs);
|
|
return;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
// If we cannot read state, proceed (fail open) to avoid deadlock.
|
|
}
|
|
}
|
|
|
|
// Reset per-cycle env flags to prevent state leaking between cycles.
|
|
// In --loop mode, process.env persists across cycles. The circuit breaker
|
|
// below will re-set FORCE_INNOVATION if the condition still holds.
|
|
// CWD Recovery: If the working directory was deleted during a previous cycle
|
|
// (e.g., by git reset/restore or directory removal), process.cwd() throws
|
|
// ENOENT and ALL subsequent operations fail. Recover by chdir to REPO_ROOT.
|
|
try {
|
|
process.cwd();
|
|
} catch (e) {
|
|
if (e && e.code === 'ENOENT') {
|
|
console.warn('[Evolver] CWD lost (ENOENT). Recovering to REPO_ROOT: ' + REPO_ROOT);
|
|
try { process.chdir(REPO_ROOT); } catch (e2) {
|
|
console.error('[Evolver] CWD recovery failed: ' + (e2 && e2.message ? e2.message : e2));
|
|
throw e;
|
|
}
|
|
} else {
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
delete process.env.FORCE_INNOVATION;
|
|
|
|
// SAFEGUARD: Git repository check.
|
|
// Solidify, rollback, and blast radius all depend on git. Without a git repo
|
|
// these operations silently produce empty results, leading to data loss.
|
|
try {
|
|
execSync('git rev-parse --git-dir', { cwd: REPO_ROOT, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 5000 });
|
|
} catch (_) {
|
|
console.error('[Evolver] FATAL: Not a git repository (' + REPO_ROOT + ').');
|
|
console.error('[Evolver] Evolver requires git for rollback, blast radius calculation, and solidify.');
|
|
console.error('[Evolver] Run "git init && git add -A && git commit -m init" in your project root, then try again.');
|
|
process.exitCode = 1;
|
|
return;
|
|
}
|
|
|
|
var dormantHypothesis = readDormantHypothesis();
|
|
if (dormantHypothesis) {
|
|
console.log('[DormantHypothesis] Recovered partial state from previous backoff: ' + (dormantHypothesis.backoff_reason || 'unknown'));
|
|
clearDormantHypothesis();
|
|
}
|
|
|
|
const startTime = Date.now();
|
|
console.log('Scanning session logs...');
|
|
|
|
// Ensure all GEP asset files exist before any operation.
|
|
// This prevents "No such file or directory" errors when external tools
|
|
// (grep, cat, etc.) reference optional append-only files like genes.jsonl.
|
|
try { ensureAssetFiles(); } catch (e) {
|
|
console.error(`[AssetInit] ensureAssetFiles failed (non-fatal): ${e.message}`);
|
|
}
|
|
|
|
// Maintenance: Clean up old logs to keep directory scan fast
|
|
if (!IS_DRY_RUN) {
|
|
performMaintenance();
|
|
} else {
|
|
console.log('[Maintenance] Skipped (dry-run mode).');
|
|
}
|
|
|
|
// --- Repair Loop Circuit Breaker ---
|
|
// Detect when the evolver is stuck in a "repair -> fail -> repair" cycle.
|
|
// If the last N events are all failed repairs with the same gene, force
|
|
// innovation intent to break out of the loop instead of retrying the same fix.
|
|
const REPAIR_LOOP_THRESHOLD = 3;
|
|
try {
|
|
const allEvents = readAllEvents();
|
|
const recent = Array.isArray(allEvents) ? allEvents.slice(-REPAIR_LOOP_THRESHOLD) : [];
|
|
if (recent.length >= REPAIR_LOOP_THRESHOLD) {
|
|
const allRepairFailed = recent.every(e =>
|
|
e && e.intent === 'repair' &&
|
|
e.outcome && e.outcome.status === 'failed'
|
|
);
|
|
if (allRepairFailed) {
|
|
const geneIds = recent.map(e => (e.genes_used && e.genes_used[0]) || 'unknown');
|
|
const sameGene = geneIds.every(id => id === geneIds[0]);
|
|
console.warn(`[CircuitBreaker] Detected ${REPAIR_LOOP_THRESHOLD} consecutive failed repairs${sameGene ? ` (gene: ${geneIds[0]})` : ''}. Forcing innovation intent to break the loop.`);
|
|
// Set env flag that downstream code reads to force innovation
|
|
process.env.FORCE_INNOVATION = 'true';
|
|
}
|
|
}
|
|
} catch (e) {
|
|
// Non-fatal: if we can't read events, proceed normally
|
|
console.error(`[CircuitBreaker] Check failed (non-fatal): ${e.message}`);
|
|
}
|
|
|
|
const recentMasterLog = readRealSessionLog();
|
|
const todayLog = readRecentLog(TODAY_LOG);
|
|
const memorySnippet = readMemorySnippet();
|
|
const userSnippet = readUserSnippet();
|
|
|
|
const cycleNum = getNextCycleId();
|
|
const cycleId = `Cycle #${cycleNum}`;
|
|
|
|
// 2. Detect Workspace State & Local Overrides
|
|
// Logic: Default to generic reporting (message)
|
|
let fileList = '';
|
|
const skillsDir = path.join(REPO_ROOT, 'skills');
|
|
|
|
// Default Reporting: Use generic `message` tool or `process.env.EVOLVE_REPORT_CMD` if set.
|
|
// This removes the hardcoded dependency on 'feishu-card' from the core logic.
|
|
let reportingDirective = `Report requirement:
|
|
- Use \`message\` tool.
|
|
- Title: Evolution ${cycleId}
|
|
- Status: [SUCCESS]
|
|
- Changes: Detail exactly what was improved.`;
|
|
|
|
// Wrapper Injection Point: The wrapper can inject a custom reporting directive via ENV.
|
|
if (process.env.EVOLVE_REPORT_DIRECTIVE) {
|
|
reportingDirective = process.env.EVOLVE_REPORT_DIRECTIVE.replace('__CYCLE_ID__', cycleId);
|
|
} else if (process.env.EVOLVE_REPORT_CMD) {
|
|
reportingDirective = `Report requirement (custom):
|
|
- Execute the custom report command:
|
|
\`\`\`
|
|
${process.env.EVOLVE_REPORT_CMD.replace('__CYCLE_ID__', cycleId)}
|
|
\`\`\`
|
|
- Ensure you pass the status and action details.`;
|
|
}
|
|
|
|
// Handle Review Mode Flag (--review)
|
|
if (IS_REVIEW_MODE) {
|
|
reportingDirective +=
|
|
'\n - REVIEW PAUSE: After generating the fix but BEFORE applying significant edits, ask the user for confirmation.';
|
|
}
|
|
|
|
const SKILLS_CACHE_FILE = path.join(MEMORY_DIR, 'skills_list_cache.json');
|
|
|
|
try {
|
|
if (fs.existsSync(skillsDir)) {
|
|
// Check cache validity (mtime of skills folder vs cache file)
|
|
let useCache = false;
|
|
const dirStats = fs.statSync(skillsDir);
|
|
if (fs.existsSync(SKILLS_CACHE_FILE)) {
|
|
const cacheStats = fs.statSync(SKILLS_CACHE_FILE);
|
|
const CACHE_TTL = 1000 * 60 * 60 * 6; // 6 Hours
|
|
const isFresh = Date.now() - cacheStats.mtimeMs < CACHE_TTL;
|
|
|
|
// Use cache if it's fresh AND newer than the directory (structure change)
|
|
if (isFresh && cacheStats.mtimeMs > dirStats.mtimeMs) {
|
|
try {
|
|
const cached = JSON.parse(fs.readFileSync(SKILLS_CACHE_FILE, 'utf8'));
|
|
fileList = cached.list;
|
|
useCache = true;
|
|
} catch (e) {}
|
|
}
|
|
}
|
|
|
|
if (!useCache) {
|
|
const skills = fs
|
|
.readdirSync(skillsDir, { withFileTypes: true })
|
|
.filter(dirent => dirent.isDirectory())
|
|
.map(dirent => {
|
|
const name = dirent.name;
|
|
let desc = 'No description';
|
|
try {
|
|
const pkg = require(path.join(skillsDir, name, 'package.json'));
|
|
if (pkg.description) desc = pkg.description.slice(0, 100) + (pkg.description.length > 100 ? '...' : '');
|
|
} catch (e) {
|
|
try {
|
|
const skillMdPath = path.join(skillsDir, name, 'SKILL.md');
|
|
if (fs.existsSync(skillMdPath)) {
|
|
const skillMd = fs.readFileSync(skillMdPath, 'utf8');
|
|
// Strategy 1: YAML Frontmatter (description: ...)
|
|
const yamlMatch = skillMd.match(/^description:\s*(.*)$/m);
|
|
if (yamlMatch) {
|
|
desc = yamlMatch[1].trim();
|
|
} else {
|
|
// Strategy 2: First non-header, non-empty line
|
|
const lines = skillMd.split('\n');
|
|
for (const line of lines) {
|
|
const trimmed = line.trim();
|
|
if (
|
|
trimmed &&
|
|
!trimmed.startsWith('#') &&
|
|
!trimmed.startsWith('---') &&
|
|
!trimmed.startsWith('```')
|
|
) {
|
|
desc = trimmed;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (desc.length > 100) desc = desc.slice(0, 100) + '...';
|
|
}
|
|
} catch (e2) {}
|
|
}
|
|
return `- **${name}**: ${desc}`;
|
|
});
|
|
fileList = skills.join('\n');
|
|
|
|
// Write cache
|
|
try {
|
|
fs.writeFileSync(SKILLS_CACHE_FILE, JSON.stringify({ list: fileList }, null, 2));
|
|
} catch (e) {}
|
|
}
|
|
}
|
|
} catch (e) {
|
|
fileList = `Error listing skills: ${e.message}`;
|
|
}
|
|
|
|
const mutationDirective = getMutationDirective(recentMasterLog);
|
|
const healthReport = checkSystemHealth();
|
|
|
|
// Feature: Mood Awareness (Mode E - Personalization)
|
|
let moodStatus = 'Mood: Unknown';
|
|
try {
|
|
const moodFile = path.join(MEMORY_DIR, 'mood.json');
|
|
if (fs.existsSync(moodFile)) {
|
|
const moodData = JSON.parse(fs.readFileSync(moodFile, 'utf8'));
|
|
moodStatus = `Mood: ${moodData.current_mood || 'Neutral'} (Intensity: ${moodData.intensity || 0})`;
|
|
}
|
|
} catch (e) {}
|
|
|
|
const scanTime = Date.now() - startTime;
|
|
const memorySize = fs.existsSync(MEMORY_FILE) ? fs.statSync(MEMORY_FILE).size : 0;
|
|
|
|
let syncDirective = 'Workspace sync: optional/disabled in this environment.';
|
|
|
|
// Check for git-sync skill availability
|
|
const hasGitSync = fs.existsSync(path.join(skillsDir, 'git-sync'));
|
|
if (hasGitSync) {
|
|
syncDirective = 'Workspace sync: run skills/git-sync/sync.sh "Evolution: Workspace Sync"';
|
|
}
|
|
|
|
const genes = loadGenes();
|
|
const capsules = loadCapsules();
|
|
const recentEvents = (() => {
|
|
try {
|
|
const all = readAllEvents();
|
|
return Array.isArray(all) ? all.filter(e => e && e.type === 'EvolutionEvent').slice(-80) : [];
|
|
} catch (e) {
|
|
return [];
|
|
}
|
|
})();
|
|
const signals = extractSignals({
|
|
recentSessionTranscript: recentMasterLog,
|
|
todayLog,
|
|
memorySnippet,
|
|
userSnippet,
|
|
recentEvents,
|
|
});
|
|
|
|
if (dormantHypothesis && Array.isArray(dormantHypothesis.signals) && dormantHypothesis.signals.length > 0) {
|
|
var dormantSignals = dormantHypothesis.signals;
|
|
var injected = 0;
|
|
for (var dsi = 0; dsi < dormantSignals.length; dsi++) {
|
|
if (!signals.includes(dormantSignals[dsi])) {
|
|
signals.push(dormantSignals[dsi]);
|
|
injected++;
|
|
}
|
|
}
|
|
if (injected > 0) {
|
|
console.log('[DormantHypothesis] Injected ' + injected + ' signal(s) from previous interrupted cycle.');
|
|
}
|
|
}
|
|
|
|
// --- Hub Task Auto-Claim (with proactive questions) ---
|
|
// Generate questions from current context, piggyback them on the fetch call,
|
|
// then pick the best task and auto-claim it.
|
|
let activeTask = null;
|
|
let proactiveQuestions = [];
|
|
try {
|
|
proactiveQuestions = generateQuestions({
|
|
signals,
|
|
recentEvents,
|
|
sessionTranscript: recentMasterLog,
|
|
memorySnippet: memorySnippet,
|
|
});
|
|
if (proactiveQuestions.length > 0) {
|
|
console.log(`[QuestionGenerator] Generated ${proactiveQuestions.length} proactive question(s).`);
|
|
}
|
|
} catch (e) {
|
|
console.log(`[QuestionGenerator] Generation failed (non-fatal): ${e.message}`);
|
|
}
|
|
|
|
// LessonL: lessons received from Hub during fetch
|
|
let hubLessons = [];
|
|
|
|
try {
|
|
const fetchResult = await fetchTasks({ questions: proactiveQuestions });
|
|
const hubTasks = fetchResult.tasks || [];
|
|
|
|
if (fetchResult.questions_created && fetchResult.questions_created.length > 0) {
|
|
const created = fetchResult.questions_created.filter(function(q) { return !q.error; });
|
|
const failed = fetchResult.questions_created.filter(function(q) { return q.error; });
|
|
if (created.length > 0) {
|
|
console.log(`[QuestionGenerator] Hub accepted ${created.length} question(s) as bounties.`);
|
|
}
|
|
if (failed.length > 0) {
|
|
console.log(`[QuestionGenerator] Hub rejected ${failed.length} question(s): ${failed.map(function(q) { return q.error; }).join(', ')}`);
|
|
}
|
|
}
|
|
|
|
// LessonL: capture relevant lessons from Hub
|
|
if (Array.isArray(fetchResult.relevant_lessons) && fetchResult.relevant_lessons.length > 0) {
|
|
hubLessons = fetchResult.relevant_lessons;
|
|
console.log(`[LessonBank] Received ${hubLessons.length} lesson(s) from ecosystem.`);
|
|
}
|
|
|
|
if (hubTasks.length > 0) {
|
|
let taskMemoryEvents = [];
|
|
try {
|
|
const { tryReadMemoryGraphEvents } = require('./gep/memoryGraph');
|
|
taskMemoryEvents = tryReadMemoryGraphEvents(1000);
|
|
} catch {}
|
|
const best = selectBestTask(hubTasks, taskMemoryEvents);
|
|
if (best) {
|
|
const alreadyClaimed = best.status === 'claimed';
|
|
const claimed = alreadyClaimed || await claimTask(best.id || best.task_id);
|
|
if (claimed) {
|
|
activeTask = best;
|
|
const taskSignals = taskToSignals(best);
|
|
for (const sig of taskSignals) {
|
|
if (!signals.includes(sig)) signals.unshift(sig);
|
|
}
|
|
console.log(`[TaskReceiver] ${alreadyClaimed ? 'Resuming' : 'Claimed'} task: "${best.title || best.id}" (${taskSignals.length} signals injected)`);
|
|
}
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.log(`[TaskReceiver] Fetch/claim failed (non-fatal): ${e.message}`);
|
|
}
|
|
|
|
const recentErrorMatches = recentMasterLog.match(/\[ERROR|Error:|Exception:|FAIL|Failed|"isError":true/gi) || [];
|
|
const recentErrorCount = recentErrorMatches.length;
|
|
|
|
const evidence = {
|
|
// Keep short; do not store full transcripts in the graph.
|
|
recent_session_tail: String(recentMasterLog || '').slice(-6000),
|
|
today_log_tail: String(todayLog || '').slice(-2500),
|
|
};
|
|
|
|
const sessionScope = getSessionScope();
|
|
const observations = {
|
|
agent: AGENT_NAME,
|
|
session_scope: sessionScope || null,
|
|
drift_enabled: IS_RANDOM_DRIFT,
|
|
review_mode: IS_REVIEW_MODE,
|
|
dry_run: IS_DRY_RUN,
|
|
system_health: healthReport,
|
|
mood: moodStatus,
|
|
scan_ms: scanTime,
|
|
memory_size_bytes: memorySize,
|
|
recent_error_count: recentErrorCount,
|
|
node: process.version,
|
|
platform: process.platform,
|
|
cwd: process.cwd(),
|
|
evidence,
|
|
};
|
|
|
|
if (sessionScope) {
|
|
console.log(`[SessionScope] Active scope: "${sessionScope}". Evolution state and memory graph are isolated.`);
|
|
}
|
|
|
|
// Memory Graph: close last action with an inferred outcome (append-only graph, mutable state).
|
|
try {
|
|
recordOutcomeFromState({ signals, observations });
|
|
} catch (e) {
|
|
// If we can't read/write memory graph, refuse to evolve (no "memoryless evolution").
|
|
console.error(`[MemoryGraph] Outcome write failed: ${e.message}`);
|
|
console.error(`[MemoryGraph] Refusing to evolve without causal memory. Target: ${memoryGraphPath()}`);
|
|
throw new Error(`MemoryGraph Outcome write failed: ${e.message}`);
|
|
}
|
|
|
|
// Memory Graph: record current signals as a first-class node. If this fails, refuse to evolve.
|
|
try {
|
|
recordSignalSnapshot({ signals, observations });
|
|
} catch (e) {
|
|
console.error(`[MemoryGraph] Signal snapshot write failed: ${e.message}`);
|
|
console.error(`[MemoryGraph] Refusing to evolve without causal memory. Target: ${memoryGraphPath()}`);
|
|
throw new Error(`MemoryGraph Signal snapshot write failed: ${e.message}`);
|
|
}
|
|
|
|
// Capability candidates (structured, short): persist and preview.
|
|
const newCandidates = extractCapabilityCandidates({
|
|
recentSessionTranscript: recentMasterLog,
|
|
signals,
|
|
});
|
|
for (const c of newCandidates) {
|
|
try {
|
|
appendCandidateJsonl(c);
|
|
} catch (e) {}
|
|
}
|
|
const recentCandidates = readRecentCandidates(20);
|
|
const capabilityCandidatesPreview = renderCandidatesPreview(recentCandidates.slice(-8), 1600);
|
|
|
|
// External candidate zone (A2A receive): only surface candidates when local signals trigger them.
|
|
// External candidates are NEVER executed directly; they must be validated and promoted first.
|
|
let externalCandidatesPreview = '(none)';
|
|
try {
|
|
const external = readRecentExternalCandidates(50);
|
|
const list = Array.isArray(external) ? external : [];
|
|
const capsulesOnly = list.filter(x => x && x.type === 'Capsule');
|
|
const genesOnly = list.filter(x => x && x.type === 'Gene');
|
|
|
|
const matchedExternalGenes = genesOnly
|
|
.map(g => {
|
|
const pats = Array.isArray(g.signals_match) ? g.signals_match : [];
|
|
const hit = pats.reduce((acc, p) => (matchPatternToSignals(p, signals) ? acc + 1 : acc), 0);
|
|
return { gene: g, hit };
|
|
})
|
|
.filter(x => x.hit > 0)
|
|
.sort((a, b) => b.hit - a.hit)
|
|
.slice(0, 3)
|
|
.map(x => x.gene);
|
|
|
|
const matchedExternalCapsules = capsulesOnly
|
|
.map(c => {
|
|
const triggers = Array.isArray(c.trigger) ? c.trigger : [];
|
|
const score = triggers.reduce((acc, t) => (matchPatternToSignals(t, signals) ? acc + 1 : acc), 0);
|
|
return { capsule: c, score };
|
|
})
|
|
.filter(x => x.score > 0)
|
|
.sort((a, b) => b.score - a.score)
|
|
.slice(0, 3)
|
|
.map(x => x.capsule);
|
|
|
|
if (matchedExternalGenes.length || matchedExternalCapsules.length) {
|
|
externalCandidatesPreview = `\`\`\`json\n${JSON.stringify(
|
|
[
|
|
...matchedExternalGenes.map(g => ({
|
|
type: g.type,
|
|
id: g.id,
|
|
category: g.category || null,
|
|
signals_match: g.signals_match || [],
|
|
a2a: g.a2a || null,
|
|
})),
|
|
...matchedExternalCapsules.map(c => ({
|
|
type: c.type,
|
|
id: c.id,
|
|
trigger: c.trigger,
|
|
gene: c.gene,
|
|
summary: c.summary,
|
|
confidence: c.confidence,
|
|
blast_radius: c.blast_radius || null,
|
|
outcome: c.outcome || null,
|
|
success_streak: c.success_streak || null,
|
|
a2a: c.a2a || null,
|
|
})),
|
|
],
|
|
null,
|
|
2
|
|
)}\n\`\`\``;
|
|
}
|
|
} catch (e) {}
|
|
|
|
// Search-First Evolution: query Hub for reusable solutions before local reasoning.
|
|
let hubHit = null;
|
|
try {
|
|
hubHit = await hubSearch(signals, { timeoutMs: 8000 });
|
|
if (hubHit && hubHit.hit) {
|
|
console.log(`[SearchFirst] Hub hit: asset=${hubHit.asset_id}, score=${hubHit.score}, mode=${hubHit.mode}`);
|
|
} else {
|
|
console.log(`[SearchFirst] No hub match (reason: ${hubHit && hubHit.reason ? hubHit.reason : 'unknown'}). Proceeding with local evolution.`);
|
|
}
|
|
} catch (e) {
|
|
console.log(`[SearchFirst] Hub search failed (non-fatal): ${e.message}`);
|
|
hubHit = { hit: false, reason: 'exception' };
|
|
}
|
|
|
|
// Memory Graph reasoning: prefer high-confidence paths, suppress known low-success paths (unless drift is explicit).
|
|
let memoryAdvice = null;
|
|
try {
|
|
memoryAdvice = getMemoryAdvice({ signals, genes, driftEnabled: IS_RANDOM_DRIFT });
|
|
} catch (e) {
|
|
console.error(`[MemoryGraph] Read failed: ${e.message}`);
|
|
console.error(`[MemoryGraph] Refusing to evolve without causal memory. Target: ${memoryGraphPath()}`);
|
|
throw new Error(`MemoryGraph Read failed: ${e.message}`);
|
|
}
|
|
|
|
// Reflection Phase: periodically pause to assess evolution strategy.
|
|
try {
|
|
const cycleState = fs.existsSync(STATE_FILE) ? JSON.parse(fs.readFileSync(STATE_FILE, 'utf8')) : {};
|
|
const cycleCount = cycleState.cycleCount || 0;
|
|
if (shouldReflect({ cycleCount, recentEvents })) {
|
|
const narrativeSummary = loadNarrativeSummary(3000);
|
|
const reflectionCtx = buildReflectionContext({
|
|
recentEvents,
|
|
signals,
|
|
memoryAdvice,
|
|
narrative: narrativeSummary,
|
|
});
|
|
recordReflection({
|
|
cycle_count: cycleCount,
|
|
signals_snapshot: signals.slice(0, 20),
|
|
preferred_gene: memoryAdvice && memoryAdvice.preferredGeneId ? memoryAdvice.preferredGeneId : null,
|
|
banned_genes: memoryAdvice && Array.isArray(memoryAdvice.bannedGeneIds) ? memoryAdvice.bannedGeneIds : [],
|
|
context_preview: reflectionCtx.slice(0, 1000),
|
|
});
|
|
console.log(`[Reflection] Strategic reflection recorded at cycle ${cycleCount}.`);
|
|
}
|
|
} catch (e) {
|
|
console.log('[Reflection] Failed (non-fatal): ' + (e && e.message ? e.message : e));
|
|
}
|
|
|
|
var recentFailedCapsules = [];
|
|
try {
|
|
recentFailedCapsules = readRecentFailedCapsules(50);
|
|
} catch (e) {
|
|
console.log('[FailedCapsules] Read failed (non-fatal): ' + e.message);
|
|
}
|
|
|
|
const { selectedGene, capsuleCandidates, selector } = selectGeneAndCapsule({
|
|
genes,
|
|
capsules,
|
|
signals,
|
|
memoryAdvice,
|
|
driftEnabled: IS_RANDOM_DRIFT,
|
|
failedCapsules: recentFailedCapsules,
|
|
});
|
|
|
|
const selectedBy = memoryAdvice && memoryAdvice.preferredGeneId ? 'memory_graph+selector' : 'selector';
|
|
const capsulesUsed = Array.isArray(capsuleCandidates)
|
|
? capsuleCandidates.map(c => (c && c.id ? String(c.id) : null)).filter(Boolean)
|
|
: [];
|
|
const selectedCapsuleId = capsulesUsed.length ? capsulesUsed[0] : null;
|
|
|
|
// Personality selection (natural selection + small mutation when triggered).
|
|
// This state is persisted in MEMORY_DIR and is treated as an evolution control surface (not role-play).
|
|
const personalitySelection = selectPersonalityForRun({
|
|
driftEnabled: IS_RANDOM_DRIFT,
|
|
signals,
|
|
recentEvents,
|
|
});
|
|
const personalityState = personalitySelection && personalitySelection.personality_state ? personalitySelection.personality_state : null;
|
|
|
|
// Mutation object is mandatory for every evolution run.
|
|
const tail = Array.isArray(recentEvents) ? recentEvents.slice(-6) : [];
|
|
const tailOutcomes = tail
|
|
.map(e => (e && e.outcome && e.outcome.status ? String(e.outcome.status) : null))
|
|
.filter(Boolean);
|
|
const stableSuccess = tailOutcomes.length >= 6 && tailOutcomes.every(s => s === 'success');
|
|
const tailAvgScore =
|
|
tail.length > 0
|
|
? tail.reduce((acc, e) => acc + (e && e.outcome && Number.isFinite(Number(e.outcome.score)) ? Number(e.outcome.score) : 0), 0) /
|
|
tail.length
|
|
: 0;
|
|
const innovationPressure =
|
|
!IS_RANDOM_DRIFT &&
|
|
personalityState &&
|
|
Number.isFinite(Number(personalityState.creativity)) &&
|
|
Number(personalityState.creativity) >= 0.75 &&
|
|
stableSuccess &&
|
|
tailAvgScore >= 0.7;
|
|
const forceInnovation =
|
|
String(process.env.FORCE_INNOVATION || process.env.EVOLVE_FORCE_INNOVATION || '').toLowerCase() === 'true';
|
|
const mutationInnovateMode = !!IS_RANDOM_DRIFT || !!innovationPressure || !!forceInnovation;
|
|
const mutationSignals = innovationPressure ? [...(Array.isArray(signals) ? signals : []), 'stable_success_plateau'] : signals;
|
|
const mutationSignalsEffective = forceInnovation
|
|
? [...(Array.isArray(mutationSignals) ? mutationSignals : []), 'force_innovation']
|
|
: mutationSignals;
|
|
|
|
const allowHighRisk =
|
|
!!IS_RANDOM_DRIFT &&
|
|
!!personalitySelection &&
|
|
!!personalitySelection.personality_known &&
|
|
personalityState &&
|
|
isHighRiskMutationAllowed(personalityState) &&
|
|
Number(personalityState.rigor) >= 0.8 &&
|
|
Number(personalityState.risk_tolerance) <= 0.3 &&
|
|
!(Array.isArray(signals) && signals.includes('log_error'));
|
|
const mutation = buildMutation({
|
|
signals: mutationSignalsEffective,
|
|
selectedGene,
|
|
driftEnabled: mutationInnovateMode,
|
|
personalityState,
|
|
allowHighRisk,
|
|
});
|
|
|
|
// Memory Graph: record hypothesis bridging Signal -> Action. If this fails, refuse to evolve.
|
|
let hypothesisId = null;
|
|
try {
|
|
const hyp = recordHypothesis({
|
|
signals,
|
|
mutation,
|
|
personality_state: personalityState,
|
|
selectedGene,
|
|
selector,
|
|
driftEnabled: mutationInnovateMode,
|
|
selectedBy,
|
|
capsulesUsed,
|
|
observations,
|
|
});
|
|
hypothesisId = hyp && hyp.hypothesisId ? hyp.hypothesisId : null;
|
|
} catch (e) {
|
|
console.error(`[MemoryGraph] Hypothesis write failed: ${e.message}`);
|
|
console.error(`[MemoryGraph] Refusing to evolve without causal memory. Target: ${memoryGraphPath()}`);
|
|
throw new Error(`MemoryGraph Hypothesis write failed: ${e.message}`);
|
|
}
|
|
|
|
// Memory Graph: record the chosen causal path for this run. If this fails, refuse to output a mutation prompt.
|
|
try {
|
|
recordAttempt({
|
|
signals,
|
|
mutation,
|
|
personality_state: personalityState,
|
|
selectedGene,
|
|
selector,
|
|
driftEnabled: mutationInnovateMode,
|
|
selectedBy,
|
|
hypothesisId,
|
|
capsulesUsed,
|
|
observations,
|
|
});
|
|
} catch (e) {
|
|
console.error(`[MemoryGraph] Attempt write failed: ${e.message}`);
|
|
console.error(`[MemoryGraph] Refusing to evolve without causal memory. Target: ${memoryGraphPath()}`);
|
|
throw new Error(`MemoryGraph Attempt write failed: ${e.message}`);
|
|
}
|
|
|
|
// Solidify state: capture minimal, auditable context for post-patch validation + asset write.
|
|
// This enforces strict protocol closure after patch application.
|
|
try {
|
|
const runId = `run_${Date.now()}`;
|
|
const parentEventId = getLastEventId();
|
|
const selectedBy = memoryAdvice && memoryAdvice.preferredGeneId ? 'memory_graph+selector' : 'selector';
|
|
|
|
// Baseline snapshot (before any edits).
|
|
let baselineUntracked = [];
|
|
let baselineHead = null;
|
|
try {
|
|
const out = execSync('git ls-files --others --exclude-standard', {
|
|
cwd: REPO_ROOT,
|
|
encoding: 'utf8',
|
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
timeout: 4000,
|
|
windowsHide: true,
|
|
});
|
|
baselineUntracked = String(out)
|
|
.split('\n')
|
|
.map(l => l.trim())
|
|
.filter(Boolean);
|
|
} catch (e) {}
|
|
|
|
try {
|
|
const out = execSync('git rev-parse HEAD', {
|
|
cwd: REPO_ROOT,
|
|
encoding: 'utf8',
|
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
timeout: 4000,
|
|
windowsHide: true,
|
|
});
|
|
baselineHead = String(out || '').trim() || null;
|
|
} catch (e) {}
|
|
|
|
const maxFiles =
|
|
selectedGene && selectedGene.constraints && Number.isFinite(Number(selectedGene.constraints.max_files))
|
|
? Number(selectedGene.constraints.max_files)
|
|
: 12;
|
|
const blastRadiusEstimate = {
|
|
files: Number.isFinite(maxFiles) && maxFiles > 0 ? maxFiles : 0,
|
|
lines: Number.isFinite(maxFiles) && maxFiles > 0 ? Math.round(maxFiles * 80) : 0,
|
|
};
|
|
|
|
// Merge into existing state to preserve last_solidify (do not wipe it).
|
|
const prevState = readStateForSolidify();
|
|
prevState.last_run = {
|
|
run_id: runId,
|
|
created_at: new Date().toISOString(),
|
|
parent_event_id: parentEventId || null,
|
|
selected_gene_id: selectedGene && selectedGene.id ? selectedGene.id : null,
|
|
selected_capsule_id: selectedCapsuleId,
|
|
selector: selector || null,
|
|
signals: Array.isArray(signals) ? signals : [],
|
|
mutation: mutation || null,
|
|
mutation_id: mutation && mutation.id ? mutation.id : null,
|
|
personality_state: personalityState || null,
|
|
personality_key: personalitySelection && personalitySelection.personality_key ? personalitySelection.personality_key : null,
|
|
personality_known: !!(personalitySelection && personalitySelection.personality_known),
|
|
personality_mutations:
|
|
personalitySelection && Array.isArray(personalitySelection.personality_mutations)
|
|
? personalitySelection.personality_mutations
|
|
: [],
|
|
drift: !!IS_RANDOM_DRIFT,
|
|
selected_by: selectedBy,
|
|
source_type: hubHit && hubHit.hit ? (hubHit.mode === 'direct' ? 'reused' : 'reference') : 'generated',
|
|
reused_asset_id: hubHit && hubHit.hit ? (hubHit.asset_id || null) : null,
|
|
reused_source_node: hubHit && hubHit.hit ? (hubHit.source_node_id || null) : null,
|
|
reused_chain_id: hubHit && hubHit.hit ? (hubHit.chain_id || null) : null,
|
|
baseline_untracked: baselineUntracked,
|
|
baseline_git_head: baselineHead,
|
|
blast_radius_estimate: blastRadiusEstimate,
|
|
active_task_id: activeTask ? (activeTask.id || activeTask.task_id || null) : null,
|
|
active_task_title: activeTask ? (activeTask.title || null) : null,
|
|
applied_lessons: hubLessons.map(function(l) { return l.lesson_id; }).filter(Boolean),
|
|
hub_lessons: hubLessons,
|
|
};
|
|
writeStateForSolidify(prevState);
|
|
|
|
if (hubHit && hubHit.hit) {
|
|
const assetAction = hubHit.mode === 'direct' ? 'asset_reuse' : 'asset_reference';
|
|
logAssetCall({
|
|
run_id: runId,
|
|
action: assetAction,
|
|
asset_id: hubHit.asset_id || null,
|
|
asset_type: hubHit.match && hubHit.match.type ? hubHit.match.type : null,
|
|
source_node_id: hubHit.source_node_id || null,
|
|
chain_id: hubHit.chain_id || null,
|
|
score: hubHit.score || null,
|
|
mode: hubHit.mode,
|
|
signals: Array.isArray(signals) ? signals : [],
|
|
extra: {
|
|
selected_gene_id: selectedGene && selectedGene.id ? selectedGene.id : null,
|
|
task_id: activeTask ? (activeTask.id || activeTask.task_id || null) : null,
|
|
},
|
|
});
|
|
}
|
|
} catch (e) {
|
|
console.error(`[SolidifyState] Write failed: ${e.message}`);
|
|
}
|
|
|
|
const genesPreview = `\`\`\`json\n${JSON.stringify(genes.slice(0, 6), null, 2)}\n\`\`\``;
|
|
const capsulesPreview = `\`\`\`json\n${JSON.stringify(capsules.slice(-3), null, 2)}\n\`\`\``;
|
|
|
|
const reviewNote = IS_REVIEW_MODE
|
|
? 'Review mode: before significant edits, pause and ask the user for confirmation.'
|
|
: 'Review mode: disabled.';
|
|
|
|
// Build recent evolution history summary for context injection
|
|
const recentHistorySummary = (() => {
|
|
if (!recentEvents || recentEvents.length === 0) return '(no prior evolution events)';
|
|
const last8 = recentEvents.slice(-8);
|
|
const lines = last8.map((evt, idx) => {
|
|
const sigs = Array.isArray(evt.signals) ? evt.signals.slice(0, 3).join(', ') : '?';
|
|
const gene = Array.isArray(evt.genes_used) && evt.genes_used.length ? evt.genes_used[0] : 'none';
|
|
const outcome = evt.outcome && evt.outcome.status ? evt.outcome.status : '?';
|
|
const ts = evt.meta && evt.meta.at ? evt.meta.at : (evt.id || '');
|
|
return ` ${idx + 1}. [${evt.intent || '?'}] signals=[${sigs}] gene=${gene} outcome=${outcome} @${ts}`;
|
|
});
|
|
return lines.join('\n');
|
|
})();
|
|
|
|
const context = `
|
|
Runtime state:
|
|
- System health: ${healthReport}
|
|
- Agent state: ${moodStatus}
|
|
- Scan duration: ${scanTime}ms
|
|
- Memory size: ${memorySize} bytes
|
|
- Skills available (if any):
|
|
${fileList || '[skills directory not found]'}
|
|
|
|
Notes:
|
|
- ${reviewNote}
|
|
- ${reportingDirective}
|
|
- ${syncDirective}
|
|
|
|
Recent Evolution History (last 8 cycles -- DO NOT repeat the same intent+signal+gene):
|
|
${recentHistorySummary}
|
|
IMPORTANT: If you see 3+ consecutive "repair" cycles with the same gene, you MUST switch to "innovate" intent.
|
|
${(() => {
|
|
// Compute consecutive failure count from recent events for context injection
|
|
let cfc = 0;
|
|
const evts = Array.isArray(recentEvents) ? recentEvents : [];
|
|
for (let i = evts.length - 1; i >= 0; i--) {
|
|
if (evts[i] && evts[i].outcome && evts[i].outcome.status === 'failed') cfc++;
|
|
else break;
|
|
}
|
|
if (cfc >= 3) {
|
|
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.`;
|
|
}
|
|
return '';
|
|
})()}
|
|
|
|
External candidates (A2A receive zone; staged only, never execute directly):
|
|
${externalCandidatesPreview}
|
|
|
|
Global memory (MEMORY.md):
|
|
\`\`\`
|
|
${memorySnippet}
|
|
\`\`\`
|
|
|
|
User registry (USER.md):
|
|
\`\`\`
|
|
${userSnippet}
|
|
\`\`\`
|
|
|
|
Recent memory snippet:
|
|
\`\`\`
|
|
${todayLog.slice(-3000)}
|
|
\`\`\`
|
|
|
|
Recent session transcript:
|
|
\`\`\`
|
|
${recentMasterLog}
|
|
\`\`\`
|
|
|
|
Mutation directive:
|
|
${mutationDirective}
|
|
`.trim();
|
|
|
|
// Build the prompt: in direct-reuse mode, use a minimal reuse prompt.
|
|
// In reference mode (or no hit), use the full GEP prompt with hub match injected.
|
|
const isDirectReuse = hubHit && hubHit.hit && hubHit.mode === 'direct';
|
|
const hubMatchedBlock = hubHit && hubHit.hit && hubHit.mode === 'reference'
|
|
? buildHubMatchedBlock({ capsule: hubHit.match })
|
|
: null;
|
|
|
|
const prompt = isDirectReuse
|
|
? buildReusePrompt({
|
|
capsule: hubHit.match,
|
|
signals,
|
|
nowIso: new Date().toISOString(),
|
|
})
|
|
: buildGepPrompt({
|
|
nowIso: new Date().toISOString(),
|
|
context,
|
|
signals,
|
|
selector,
|
|
parentEventId: getLastEventId(),
|
|
selectedGene,
|
|
capsuleCandidates,
|
|
genesPreview,
|
|
capsulesPreview,
|
|
capabilityCandidatesPreview,
|
|
externalCandidatesPreview,
|
|
hubMatchedBlock,
|
|
failedCapsules: recentFailedCapsules,
|
|
hubLessons,
|
|
});
|
|
|
|
// Optional: emit a compact thought process block for wrappers (noise-controlled).
|
|
const emitThought = String(process.env.EVOLVE_EMIT_THOUGHT_PROCESS || '').toLowerCase() === 'true';
|
|
if (emitThought) {
|
|
const s = Array.isArray(signals) ? signals : [];
|
|
const thought = [
|
|
`cycle_id: ${cycleId}`,
|
|
`signals_count: ${s.length}`,
|
|
`signals: ${s.slice(0, 12).join(', ')}${s.length > 12 ? ' ...' : ''}`,
|
|
`selected_gene: ${selectedGene && selectedGene.id ? String(selectedGene.id) : '(none)'}`,
|
|
`selected_capsule: ${selectedCapsuleId ? String(selectedCapsuleId) : '(none)'}`,
|
|
`mutation_category: ${mutation && mutation.category ? String(mutation.category) : '(none)'}`,
|
|
`force_innovation: ${forceInnovation ? 'true' : 'false'}`,
|
|
`source_type: ${hubHit && hubHit.hit ? (isDirectReuse ? 'reused' : 'reference') : 'generated'}`,
|
|
`hub_reuse_mode: ${isDirectReuse ? 'direct' : hubMatchedBlock ? 'reference' : 'none'}`,
|
|
].join('\n');
|
|
console.log(`[THOUGHT_PROCESS]\n${thought}\n[/THOUGHT_PROCESS]`);
|
|
}
|
|
|
|
const printPrompt = String(process.env.EVOLVE_PRINT_PROMPT || '').toLowerCase() === 'true';
|
|
|
|
// Default behavior (v1.4.1+): "execute-by-default" by bridging prompt -> sub-agent via sessions_spawn.
|
|
// This project is the Brain; the Hand is a spawned executor agent. Wrappers can disable bridging with EVOLVE_BRIDGE=false.
|
|
if (bridgeEnabled) {
|
|
// Reuse the run_id stored in the solidify state when possible.
|
|
let runId = `run_${Date.now()}`;
|
|
try {
|
|
const st = readStateForSolidify();
|
|
if (st && st.last_run && st.last_run.run_id) runId = String(st.last_run.run_id);
|
|
} catch (e) {}
|
|
let artifact = null;
|
|
try {
|
|
artifact = writePromptArtifact({
|
|
memoryDir: getEvolutionDir(),
|
|
cycleId,
|
|
runId,
|
|
prompt,
|
|
meta: {
|
|
agent: AGENT_NAME,
|
|
drift_enabled: IS_RANDOM_DRIFT,
|
|
review_mode: IS_REVIEW_MODE,
|
|
dry_run: IS_DRY_RUN,
|
|
mutation_id: mutation && mutation.id ? mutation.id : null,
|
|
personality_key: personalitySelection && personalitySelection.personality_key ? personalitySelection.personality_key : null,
|
|
},
|
|
});
|
|
} catch (e) {
|
|
artifact = null;
|
|
}
|
|
|
|
const executorTask = [
|
|
'You are the executor (the Hand).',
|
|
'Your job is to apply a safe, minimal patch in this repo following the attached GEP protocol prompt.',
|
|
artifact && artifact.promptPath ? `Prompt file: ${artifact.promptPath}` : 'Prompt file: (unavailable)',
|
|
'',
|
|
'After applying changes and validations, you MUST run:',
|
|
' node index.js solidify',
|
|
'',
|
|
'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.',
|
|
'Example:',
|
|
'sessions_spawn({ task: "exec: node skills/feishu-evolver-wrapper/lifecycle.js ensure", agentId: "main", cleanup: "delete", label: "gep_loop_next" })',
|
|
'',
|
|
'GEP protocol prompt (may be truncated here; prefer the prompt file if provided):',
|
|
clip(prompt, 24000),
|
|
].join('\n');
|
|
|
|
const spawn = renderSessionsSpawnCall({
|
|
task: executorTask,
|
|
agentId: AGENT_NAME,
|
|
cleanup: 'delete',
|
|
label: `gep_bridge_${cycleNum}`,
|
|
});
|
|
|
|
console.log('\n[BRIDGE ENABLED] Spawning executor agent via sessions_spawn.');
|
|
console.log(spawn);
|
|
if (printPrompt) {
|
|
console.log('\n[PROMPT OUTPUT] (EVOLVE_PRINT_PROMPT=true)');
|
|
console.log(prompt);
|
|
}
|
|
} else {
|
|
console.log(prompt);
|
|
console.log('\n[SOLIDIFY REQUIRED] After applying the patch and validations, run: node index.js solidify');
|
|
}
|
|
}
|
|
|
|
module.exports = { run };
|
|
|