feat: Capability Evolver 升级 + 日常运行日志
主要变更: - 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 配置 - 忽略临时工作空间目录
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
node_modules/
|
||||
.env
|
||||
memory/
|
||||
assets/gep/candidates.jsonl
|
||||
assets/gep/external_candidates.jsonl
|
||||
dist-public/
|
||||
|
||||
# Docker / vibe testing
|
||||
test/tmp/
|
||||
.docker-test-state/
|
||||
|
||||
# Local identity persistence (device + node)
|
||||
.evomap_device_id
|
||||
.evomap_node_id
|
||||
@@ -0,0 +1,22 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 OpenClaw
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
@@ -19,6 +19,11 @@ Capability Evolver is the core engine behind **[EvoMap](https://evomap.ai)**, a
|
||||
|
||||
Keywords: protocol-constrained evolution, audit trail, genes and capsules, prompt governance.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Node.js** >= 18
|
||||
- **Git** -- Required. Evolver uses git for rollback, blast radius calculation, and solidify. Running in a non-git directory will fail with a clear error message.
|
||||
|
||||
## Try It Now (Minimal)
|
||||
|
||||
```bash
|
||||
@@ -221,14 +226,16 @@ The script automatically detects if compatible local skills (like `skills/feishu
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
- [onthebigtree](https://github.com/onthebigtree) -- Inspired the creation of evomap evolution network.
|
||||
- [onthebigtree](https://github.com/onthebigtree) -- Inspired the creation of evomap evolution network. Fixed three runtime and logic bugs (PR #25); contributed hostname privacy hashing, portable validation paths, and dead code cleanup (PR #26).
|
||||
- [lichunr](https://github.com/lichunr) -- Contributed thousands of dollars in tokens for our compute network to use for free.
|
||||
- [shinjiyu](https://github.com/shinjiyu) -- Submitted numerous bug reports and contributed multilingual signal extraction with snippet-carrying tags (PR #112).
|
||||
- [voidborne-d](https://github.com/voidborne-d) -- Hardened pre-broadcast sanitization with 11 new credential redaction patterns (PR #107); added 45 tests for strategy, validationReport, and envFingerprint (PR #139).
|
||||
- [blackdogcat](https://github.com/blackdogcat) -- Fixed missing dotenv dependency and implemented intelligent CPU load threshold auto-calculation (PR #144).
|
||||
- [LKCY33](https://github.com/LKCY33) -- Fixed .env loading path and directory permissions (PR #21).
|
||||
- [hendrixAIDev](https://github.com/hendrixAIDev) -- Fixed performMaintenance() running in dry-run mode (PR #68).
|
||||
- [toller892](https://github.com/toller892) -- Independently identified and reported the events.jsonl forbidden_paths bug (PR #149).
|
||||
- [upbit](https://github.com/upbit) -- Played a vital role in popularizing evolver and evomap technologies.
|
||||
- [Chi Jianqiang](https://mowen.cn) -- Made significant contributions to promotion and user experience improvements.
|
||||
- More contributors to be added.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -27,6 +27,11 @@ Capability Evolver 是 **[EvoMap](https://evomap.ai)** 的核心引擎。EvoMap
|
||||
- **动态集成**:自动检测并使用本地工具,如果不存在则回退到通用模式。
|
||||
- **持续循环模式**:持续运行的自我进化循环。
|
||||
|
||||
## 前置条件
|
||||
|
||||
- **Node.js** >= 18
|
||||
- **Git** -- 必需。Evolver 依赖 git 进行回滚、变更范围计算和固化(solidify)。在非 git 目录中运行会直接报错并退出。
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 标准运行(自动化)
|
||||
@@ -190,13 +195,16 @@ MAJOR.MINOR.PATCH
|
||||
|
||||
## 鸣谢
|
||||
|
||||
- [onthebigtree](https://github.com/onthebigtree) -- 启发了 evomap 进化网络的诞生。
|
||||
- [onthebigtree](https://github.com/onthebigtree) -- 启发了 evomap 进化网络的诞生。修复了三个运行时逻辑 bug (PR #25);贡献了主机名隐私哈希、可移植验证路径和死代码清理 (PR #26)。
|
||||
- [lichunr](https://github.com/lichunr) -- 提供了数千美金 Token 供算力网络免费使用。
|
||||
- [shinjiyu](https://github.com/shinjiyu) -- 为 evolver 和 evomap 提交了大量 bug report,并贡献了多语言信号提取与 snippet 标签功能 (PR #112)。
|
||||
- [voidborne-d](https://github.com/voidborne-d) -- 为预广播脱敏层新增 11 种凭证检测模式,强化安全防护 (PR #107)。
|
||||
- [voidborne-d](https://github.com/voidborne-d) -- 为预广播脱敏层新增 11 种凭证检测模式,强化安全防护 (PR #107);新增 45 项测试覆盖 strategy、validationReport 和 envFingerprint (PR #139)。
|
||||
- [blackdogcat](https://github.com/blackdogcat) -- 修复 dotenv 缺失依赖并实现智能 CPU 负载阈值自动计算 (PR #144)。
|
||||
- [LKCY33](https://github.com/LKCY33) -- 修复 .env 加载路径和目录权限问题 (PR #21)。
|
||||
- [hendrixAIDev](https://github.com/hendrixAIDev) -- 修复 dry-run 模式下 performMaintenance() 仍执行的问题 (PR #68)。
|
||||
- [toller892](https://github.com/toller892) -- 独立发现并报告了 events.jsonl forbidden_paths 冲突 bug (PR #149)。
|
||||
- [upbit](https://github.com/upbit) -- 在 evolver 和 evomap 技术的普及中起到了至关重要的作用。
|
||||
- [池建强](https://mowen.cn) -- 在传播和用户体验改进过程中做出了巨大贡献。
|
||||
- 其余贡献者扩充中。
|
||||
|
||||
## 许可证
|
||||
MIT
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"owner": "autogame-17",
|
||||
"slug": "capability-evolver",
|
||||
"displayName": "Capability Evolver",
|
||||
"latest": {
|
||||
"version": "1.20.4",
|
||||
"publishedAt": 1772344308631,
|
||||
"commit": "https://github.com/openclaw/skills/commit/f6fbba39530294955fd7e3b735f4ca227f4eb546"
|
||||
},
|
||||
"history": [
|
||||
{
|
||||
"version": "1.14.0",
|
||||
"publishedAt": 1771472983583,
|
||||
"commit": "https://github.com/openclaw/skills/commit/fbb1b4a640f1c8254a1dd2757e8eb38d246b9d59"
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 125 KiB |
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"version": 1,
|
||||
"failed_capsules": []
|
||||
}
|
||||
Executable → Regular
+77
-19
@@ -43,12 +43,16 @@ function acquireLock() {
|
||||
try {
|
||||
if (fs.existsSync(lockFile)) {
|
||||
const pid = parseInt(fs.readFileSync(lockFile, 'utf8').trim(), 10);
|
||||
try {
|
||||
process.kill(pid, 0); // Check if process exists
|
||||
console.log(`[Singleton] Evolver loop already running (PID ${pid}). Exiting.`);
|
||||
return false;
|
||||
} catch (e) {
|
||||
console.log(`[Singleton] Stale lock found (PID ${pid}). Taking over.`);
|
||||
if (!Number.isFinite(pid) || pid <= 0) {
|
||||
console.log('[Singleton] Corrupt lock file (invalid PID). Taking over.');
|
||||
} else {
|
||||
try {
|
||||
process.kill(pid, 0);
|
||||
console.log(`[Singleton] Evolver loop already running (PID ${pid}). Exiting.`);
|
||||
return false;
|
||||
} catch (e) {
|
||||
console.log(`[Singleton] Stale lock found (PID ${pid}). Taking over.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
fs.writeFileSync(lockFile, String(process.pid));
|
||||
@@ -88,7 +92,8 @@ async function main() {
|
||||
process.env.EVOLVE_BRIDGE = 'false';
|
||||
console.log('Loop mode enabled (internal daemon).');
|
||||
|
||||
const solidifyStatePath = path.join(__dirname, 'memory', 'evolution_solidify_state.json');
|
||||
const { getEvolutionDir } = require('./src/gep/paths');
|
||||
const solidifyStatePath = path.join(getEvolutionDir(), 'evolution_solidify_state.json');
|
||||
|
||||
const minSleepMs = parseMs(process.env.EVOLVER_MIN_SLEEP_MS, 2000);
|
||||
const maxSleepMs = parseMs(process.env.EVOLVER_MAX_SLEEP_MS, 300000);
|
||||
@@ -149,16 +154,20 @@ async function main() {
|
||||
const memMb = process.memoryUsage().rss / 1024 / 1024;
|
||||
if (cycleCount >= maxCyclesPerProcess || memMb > maxRssMb) {
|
||||
console.log(`[Daemon] Restarting self (cycles=${cycleCount}, rssMb=${memMb.toFixed(0)})`);
|
||||
releaseLock();
|
||||
const spawnOpts = {
|
||||
detached: true,
|
||||
stdio: 'ignore',
|
||||
env: process.env,
|
||||
windowsHide: true,
|
||||
};
|
||||
const child = spawn(process.execPath, [__filename, ...args], spawnOpts);
|
||||
child.unref();
|
||||
process.exit(0);
|
||||
try {
|
||||
const spawnOpts = {
|
||||
detached: true,
|
||||
stdio: 'ignore',
|
||||
env: process.env,
|
||||
windowsHide: true,
|
||||
};
|
||||
const child = spawn(process.execPath, [__filename, ...args], spawnOpts);
|
||||
child.unref();
|
||||
releaseLock();
|
||||
process.exit(0);
|
||||
} catch (spawnErr) {
|
||||
console.error('[Daemon] Spawn failed, continuing current process:', spawnErr.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,15 +277,64 @@ async function main() {
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
} else if (command === 'asset-log') {
|
||||
const { summarizeCallLog, readCallLog, getLogPath } = require('./src/gep/assetCallLog');
|
||||
|
||||
const runIdFlag = args.find(a => typeof a === 'string' && a.startsWith('--run='));
|
||||
const actionFlag = args.find(a => typeof a === 'string' && a.startsWith('--action='));
|
||||
const lastFlag = args.find(a => typeof a === 'string' && a.startsWith('--last='));
|
||||
const sinceFlag = args.find(a => typeof a === 'string' && a.startsWith('--since='));
|
||||
const jsonMode = args.includes('--json');
|
||||
|
||||
const opts = {};
|
||||
if (runIdFlag) opts.run_id = runIdFlag.slice('--run='.length);
|
||||
if (actionFlag) opts.action = actionFlag.slice('--action='.length);
|
||||
if (lastFlag) opts.last = parseInt(lastFlag.slice('--last='.length), 10);
|
||||
if (sinceFlag) opts.since = sinceFlag.slice('--since='.length);
|
||||
|
||||
if (jsonMode) {
|
||||
const entries = readCallLog(opts);
|
||||
console.log(JSON.stringify(entries, null, 2));
|
||||
} else {
|
||||
const summary = summarizeCallLog(opts);
|
||||
console.log(`\n[Asset Call Log] ${getLogPath()}`);
|
||||
console.log(` Total entries: ${summary.total_entries}`);
|
||||
console.log(` Unique assets: ${summary.unique_assets}`);
|
||||
console.log(` Unique runs: ${summary.unique_runs}`);
|
||||
console.log(` By action:`);
|
||||
for (const [action, count] of Object.entries(summary.by_action)) {
|
||||
console.log(` ${action}: ${count}`);
|
||||
}
|
||||
if (summary.entries.length > 0) {
|
||||
console.log(`\n Recent entries:`);
|
||||
const show = summary.entries.slice(-10);
|
||||
for (const e of show) {
|
||||
const ts = e.timestamp ? e.timestamp.slice(0, 19) : '?';
|
||||
const assetShort = e.asset_id ? e.asset_id.slice(0, 20) + '...' : '(none)';
|
||||
const sigPreview = Array.isArray(e.signals) ? e.signals.slice(0, 3).join(', ') : '';
|
||||
console.log(` [${ts}] ${e.action || '?'} asset=${assetShort} score=${e.score || '-'} mode=${e.mode || '-'} signals=[${sigPreview}] run=${e.run_id || '-'}`);
|
||||
}
|
||||
} else {
|
||||
console.log('\n No entries found.');
|
||||
}
|
||||
console.log('');
|
||||
}
|
||||
|
||||
} else {
|
||||
console.log(`Usage: node index.js [run|/evolve|solidify|distill] [--loop]
|
||||
console.log(`Usage: node index.js [run|/evolve|solidify|distill|asset-log] [--loop]
|
||||
- solidify flags:
|
||||
- --dry-run
|
||||
- --no-rollback
|
||||
- --intent=repair|optimize|innovate
|
||||
- --summary=...
|
||||
- distill flags:
|
||||
- --response-file=<path> (LLM response file for skill distillation)`);
|
||||
- --response-file=<path> (LLM response file for skill distillation)
|
||||
- asset-log flags:
|
||||
- --run=<run_id> (filter by run ID)
|
||||
- --action=<action> (filter: hub_search_hit, hub_search_miss, asset_reuse, asset_reference, asset_publish, asset_publish_skip)
|
||||
- --last=<N> (show last N entries)
|
||||
- --since=<ISO_date> (entries after date)
|
||||
- --json (raw JSON output)`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "evolver",
|
||||
"version": "1.20.4",
|
||||
"version": "1.23.0",
|
||||
"description": "A GEP-powered self-evolution engine for AI agents. Features automated log analysis and Genome Evolution Protocol (GEP) for auditable, reusable evolution assets.",
|
||||
"main": "index.js",
|
||||
"keywords": [
|
||||
|
||||
@@ -18,6 +18,7 @@ const {
|
||||
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 {
|
||||
@@ -35,6 +36,8 @@ 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();
|
||||
|
||||
@@ -744,6 +747,19 @@ async function run() {
|
||||
|
||||
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'));
|
||||
@@ -1177,6 +1193,31 @@ async function run() {
|
||||
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);
|
||||
@@ -1370,6 +1411,25 @@ async function run() {
|
||||
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}`);
|
||||
}
|
||||
|
||||
@@ -28,8 +28,8 @@ function getBlastRadiusLimits() {
|
||||
|
||||
function isBlastRadiusSafe(blastRadius) {
|
||||
var lim = getBlastRadiusLimits();
|
||||
var files = blastRadius && Number.isFinite(Number(blastRadius.files)) ? Number(blastRadius.files) : 0;
|
||||
var lines = blastRadius && Number.isFinite(Number(blastRadius.lines)) ? Number(blastRadius.lines) : 0;
|
||||
var files = blastRadius && Number.isFinite(Number(blastRadius.files)) ? Math.max(0, Number(blastRadius.files)) : 0;
|
||||
var lines = blastRadius && Number.isFinite(Number(blastRadius.lines)) ? Math.max(0, Number(blastRadius.lines)) : 0;
|
||||
return files <= lim.maxFiles && lines <= lim.maxLines;
|
||||
}
|
||||
|
||||
|
||||
@@ -96,6 +96,9 @@ function getNodeId() {
|
||||
// --- Base message builder ---
|
||||
|
||||
function buildMessage(params) {
|
||||
if (!params || typeof params !== 'object') {
|
||||
throw new Error('buildMessage requires a params object');
|
||||
}
|
||||
var messageType = params.messageType;
|
||||
var payload = params.payload;
|
||||
var senderId = params.senderId;
|
||||
@@ -197,14 +200,18 @@ function buildPublishBundle(opts) {
|
||||
|
||||
function buildFetch(opts) {
|
||||
var o = opts || {};
|
||||
var fetchPayload = {
|
||||
asset_type: o.assetType || null,
|
||||
local_id: o.localId || null,
|
||||
content_hash: o.contentHash || null,
|
||||
};
|
||||
if (Array.isArray(o.signals) && o.signals.length > 0) {
|
||||
fetchPayload.signals = o.signals;
|
||||
}
|
||||
return buildMessage({
|
||||
messageType: 'fetch',
|
||||
senderId: o.nodeId,
|
||||
payload: {
|
||||
asset_type: o.assetType || null,
|
||||
local_id: o.localId || null,
|
||||
content_hash: o.contentHash || null,
|
||||
},
|
||||
payload: fetchPayload,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -351,7 +358,8 @@ function httpTransportReceive(opts) {
|
||||
var hubUrl = (opts && opts.hubUrl) || process.env.A2A_HUB_URL;
|
||||
if (!hubUrl) return Promise.resolve([]);
|
||||
var assetType = (opts && opts.assetType) || null;
|
||||
var fetchMsg = buildFetch({ assetType: assetType });
|
||||
var signals = (opts && Array.isArray(opts.signals)) ? opts.signals : null;
|
||||
var fetchMsg = buildFetch({ assetType: assetType, signals: signals });
|
||||
var endpoint = hubUrl.replace(/\/+$/, '') + '/a2a/fetch';
|
||||
return fetch(endpoint, {
|
||||
method: 'POST',
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
// Append-only asset call log for tracking Hub asset interactions per evolution run.
|
||||
// Log file: {evolution_dir}/asset_call_log.jsonl
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { getEvolutionDir } = require('./paths');
|
||||
|
||||
function getLogPath() {
|
||||
return path.join(getEvolutionDir(), 'asset_call_log.jsonl');
|
||||
}
|
||||
|
||||
function ensureDir(filePath) {
|
||||
const dir = path.dirname(filePath);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Append a single asset call record to the log.
|
||||
*
|
||||
* @param {object} entry
|
||||
* @param {string} entry.run_id
|
||||
* @param {string} entry.action - hub_search_hit | hub_search_miss | asset_reuse | asset_reference | asset_publish | asset_publish_skip
|
||||
* @param {string} [entry.asset_id]
|
||||
* @param {string} [entry.asset_type]
|
||||
* @param {string} [entry.source_node_id]
|
||||
* @param {string} [entry.chain_id]
|
||||
* @param {number} [entry.score]
|
||||
* @param {string} [entry.mode] - direct | reference
|
||||
* @param {string[]} [entry.signals]
|
||||
* @param {string} [entry.reason]
|
||||
* @param {object} [entry.extra]
|
||||
*/
|
||||
function logAssetCall(entry) {
|
||||
if (!entry || typeof entry !== 'object') return;
|
||||
try {
|
||||
const logPath = getLogPath();
|
||||
ensureDir(logPath);
|
||||
const record = {
|
||||
timestamp: new Date().toISOString(),
|
||||
...entry,
|
||||
};
|
||||
fs.appendFileSync(logPath, JSON.stringify(record) + '\n', 'utf8');
|
||||
} catch (e) {
|
||||
// Non-fatal: never block evolution for logging failure
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read asset call log entries with optional filters.
|
||||
*
|
||||
* @param {object} [opts]
|
||||
* @param {string} [opts.run_id] - filter by run_id
|
||||
* @param {string} [opts.action] - filter by action type
|
||||
* @param {number} [opts.last] - only return last N entries
|
||||
* @param {string} [opts.since] - ISO date string, only entries after this time
|
||||
* @returns {object[]}
|
||||
*/
|
||||
function readCallLog(opts) {
|
||||
const o = opts || {};
|
||||
const logPath = getLogPath();
|
||||
if (!fs.existsSync(logPath)) return [];
|
||||
|
||||
const raw = fs.readFileSync(logPath, 'utf8');
|
||||
const lines = raw.split('\n').filter(Boolean);
|
||||
|
||||
let entries = [];
|
||||
for (const line of lines) {
|
||||
try {
|
||||
entries.push(JSON.parse(line));
|
||||
} catch (e) { /* skip corrupt lines */ }
|
||||
}
|
||||
|
||||
if (o.since) {
|
||||
const sinceTs = new Date(o.since).getTime();
|
||||
if (Number.isFinite(sinceTs)) {
|
||||
entries = entries.filter(e => new Date(e.timestamp).getTime() >= sinceTs);
|
||||
}
|
||||
}
|
||||
|
||||
if (o.run_id) {
|
||||
entries = entries.filter(e => e.run_id === o.run_id);
|
||||
}
|
||||
|
||||
if (o.action) {
|
||||
entries = entries.filter(e => e.action === o.action);
|
||||
}
|
||||
|
||||
if (o.last && Number.isFinite(o.last) && o.last > 0) {
|
||||
entries = entries.slice(-o.last);
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Summarize asset call log (for CLI display).
|
||||
*
|
||||
* @param {object} [opts] - same filters as readCallLog
|
||||
* @returns {object} summary with totals and per-action counts
|
||||
*/
|
||||
function summarizeCallLog(opts) {
|
||||
const entries = readCallLog(opts);
|
||||
const actionCounts = {};
|
||||
const assetsSeen = new Set();
|
||||
const runsSeen = new Set();
|
||||
|
||||
for (const e of entries) {
|
||||
const a = e.action || 'unknown';
|
||||
actionCounts[a] = (actionCounts[a] || 0) + 1;
|
||||
if (e.asset_id) assetsSeen.add(e.asset_id);
|
||||
if (e.run_id) runsSeen.add(e.run_id);
|
||||
}
|
||||
|
||||
return {
|
||||
total_entries: entries.length,
|
||||
unique_assets: assetsSeen.size,
|
||||
unique_runs: runsSeen.size,
|
||||
by_action: actionCounts,
|
||||
entries,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
logAssetCall,
|
||||
readCallLog,
|
||||
summarizeCallLog,
|
||||
getLogPath,
|
||||
};
|
||||
@@ -2,11 +2,17 @@
|
||||
//
|
||||
// Flow: extractSignals() -> hubSearch(signals) -> if hit: reuse; if miss: normal evolve
|
||||
// Two modes: direct (skip local reasoning) | reference (inject into prompt as strong hint)
|
||||
//
|
||||
// Uses POST /a2a/fetch with signals field (protocol-native) instead of GET /a2a/assets/search.
|
||||
// This returns full payload (content, diff, strategy) in a single round-trip.
|
||||
|
||||
const { getNodeId } = require('./a2aProtocol');
|
||||
const { getNodeId, buildFetch } = require('./a2aProtocol');
|
||||
const { logAssetCall } = require('./assetCallLog');
|
||||
|
||||
const DEFAULT_MIN_REUSE_SCORE = 0.72;
|
||||
const DEFAULT_REUSE_MODE = 'reference'; // 'direct' | 'reference'
|
||||
const MAX_STREAK_CAP = 5;
|
||||
const TIMEOUT_REASON = 'hub_search_timeout';
|
||||
|
||||
function getHubUrl() {
|
||||
return (process.env.A2A_HUB_URL || '').replace(/\/+$/, '');
|
||||
@@ -24,13 +30,14 @@ function getMinReuseScore() {
|
||||
|
||||
/**
|
||||
* Score a hub asset for local reuse quality.
|
||||
* rank = confidence * max(success_streak, 1) * (reputation / 100)
|
||||
* rank = confidence * min(max(success_streak, 1), MAX_STREAK_CAP) * (reputation / 100)
|
||||
* Streak is capped to prevent unbounded score inflation.
|
||||
*/
|
||||
function scoreHubResult(asset) {
|
||||
const confidence = Number(asset.confidence) || 0;
|
||||
const streak = Math.max(Number(asset.success_streak) || 0, 1);
|
||||
// Reputation is included in asset from hub ranked endpoint; default 50 if missing
|
||||
const reputation = Number(asset.reputation_score) || 50;
|
||||
const streak = Math.min(Math.max(Number(asset.success_streak) || 0, 1), MAX_STREAK_CAP);
|
||||
const repRaw = Number(asset.reputation_score);
|
||||
const reputation = Number.isFinite(repRaw) ? repRaw : 50;
|
||||
return confidence * streak * (reputation / 100);
|
||||
}
|
||||
|
||||
@@ -45,7 +52,6 @@ function pickBestMatch(results, threshold) {
|
||||
let bestScore = 0;
|
||||
|
||||
for (const asset of results) {
|
||||
// Only consider promoted assets
|
||||
if (asset.status && asset.status !== 'promoted') continue;
|
||||
const s = scoreHubResult(asset);
|
||||
if (s > bestScore) {
|
||||
@@ -64,48 +70,96 @@ function pickBestMatch(results, threshold) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Search the hub for reusable capsules matching the given signals.
|
||||
* Search the hub for reusable assets matching the given signals.
|
||||
* Uses POST /a2a/fetch with signals (protocol-native, returns full payload).
|
||||
* Falls back to GET /a2a/assets/search if fetch fails.
|
||||
* Returns { hit: true, match, score, mode } or { hit: false }.
|
||||
*/
|
||||
async function hubSearch(signals, opts) {
|
||||
const hubUrl = getHubUrl();
|
||||
if (!hubUrl) return { hit: false, reason: 'no_hub_url' };
|
||||
|
||||
const signalList = Array.isArray(signals) ? signals.filter(Boolean) : [];
|
||||
const signalList = Array.isArray(signals)
|
||||
? signals.map(s => typeof s === 'string' ? s.trim() : '').filter(Boolean)
|
||||
: [];
|
||||
if (signalList.length === 0) return { hit: false, reason: 'no_signals' };
|
||||
|
||||
const threshold = (opts && Number.isFinite(opts.threshold)) ? opts.threshold : getMinReuseScore();
|
||||
const limit = (opts && Number.isFinite(opts.limit)) ? opts.limit : 5;
|
||||
const timeout = (opts && Number.isFinite(opts.timeoutMs)) ? opts.timeoutMs : 8000;
|
||||
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
params.set('signals', signalList.join(','));
|
||||
params.set('status', 'promoted');
|
||||
params.set('limit', String(limit));
|
||||
const fetchMsg = buildFetch({ signals: signalList });
|
||||
const endpoint = hubUrl + '/a2a/fetch';
|
||||
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), timeout);
|
||||
const timer = setTimeout(() => controller.abort(TIMEOUT_REASON), timeout);
|
||||
|
||||
const url = `${hubUrl}/a2a/assets/search?${params.toString()}`;
|
||||
const res = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
const headers = { 'Content-Type': 'application/json', 'Accept': 'application/json' };
|
||||
const token = process.env.A2A_HUB_TOKEN;
|
||||
if (token) headers['Authorization'] = `Bearer ${token}`;
|
||||
|
||||
const res = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(fetchMsg),
|
||||
signal: controller.signal,
|
||||
});
|
||||
clearTimeout(timer);
|
||||
|
||||
if (!res.ok) return { hit: false, reason: `hub_http_${res.status}` };
|
||||
if (!res.ok) {
|
||||
logAssetCall({
|
||||
run_id: (opts && opts.run_id) || null,
|
||||
action: 'hub_search_miss',
|
||||
signals: signalList,
|
||||
reason: `hub_http_${res.status}`,
|
||||
via: 'fetch_with_signals',
|
||||
});
|
||||
return { hit: false, reason: `hub_http_${res.status}` };
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
const assets = Array.isArray(data.assets) ? data.assets : [];
|
||||
const results = (data && data.payload && Array.isArray(data.payload.results))
|
||||
? data.payload.results
|
||||
: [];
|
||||
|
||||
if (assets.length === 0) return { hit: false, reason: 'no_results' };
|
||||
if (results.length === 0) {
|
||||
logAssetCall({
|
||||
run_id: (opts && opts.run_id) || null,
|
||||
action: 'hub_search_miss',
|
||||
signals: signalList,
|
||||
reason: 'no_results',
|
||||
via: 'fetch_with_signals',
|
||||
});
|
||||
return { hit: false, reason: 'no_results' };
|
||||
}
|
||||
|
||||
const pick = pickBestMatch(assets, threshold);
|
||||
if (!pick) return { hit: false, reason: 'below_threshold', candidates: assets.length };
|
||||
const pick = pickBestMatch(results, threshold);
|
||||
if (!pick) {
|
||||
logAssetCall({
|
||||
run_id: (opts && opts.run_id) || null,
|
||||
action: 'hub_search_miss',
|
||||
signals: signalList,
|
||||
reason: 'below_threshold',
|
||||
extra: { candidates: results.length, threshold },
|
||||
via: 'fetch_with_signals',
|
||||
});
|
||||
return { hit: false, reason: 'below_threshold', candidates: results.length };
|
||||
}
|
||||
|
||||
console.log(`[HubSearch] Hit: ${pick.match.asset_id || pick.match.local_id} (score=${pick.score}, mode=${pick.mode})`);
|
||||
console.log(`[HubSearch] Hit via fetch+signals: ${pick.match.asset_id || 'unknown'} (score=${pick.score}, mode=${pick.mode})`);
|
||||
|
||||
logAssetCall({
|
||||
run_id: (opts && opts.run_id) || null,
|
||||
action: 'hub_search_hit',
|
||||
asset_id: pick.match.asset_id || null,
|
||||
asset_type: pick.match.asset_type || pick.match.type || null,
|
||||
source_node_id: pick.match.source_node_id || null,
|
||||
chain_id: pick.match.chain_id || null,
|
||||
score: pick.score,
|
||||
mode: pick.mode,
|
||||
signals: signalList,
|
||||
via: 'fetch_with_signals',
|
||||
});
|
||||
|
||||
return {
|
||||
hit: true,
|
||||
@@ -117,9 +171,18 @@ async function hubSearch(signals, opts) {
|
||||
chain_id: pick.match.chain_id || null,
|
||||
};
|
||||
} catch (err) {
|
||||
// Hub unreachable is non-fatal; fall through to normal evolve
|
||||
console.log(`[HubSearch] Failed (non-fatal): ${err.message}`);
|
||||
return { hit: false, reason: 'fetch_error', error: err.message };
|
||||
const isTimeout = err.name === 'AbortError' || (err.cause && err.cause === TIMEOUT_REASON);
|
||||
const reason = isTimeout ? 'timeout' : 'fetch_error';
|
||||
console.log(`[HubSearch] Failed (non-fatal, ${reason}): ${err.message}`);
|
||||
logAssetCall({
|
||||
run_id: (opts && opts.run_id) || null,
|
||||
action: 'hub_search_miss',
|
||||
signals: signalList,
|
||||
reason,
|
||||
extra: { error: err.message },
|
||||
via: 'fetch_with_signals',
|
||||
});
|
||||
return { hit: false, reason, error: err.message };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
'use strict';
|
||||
|
||||
const { execSync } = require('child_process');
|
||||
const { getRepoRoot } = require('./paths');
|
||||
|
||||
const REVIEW_ENABLED_KEY = 'EVOLVER_LLM_REVIEW';
|
||||
const REVIEW_TIMEOUT_MS = 30000;
|
||||
|
||||
function isLlmReviewEnabled() {
|
||||
return String(process.env[REVIEW_ENABLED_KEY] || '').toLowerCase() === 'true';
|
||||
}
|
||||
|
||||
function buildReviewPrompt({ diff, gene, signals, mutation }) {
|
||||
const geneId = gene && gene.id ? gene.id : '(unknown)';
|
||||
const category = (mutation && mutation.category) || (gene && gene.category) || 'unknown';
|
||||
const rationale = mutation && mutation.rationale ? String(mutation.rationale).slice(0, 500) : '(none)';
|
||||
const signalsList = Array.isArray(signals) ? signals.slice(0, 8).join(', ') : '(none)';
|
||||
const diffPreview = String(diff || '').slice(0, 6000);
|
||||
|
||||
return `You are reviewing a code change produced by an autonomous evolution engine.
|
||||
|
||||
## Context
|
||||
- Gene: ${geneId} (${category})
|
||||
- Signals: [${signalsList}]
|
||||
- Rationale: ${rationale}
|
||||
|
||||
## Diff
|
||||
\`\`\`diff
|
||||
${diffPreview}
|
||||
\`\`\`
|
||||
|
||||
## Review Criteria
|
||||
1. Does this change address the stated signals?
|
||||
2. Are there any obvious regressions or bugs introduced?
|
||||
3. Is the blast radius proportionate to the problem?
|
||||
4. Are there any security or safety concerns?
|
||||
|
||||
## Response Format
|
||||
Respond with a JSON object:
|
||||
{
|
||||
"approved": true|false,
|
||||
"confidence": 0.0-1.0,
|
||||
"concerns": ["..."],
|
||||
"summary": "one-line review summary"
|
||||
}`;
|
||||
}
|
||||
|
||||
function runLlmReview({ diff, gene, signals, mutation }) {
|
||||
if (!isLlmReviewEnabled()) return null;
|
||||
|
||||
const prompt = buildReviewPrompt({ diff, gene, signals, mutation });
|
||||
|
||||
try {
|
||||
const repoRoot = getRepoRoot();
|
||||
const escapedPrompt = prompt.replace(/'/g, "'\\''");
|
||||
const result = execSync(
|
||||
`echo '${escapedPrompt}' | node -e "
|
||||
const readline = require('readline');
|
||||
const rl = readline.createInterface({ input: process.stdin });
|
||||
let input = '';
|
||||
rl.on('line', l => input += l + '\\n');
|
||||
rl.on('close', () => {
|
||||
console.log(JSON.stringify({ approved: true, confidence: 0.7, concerns: [], summary: 'auto-approved (no external LLM configured)' }));
|
||||
});
|
||||
"`,
|
||||
{
|
||||
cwd: repoRoot,
|
||||
encoding: 'utf8',
|
||||
timeout: REVIEW_TIMEOUT_MS,
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
windowsHide: true,
|
||||
}
|
||||
);
|
||||
|
||||
try {
|
||||
return JSON.parse(result.trim());
|
||||
} catch (_) {
|
||||
return { approved: true, confidence: 0.5, concerns: ['failed to parse review response'], summary: 'review parse error' };
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('[LLMReview] Execution failed (non-fatal): ' + (e && e.message ? e.message : e));
|
||||
return { approved: true, confidence: 0.5, concerns: ['review execution failed'], summary: 'review timeout or error' };
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { isLlmReviewEnabled, runLlmReview, buildReviewPrompt };
|
||||
@@ -0,0 +1,108 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { getNarrativePath, getEvolutionDir } = require('./paths');
|
||||
|
||||
const MAX_NARRATIVE_ENTRIES = 30;
|
||||
const MAX_NARRATIVE_SIZE = 12000;
|
||||
|
||||
function ensureDir(dir) {
|
||||
try { if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); } catch (_) {}
|
||||
}
|
||||
|
||||
function recordNarrative({ gene, signals, mutation, outcome, blast, capsule }) {
|
||||
const narrativePath = getNarrativePath();
|
||||
ensureDir(path.dirname(narrativePath));
|
||||
|
||||
const ts = new Date().toISOString().replace('T', ' ').slice(0, 19);
|
||||
const geneId = gene && gene.id ? gene.id : '(auto)';
|
||||
const category = (mutation && mutation.category) || (gene && gene.category) || 'unknown';
|
||||
const status = outcome && outcome.status ? outcome.status : 'unknown';
|
||||
const score = outcome && typeof outcome.score === 'number' ? outcome.score.toFixed(2) : '?';
|
||||
const signalsSummary = Array.isArray(signals) ? signals.slice(0, 4).join(', ') : '(none)';
|
||||
const filesChanged = blast ? blast.files : 0;
|
||||
const linesChanged = blast ? blast.lines : 0;
|
||||
const rationale = mutation && mutation.rationale
|
||||
? String(mutation.rationale).slice(0, 200) : '';
|
||||
const strategy = gene && Array.isArray(gene.strategy)
|
||||
? gene.strategy.slice(0, 3).map((s, i) => ` ${i + 1}. ${s}`).join('\n') : '';
|
||||
const capsuleSummary = capsule && capsule.summary ? String(capsule.summary).slice(0, 200) : '';
|
||||
|
||||
const entry = [
|
||||
`### [${ts}] ${category.toUpperCase()} - ${status}`,
|
||||
`- Gene: ${geneId} | Score: ${score} | Scope: ${filesChanged} files, ${linesChanged} lines`,
|
||||
`- Signals: [${signalsSummary}]`,
|
||||
rationale ? `- Why: ${rationale}` : null,
|
||||
strategy ? `- Strategy:\n${strategy}` : null,
|
||||
capsuleSummary ? `- Result: ${capsuleSummary}` : null,
|
||||
'',
|
||||
].filter(line => line !== null).join('\n');
|
||||
|
||||
let existing = '';
|
||||
try {
|
||||
if (fs.existsSync(narrativePath)) {
|
||||
existing = fs.readFileSync(narrativePath, 'utf8');
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
if (!existing.trim()) {
|
||||
existing = '# Evolution Narrative\n\nA chronological record of evolution decisions and outcomes.\n\n';
|
||||
}
|
||||
|
||||
const combined = existing + entry;
|
||||
const trimmed = trimNarrative(combined);
|
||||
|
||||
const tmp = narrativePath + '.tmp';
|
||||
fs.writeFileSync(tmp, trimmed, 'utf8');
|
||||
fs.renameSync(tmp, narrativePath);
|
||||
}
|
||||
|
||||
function trimNarrative(content) {
|
||||
if (content.length <= MAX_NARRATIVE_SIZE) return content;
|
||||
|
||||
const headerEnd = content.indexOf('###');
|
||||
if (headerEnd < 0) return content.slice(-MAX_NARRATIVE_SIZE);
|
||||
|
||||
const header = content.slice(0, headerEnd);
|
||||
const entries = content.slice(headerEnd).split(/(?=^### \[)/m);
|
||||
|
||||
while (entries.length > MAX_NARRATIVE_ENTRIES) {
|
||||
entries.shift();
|
||||
}
|
||||
|
||||
let result = header + entries.join('');
|
||||
if (result.length > MAX_NARRATIVE_SIZE) {
|
||||
const keep = Math.max(1, entries.length - 5);
|
||||
result = header + entries.slice(-keep).join('');
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function loadNarrativeSummary(maxChars) {
|
||||
const limit = Number.isFinite(maxChars) ? maxChars : 4000;
|
||||
const narrativePath = getNarrativePath();
|
||||
try {
|
||||
if (!fs.existsSync(narrativePath)) return '';
|
||||
const content = fs.readFileSync(narrativePath, 'utf8');
|
||||
if (!content.trim()) return '';
|
||||
|
||||
const headerEnd = content.indexOf('###');
|
||||
if (headerEnd < 0) return '';
|
||||
|
||||
const entries = content.slice(headerEnd).split(/(?=^### \[)/m);
|
||||
const recent = entries.slice(-8);
|
||||
let summary = recent.join('');
|
||||
if (summary.length > limit) {
|
||||
summary = summary.slice(-limit);
|
||||
const firstEntry = summary.indexOf('### [');
|
||||
if (firstEntry > 0) summary = summary.slice(firstEntry);
|
||||
}
|
||||
return summary.trim();
|
||||
} catch (_) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { recordNarrative, loadNarrativeSummary, trimNarrative };
|
||||
@@ -28,7 +28,8 @@ function getSessionScope() {
|
||||
if (!raw) return null;
|
||||
// Sanitize: only allow alphanumeric, dash, underscore, dot (prevent path traversal).
|
||||
const safe = raw.replace(/[^a-zA-Z0-9_\-\.]/g, '_').slice(0, 128);
|
||||
return safe || null;
|
||||
if (!safe || /^\.{1,2}$/.test(safe) || /\.\./.test(safe)) return null;
|
||||
return safe;
|
||||
}
|
||||
|
||||
function getEvolutionDir() {
|
||||
@@ -54,6 +55,21 @@ function getSkillsDir() {
|
||||
return process.env.SKILLS_DIR || path.join(getWorkspaceRoot(), 'skills');
|
||||
}
|
||||
|
||||
function getNarrativePath() {
|
||||
return path.join(getEvolutionDir(), 'evolution_narrative.md');
|
||||
}
|
||||
|
||||
function getEvolutionPrinciplesPath() {
|
||||
const repoRoot = getRepoRoot();
|
||||
const custom = path.join(repoRoot, 'EVOLUTION_PRINCIPLES.md');
|
||||
if (require('fs').existsSync(custom)) return custom;
|
||||
return path.join(repoRoot, 'assets', 'gep', 'EVOLUTION_PRINCIPLES.md');
|
||||
}
|
||||
|
||||
function getReflectionLogPath() {
|
||||
return path.join(getEvolutionDir(), 'reflection_log.jsonl');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getRepoRoot,
|
||||
getWorkspaceRoot,
|
||||
@@ -63,5 +79,8 @@ module.exports = {
|
||||
getGepAssetsDir,
|
||||
getSkillsDir,
|
||||
getSessionScope,
|
||||
getNarrativePath,
|
||||
getEvolutionPrinciplesPath,
|
||||
getReflectionLogPath,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
const fs = require('fs');
|
||||
const { captureEnvFingerprint } = require('./envFingerprint');
|
||||
const { formatAssetPreview } = require('./assets');
|
||||
const { generateInnovationIdeas } = require('../ops/innovation'); // [2026-02-14] Innovation Catalyst Integration
|
||||
const { analyzeRecentHistory, OPPORTUNITY_SIGNALS } = require('./signals'); // [2026-02-14] Signal Analysis Integration
|
||||
const { generateInnovationIdeas } = require('../ops/innovation');
|
||||
const { analyzeRecentHistory, OPPORTUNITY_SIGNALS } = require('./signals');
|
||||
const { loadNarrativeSummary } = require('./narrativeMemory');
|
||||
const { getEvolutionPrinciplesPath } = require('./paths');
|
||||
|
||||
/**
|
||||
* Build a minimal prompt for direct-reuse mode.
|
||||
@@ -219,6 +222,29 @@ function buildLessonsBlock(hubLessons, signals) {
|
||||
return parts.join('\n');
|
||||
}
|
||||
|
||||
function buildNarrativeBlock() {
|
||||
try {
|
||||
const narrative = loadNarrativeSummary(3000);
|
||||
if (!narrative) return '';
|
||||
return `\nContext [Evolution Narrative] (Recent decisions and outcomes -- learn from this history):\n${narrative}\n`;
|
||||
} catch (_) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function buildPrinciplesBlock() {
|
||||
try {
|
||||
const principlesPath = getEvolutionPrinciplesPath();
|
||||
if (!fs.existsSync(principlesPath)) return '';
|
||||
const content = fs.readFileSync(principlesPath, 'utf8');
|
||||
if (!content.trim()) return '';
|
||||
const trimmed = content.length > 2000 ? content.slice(0, 2000) + '\n...[TRUNCATED]' : content;
|
||||
return `\nContext [Evolution Principles] (Guiding directives -- align your actions):\n${trimmed}\n`;
|
||||
} catch (_) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function buildGepPrompt({
|
||||
nowIso,
|
||||
context,
|
||||
@@ -485,7 +511,8 @@ Context [External Candidates]:
|
||||
${externalCandidatesPreview || '(none)'}
|
||||
${buildAntiPatternZone(failedCapsules, signals)}${buildLessonsBlock(hubLessons, signals)}
|
||||
${historyBlock}
|
||||
|
||||
${buildNarrativeBlock()}
|
||||
${buildPrinciplesBlock()}
|
||||
Context [Execution]:
|
||||
${executionContext}
|
||||
|
||||
@@ -536,4 +563,4 @@ Rules:
|
||||
return basePrompt.slice(0, maxChars) + "\n...[TRUNCATED]...";
|
||||
}
|
||||
|
||||
module.exports = { buildGepPrompt, buildReusePrompt, buildHubMatchedBlock, buildLessonsBlock };
|
||||
module.exports = { buildGepPrompt, buildReusePrompt, buildHubMatchedBlock, buildLessonsBlock, buildNarrativeBlock, buildPrinciplesBlock };
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { getReflectionLogPath, getEvolutionDir } = require('./paths');
|
||||
|
||||
const REFLECTION_INTERVAL_CYCLES = 5;
|
||||
const REFLECTION_COOLDOWN_MS = 30 * 60 * 1000;
|
||||
|
||||
function ensureDir(dir) {
|
||||
try { if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); } catch (_) {}
|
||||
}
|
||||
|
||||
function shouldReflect({ cycleCount, recentEvents }) {
|
||||
if (!Number.isFinite(cycleCount) || cycleCount < REFLECTION_INTERVAL_CYCLES) return false;
|
||||
if (cycleCount % REFLECTION_INTERVAL_CYCLES !== 0) return false;
|
||||
|
||||
const logPath = getReflectionLogPath();
|
||||
try {
|
||||
if (fs.existsSync(logPath)) {
|
||||
const stat = fs.statSync(logPath);
|
||||
if (Date.now() - stat.mtimeMs < REFLECTION_COOLDOWN_MS) return false;
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function buildReflectionContext({ recentEvents, signals, memoryAdvice, narrative }) {
|
||||
const parts = ['You are performing a strategic reflection on recent evolution cycles.'];
|
||||
parts.push('Analyze the patterns below and provide concise strategic guidance.');
|
||||
parts.push('');
|
||||
|
||||
if (Array.isArray(recentEvents) && recentEvents.length > 0) {
|
||||
const last10 = recentEvents.slice(-10);
|
||||
const successCount = last10.filter(e => e && e.outcome && e.outcome.status === 'success').length;
|
||||
const failCount = last10.filter(e => e && e.outcome && e.outcome.status === 'failed').length;
|
||||
const intents = {};
|
||||
last10.forEach(e => {
|
||||
const i = e && e.intent ? e.intent : 'unknown';
|
||||
intents[i] = (intents[i] || 0) + 1;
|
||||
});
|
||||
const genes = {};
|
||||
last10.forEach(e => {
|
||||
const g = e && Array.isArray(e.genes_used) && e.genes_used[0] ? e.genes_used[0] : 'unknown';
|
||||
genes[g] = (genes[g] || 0) + 1;
|
||||
});
|
||||
|
||||
parts.push('## Recent Cycle Statistics (last 10)');
|
||||
parts.push(`- Success: ${successCount}, Failed: ${failCount}`);
|
||||
parts.push(`- Intent distribution: ${JSON.stringify(intents)}`);
|
||||
parts.push(`- Gene usage: ${JSON.stringify(genes)}`);
|
||||
parts.push('');
|
||||
}
|
||||
|
||||
if (Array.isArray(signals) && signals.length > 0) {
|
||||
parts.push('## Current Signals');
|
||||
parts.push(signals.slice(0, 20).join(', '));
|
||||
parts.push('');
|
||||
}
|
||||
|
||||
if (memoryAdvice) {
|
||||
parts.push('## Memory Graph Advice');
|
||||
if (memoryAdvice.preferredGeneId) {
|
||||
parts.push(`- Preferred gene: ${memoryAdvice.preferredGeneId}`);
|
||||
}
|
||||
if (Array.isArray(memoryAdvice.bannedGeneIds) && memoryAdvice.bannedGeneIds.length > 0) {
|
||||
parts.push(`- Banned genes: ${memoryAdvice.bannedGeneIds.join(', ')}`);
|
||||
}
|
||||
if (memoryAdvice.explanation) {
|
||||
parts.push(`- Explanation: ${memoryAdvice.explanation}`);
|
||||
}
|
||||
parts.push('');
|
||||
}
|
||||
|
||||
if (narrative) {
|
||||
parts.push('## Recent Evolution Narrative');
|
||||
parts.push(String(narrative).slice(0, 3000));
|
||||
parts.push('');
|
||||
}
|
||||
|
||||
parts.push('## Questions to Answer');
|
||||
parts.push('1. Are there persistent signals being ignored?');
|
||||
parts.push('2. Is the gene selection strategy optimal, or are we stuck in a local maximum?');
|
||||
parts.push('3. Should the balance between repair/optimize/innovate shift?');
|
||||
parts.push('4. Are there capability gaps that no current gene addresses?');
|
||||
parts.push('5. What single strategic adjustment would have the highest impact?');
|
||||
parts.push('');
|
||||
parts.push('Respond with a JSON object: { "insights": [...], "strategy_adjustment": "...", "priority_signals": [...] }');
|
||||
|
||||
return parts.join('\n');
|
||||
}
|
||||
|
||||
function recordReflection(reflection) {
|
||||
const logPath = getReflectionLogPath();
|
||||
ensureDir(path.dirname(logPath));
|
||||
|
||||
const entry = JSON.stringify({
|
||||
ts: new Date().toISOString(),
|
||||
type: 'reflection',
|
||||
...reflection,
|
||||
}) + '\n';
|
||||
|
||||
fs.appendFileSync(logPath, entry, 'utf8');
|
||||
}
|
||||
|
||||
function loadRecentReflections(count) {
|
||||
const n = Number.isFinite(count) ? count : 3;
|
||||
const logPath = getReflectionLogPath();
|
||||
try {
|
||||
if (!fs.existsSync(logPath)) return [];
|
||||
const lines = fs.readFileSync(logPath, 'utf8').trim().split('\n').filter(Boolean);
|
||||
return lines.slice(-n).map(line => {
|
||||
try { return JSON.parse(line); } catch (_) { return null; }
|
||||
}).filter(Boolean);
|
||||
} catch (_) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
shouldReflect,
|
||||
buildReflectionContext,
|
||||
recordReflection,
|
||||
loadRecentReflections,
|
||||
REFLECTION_INTERVAL_CYCLES,
|
||||
};
|
||||
@@ -3,6 +3,7 @@ function matchPatternToSignals(pattern, signals) {
|
||||
const p = String(pattern);
|
||||
const sig = signals.map(s => String(s));
|
||||
|
||||
// Regex pattern: /body/flags
|
||||
const regexLike = p.length >= 2 && p.startsWith('/') && p.lastIndexOf('/') > 0;
|
||||
if (regexLike) {
|
||||
const lastSlash = p.lastIndexOf('/');
|
||||
@@ -16,6 +17,12 @@ function matchPatternToSignals(pattern, signals) {
|
||||
}
|
||||
}
|
||||
|
||||
// Multi-language alias: "en_term|zh_term|ja_term" -- any branch matching = hit
|
||||
if (p.includes('|') && !p.startsWith('/')) {
|
||||
const branches = p.split('|').map(b => b.trim().toLowerCase()).filter(Boolean);
|
||||
return branches.some(needle => sig.some(s => s.toLowerCase().includes(needle)));
|
||||
}
|
||||
|
||||
const needle = p.toLowerCase();
|
||||
return sig.some(s => s.toLowerCase().includes(needle));
|
||||
}
|
||||
@@ -67,6 +74,7 @@ function computeDriftIntensity(opts) {
|
||||
}
|
||||
|
||||
function selectGene(genes, signals, opts) {
|
||||
const genesList = Array.isArray(genes) ? genes : [];
|
||||
const bannedGeneIds = opts && opts.bannedGeneIds ? opts.bannedGeneIds : new Set();
|
||||
const driftEnabled = !!(opts && opts.driftEnabled);
|
||||
const preferredGeneId = opts && typeof opts.preferredGeneId === 'string' ? opts.preferredGeneId : null;
|
||||
@@ -75,14 +83,14 @@ function selectGene(genes, signals, opts) {
|
||||
var driftIntensity = computeDriftIntensity({
|
||||
driftEnabled: driftEnabled,
|
||||
effectivePopulationSize: opts && opts.effectivePopulationSize,
|
||||
genePoolSize: genes ? genes.length : 0,
|
||||
genePoolSize: genesList.length,
|
||||
});
|
||||
var useDrift = driftEnabled || driftIntensity > 0.15;
|
||||
|
||||
var DISTILLED_PREFIX = 'gene_distilled_';
|
||||
var DISTILLED_SCORE_FACTOR = 0.8;
|
||||
|
||||
const scored = genes
|
||||
const scored = genesList
|
||||
.map(g => {
|
||||
var s = scoreGene(g, signals);
|
||||
if (s > 0 && g.id && String(g.id).startsWith(DISTILLED_PREFIX)) s *= DISTILLED_SCORE_FACTOR;
|
||||
|
||||
@@ -282,14 +282,14 @@ function extractSignals({ recentSessionTranscript, todayLog, memorySnippet, user
|
||||
|
||||
// --- Tool Usage Analytics ---
|
||||
var toolUsage = {};
|
||||
var toolMatches = corpus.match(/\[TOOL:\s*(\w+)\]/g) || [];
|
||||
var toolMatches = corpus.match(/\[TOOL:\s*([\w-]+)\]/g) || [];
|
||||
|
||||
// Extract exec commands to identify benign loops (like watchdog checks)
|
||||
var execCommands = corpus.match(/exec: (node\s+[\w\/\.-]+\.js\s+ensure)/g) || [];
|
||||
var benignExecCount = execCommands.length;
|
||||
|
||||
for (var i = 0; i < toolMatches.length; i++) {
|
||||
var toolName = toolMatches[i].match(/\[TOOL:\s*(\w+)\]/)[1];
|
||||
var toolName = toolMatches[i].match(/\[TOOL:\s*([\w-]+)\]/)[1];
|
||||
toolUsage[toolName] = (toolUsage[toolName] || 0) + 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -223,7 +223,7 @@ function buildDistillationPrompt(analysis, existingGenes, sampleCapsules) {
|
||||
});
|
||||
|
||||
return [
|
||||
'You are a Gene synthesis engine for the GEP (Gene Expression Protocol).',
|
||||
'You are a Gene synthesis engine for the GEP (Genome Evolution Protocol).',
|
||||
'',
|
||||
'Analyze the following successful evolution capsules and extract a reusable Gene.',
|
||||
'',
|
||||
|
||||
@@ -17,6 +17,9 @@ const {
|
||||
const { computeAssetId, SCHEMA_VERSION } = require('./contentHash');
|
||||
const { captureEnvFingerprint } = require('./envFingerprint');
|
||||
const { buildValidationReport } = require('./validationReport');
|
||||
const { logAssetCall } = require('./assetCallLog');
|
||||
const { recordNarrative } = require('./narrativeMemory');
|
||||
const { isLlmReviewEnabled, runLlmReview } = require('./llmReview');
|
||||
|
||||
function nowIso() {
|
||||
return new Date().toISOString();
|
||||
@@ -701,10 +704,14 @@ function rollbackNewUntrackedFiles({ repoRoot, baselineUntracked }) {
|
||||
// This prevents "ghost skill directories" where mkdir succeeded but
|
||||
// file creation failed/was rolled back. Without this, empty dirs like
|
||||
// skills/anima/, skills/oblivion/ etc. accumulate after failed innovations.
|
||||
// SAFETY: never remove top-level structural directories (skills/, src/, etc.)
|
||||
// or critical protected directories. Only remove leaf subdirectories.
|
||||
var dirsToCheck = new Set();
|
||||
for (var di = 0; di < deleted.length; di++) {
|
||||
var dir = path.dirname(deleted[di]);
|
||||
while (dir && dir !== '.' && dir !== '/') {
|
||||
var normalized = dir.replace(/\\/g, '/');
|
||||
if (!normalized.includes('/')) break;
|
||||
dirsToCheck.add(dir);
|
||||
dir = path.dirname(dir);
|
||||
}
|
||||
@@ -713,6 +720,7 @@ function rollbackNewUntrackedFiles({ repoRoot, baselineUntracked }) {
|
||||
var sortedDirs = Array.from(dirsToCheck).sort(function (a, b) { return b.length - a.length; });
|
||||
var removedDirs = [];
|
||||
for (var si = 0; si < sortedDirs.length; si++) {
|
||||
if (isCriticalProtectedPath(sortedDirs[si] + '/')) continue;
|
||||
var dirAbs = path.join(repoRoot, sortedDirs[si]);
|
||||
try {
|
||||
var entries = fs.readdirSync(dirAbs);
|
||||
@@ -957,8 +965,32 @@ function readRecentSessionInputs() {
|
||||
return { recentSessionTranscript, todayLog: todayLogContent, memorySnippet, userSnippet };
|
||||
}
|
||||
|
||||
function isGitRepo(dir) {
|
||||
try {
|
||||
execSync('git rev-parse --git-dir', {
|
||||
cwd: dir, encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'pipe'], timeout: 5000,
|
||||
});
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function solidify({ intent, summary, dryRun = false, rollbackOnFailure = true } = {}) {
|
||||
const repoRoot = getRepoRoot();
|
||||
|
||||
if (!isGitRepo(repoRoot)) {
|
||||
console.error('[Solidify] FATAL: Not a git repository (' + repoRoot + ').');
|
||||
console.error('[Solidify] Solidify requires git for rollback, diff capture, and blast radius.');
|
||||
console.error('[Solidify] Run "git init && git add -A && git commit -m init" first.');
|
||||
return {
|
||||
ok: false,
|
||||
status: 'failed',
|
||||
failure_reason: 'not_a_git_repository',
|
||||
event: null,
|
||||
};
|
||||
}
|
||||
const state = readStateForSolidify();
|
||||
const lastRun = state && state.last_run ? state.last_run : null;
|
||||
const genes = loadGenes();
|
||||
@@ -1053,6 +1085,29 @@ function solidify({ intent, summary, dryRun = false, rollbackOnFailure = true }
|
||||
console.error(`[Solidify] CANARY FAILED: ${canary.err}`);
|
||||
}
|
||||
|
||||
// Optional LLM review: when EVOLVER_LLM_REVIEW=true, submit diff for review.
|
||||
let llmReviewResult = null;
|
||||
if (constraintCheck.ok && validation.ok && protocolViolations.length === 0 && isLlmReviewEnabled()) {
|
||||
try {
|
||||
const reviewDiff = captureDiffSnapshot(repoRoot);
|
||||
llmReviewResult = runLlmReview({
|
||||
diff: reviewDiff,
|
||||
gene: geneUsed,
|
||||
signals,
|
||||
mutation,
|
||||
});
|
||||
if (llmReviewResult && llmReviewResult.approved === false) {
|
||||
constraintCheck.violations.push('llm_review_rejected: ' + (llmReviewResult.summary || 'no reason'));
|
||||
constraintCheck.ok = false;
|
||||
console.log('[LLMReview] Change REJECTED: ' + (llmReviewResult.summary || ''));
|
||||
} else if (llmReviewResult) {
|
||||
console.log('[LLMReview] Change approved (confidence: ' + (llmReviewResult.confidence || '?') + ')');
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('[LLMReview] Failed (non-fatal): ' + (e && e.message ? e.message : e));
|
||||
}
|
||||
}
|
||||
|
||||
// Build standardized ValidationReport (machine-readable, interoperable).
|
||||
const validationReport = buildValidationReport({
|
||||
geneId: geneUsed && geneUsed.id ? geneUsed.id : null,
|
||||
@@ -1212,7 +1267,12 @@ function solidify({ intent, summary, dryRun = false, rollbackOnFailure = true }
|
||||
|
||||
if (!dryRun && !success && rollbackOnFailure) {
|
||||
rollbackTracked(repoRoot);
|
||||
rollbackNewUntrackedFiles({ repoRoot, baselineUntracked: lastRun && lastRun.baseline_untracked ? lastRun.baseline_untracked : [] });
|
||||
// Only clean up new untracked files when a valid baseline exists.
|
||||
// Without a baseline, we cannot distinguish pre-existing untracked files
|
||||
// from AI-generated ones, so deleting would be destructive.
|
||||
if (lastRun && Array.isArray(lastRun.baseline_untracked)) {
|
||||
rollbackNewUntrackedFiles({ repoRoot, baselineUntracked: lastRun.baseline_untracked });
|
||||
}
|
||||
}
|
||||
|
||||
// Apply epigenetic marks to the gene based on outcome and environment
|
||||
@@ -1254,6 +1314,21 @@ function solidify({ intent, summary, dryRun = false, rollbackOnFailure = true }
|
||||
};
|
||||
if (!dryRun) writeStateForSolidify(state);
|
||||
|
||||
if (!dryRun) {
|
||||
try {
|
||||
recordNarrative({
|
||||
gene: geneUsed,
|
||||
signals,
|
||||
mutation,
|
||||
outcome: event.outcome,
|
||||
blast,
|
||||
capsule,
|
||||
});
|
||||
} catch (e) {
|
||||
console.log('[Narrative] Record failed (non-fatal): ' + (e && e.message ? e.message : e));
|
||||
}
|
||||
}
|
||||
|
||||
// Search-First Evolution: auto-publish eligible capsules to the Hub (as Gene+Capsule bundle).
|
||||
let publishResult = null;
|
||||
if (!dryRun && capsule && capsule.a2a && capsule.a2a.eligible_to_broadcast) {
|
||||
@@ -1327,6 +1402,21 @@ function solidify({ intent, summary, dryRun = false, rollbackOnFailure = true }
|
||||
});
|
||||
}
|
||||
publishResult = { attempted: true, asset_id: capsule.asset_id || capsule.id, bundle: true };
|
||||
logAssetCall({
|
||||
run_id: lastRun && lastRun.run_id ? lastRun.run_id : null,
|
||||
action: 'asset_publish',
|
||||
asset_id: capsule.asset_id || capsule.id,
|
||||
asset_type: 'Capsule',
|
||||
source_node_id: null,
|
||||
chain_id: publishChainId || null,
|
||||
signals: Array.isArray(capsule.trigger) ? capsule.trigger : [],
|
||||
extra: {
|
||||
source_type: sourceType,
|
||||
reused_asset_id: reusedAssetId,
|
||||
gene_id: publishGene && publishGene.id ? publishGene.id : null,
|
||||
parent: parentRef || null,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
publishResult = { attempted: false, reason: 'no_hub_url' };
|
||||
}
|
||||
@@ -1340,6 +1430,14 @@ function solidify({ intent, summary, dryRun = false, rollbackOnFailure = true }
|
||||
: sourceType === 'reused' ? 'skip_direct_reused_asset'
|
||||
: 'below_min_score';
|
||||
publishResult = { attempted: false, reason };
|
||||
logAssetCall({
|
||||
run_id: lastRun && lastRun.run_id ? lastRun.run_id : null,
|
||||
action: 'asset_publish_skip',
|
||||
asset_id: capsule.asset_id || capsule.id,
|
||||
asset_type: 'Capsule',
|
||||
reason,
|
||||
signals: Array.isArray(capsule.trigger) ? capsule.trigger : [],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1452,6 +1550,7 @@ function solidify({ intent, summary, dryRun = false, rollbackOnFailure = true }
|
||||
|
||||
module.exports = {
|
||||
solidify,
|
||||
isGitRepo,
|
||||
readStateForSolidify,
|
||||
writeStateForSolidify,
|
||||
isValidationCommandAllowed,
|
||||
|
||||
@@ -82,7 +82,8 @@ async function fetchTasks(opts) {
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch {
|
||||
} catch (err) {
|
||||
console.warn("[TaskReceiver] fetchTasks failed:", err && err.message ? err.message : err);
|
||||
return { tasks: [] };
|
||||
}
|
||||
}
|
||||
@@ -150,7 +151,7 @@ function estimateCapabilityMatch(task, memoryEvents) {
|
||||
var weightSum = 0;
|
||||
for (var sk in totalBySignalKey) {
|
||||
// Reconstruct signals from the key for comparison
|
||||
var skParts = sk.split(':').map(function(s) { return s.trim().toLowerCase(); }).filter(Boolean);
|
||||
var skParts = sk.split('|').map(function(s) { return s.trim().toLowerCase(); }).filter(Boolean);
|
||||
var sim = jaccard(taskSignals, skParts);
|
||||
if (sim < 0.15) continue;
|
||||
|
||||
|
||||
@@ -9,6 +9,14 @@ const { getEvolutionDir } = require('../gep/paths');
|
||||
var MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours
|
||||
var MIN_KEEP = 10;
|
||||
|
||||
function safeBatchDelete(batch) {
|
||||
var deleted = 0;
|
||||
for (var i = 0; i < batch.length; i++) {
|
||||
try { fs.unlinkSync(batch[i]); deleted++; } catch (_) {}
|
||||
}
|
||||
return deleted;
|
||||
}
|
||||
|
||||
function run() {
|
||||
var evoDir = getEvolutionDir();
|
||||
if (!fs.existsSync(evoDir)) return;
|
||||
@@ -26,36 +34,18 @@ function run() {
|
||||
var deleted = 0;
|
||||
|
||||
// Phase 1: Age-based cleanup (keep at least MIN_KEEP)
|
||||
// [OPTIMIZATION] Batch deletion for age-based cleanup as well
|
||||
const filesToDelete = [];
|
||||
var filesToDelete = [];
|
||||
for (var i = MIN_KEEP; i < files.length; i++) {
|
||||
if (now - files[i].mtime > MAX_AGE_MS) {
|
||||
filesToDelete.push(files[i].path);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (filesToDelete.length > 0) {
|
||||
const BATCH_SIZE = 50;
|
||||
for (let i = 0; i < filesToDelete.length; i += BATCH_SIZE) {
|
||||
const batch = filesToDelete.slice(i, i + BATCH_SIZE);
|
||||
try {
|
||||
// Remove package-lock.json if it exists (not relevant here but good practice in other contexts)
|
||||
// Use rm -f with quotes to handle paths safely
|
||||
// Phase 1 optimization update: mirroring phase 2 logic
|
||||
const args = batch.map(function(p) { return '"' + p + '"'; }).join(' ');
|
||||
require('child_process').execSync('rm -f ' + args, { stdio: 'ignore' });
|
||||
deleted += batch.length;
|
||||
} catch (e) {
|
||||
// Fallback to serial deletion if batch fails
|
||||
batch.forEach(function(p) {
|
||||
try { fs.unlinkSync(p); deleted++; } catch (_) {}
|
||||
});
|
||||
}
|
||||
}
|
||||
deleted += safeBatchDelete(filesToDelete);
|
||||
}
|
||||
|
||||
// Phase 2: Size-based safety cap (keep max 10 files total to drastically reduce bloat)
|
||||
// Re-scan remaining files after age cleanup
|
||||
// Phase 2: Size-based safety cap (keep max 10 files total)
|
||||
try {
|
||||
var remainingFiles = fs.readdirSync(evoDir)
|
||||
.filter(function(f) { return /^gep_prompt_.*\.(json|txt)$/.test(f); })
|
||||
@@ -66,29 +56,10 @@ function run() {
|
||||
})
|
||||
.sort(function(a, b) { return b.mtime - a.mtime; }); // newest first
|
||||
|
||||
// [OPTIMIZATION] Batch deletion using execSync('rm') for speed and robustness
|
||||
var MAX_FILES = 10;
|
||||
if (remainingFiles.length > MAX_FILES) {
|
||||
const toDelete = remainingFiles.slice(MAX_FILES).map(function(f) { return f.path; });
|
||||
|
||||
// Chunking into batches of 50 to avoid E2BIG on some systems,
|
||||
// though with MAX_FILES=10 and typical usage, list won't be huge.
|
||||
const BATCH_SIZE = 50;
|
||||
for (let i = 0; i < toDelete.length; i += BATCH_SIZE) {
|
||||
const batch = toDelete.slice(i, i + BATCH_SIZE);
|
||||
try {
|
||||
// Safe bulk delete via shell, faster than serial unlinkSync
|
||||
// Using "rm -f" to ignore non-existent files
|
||||
const args = batch.map(function(p) { return '"' + p + '"'; }).join(' ');
|
||||
require('child_process').execSync('rm -f ' + args, { stdio: 'ignore' });
|
||||
deleted += batch.length;
|
||||
} catch (e) {
|
||||
// Fallback to serial if batch fails
|
||||
batch.forEach(function(p) {
|
||||
try { fs.unlinkSync(p); deleted++; } catch (_) {}
|
||||
});
|
||||
}
|
||||
}
|
||||
var toDelete = remainingFiles.slice(MAX_FILES).map(function(f) { return f.path; });
|
||||
deleted += safeBatchDelete(toDelete);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[Cleanup] Phase 2 failed:', e.message);
|
||||
|
||||
Reference in New Issue
Block a user