@evomap/evolver 1.87.4 → 1.88.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +848 -33
- package/package.json +1 -1
- package/scripts/build_binaries.js +11 -1
- package/src/adapters/hookAdapter.js +3 -1
- package/src/adapters/scripts/_runtimePaths.js +24 -0
- package/src/adapters/scripts/evolver-session-end.js +110 -78
- package/src/adapters/scripts/evolver-session-start.js +100 -0
- package/src/config.js +43 -8
- package/src/evolve/guards.js +1 -1
- package/src/evolve/pipeline/collect.js +1 -1
- package/src/evolve/pipeline/dispatch.js +1 -1
- package/src/evolve/pipeline/enrich.js +1 -1
- package/src/evolve/pipeline/hub.js +1 -1
- package/src/evolve/pipeline/select.js +1 -1
- package/src/evolve/pipeline/signals.js +1 -1
- package/src/evolve/utils.js +1 -1
- package/src/evolve.js +1 -1
- package/src/forceUpdate.js +42 -21
- package/src/gep/a2aProtocol.js +1 -1
- package/src/gep/assetStore.js +40 -0
- package/src/gep/candidateEval.js +1 -1
- package/src/gep/candidates.js +1 -1
- package/src/gep/contentHash.js +1 -1
- package/src/gep/crypto.js +1 -1
- package/src/gep/curriculum.js +1 -1
- package/src/gep/deviceId.js +1 -1
- package/src/gep/envFingerprint.js +1 -1
- package/src/gep/epigenetics.js +1 -1
- package/src/gep/explore.js +1 -1
- package/src/gep/featureFlags.js +4 -0
- package/src/gep/gitOps.js +7 -2
- package/src/gep/hash.js +1 -1
- package/src/gep/hubFetch.js +1 -1
- package/src/gep/hubReview.js +1 -1
- package/src/gep/hubSearch.js +1 -1
- package/src/gep/hubVerify.js +1 -1
- package/src/gep/idleScheduler.js +78 -0
- package/src/gep/learningSignals.js +1 -1
- package/src/gep/mailboxTransport.js +34 -0
- package/src/gep/memoryGraph.js +1 -1
- package/src/gep/memoryGraphAdapter.js +1 -1
- package/src/gep/mutation.js +1 -1
- package/src/gep/narrativeMemory.js +1 -1
- package/src/gep/openPRRegistry.js +1 -1
- package/src/gep/paths.js +16 -2
- package/src/gep/personality.js +1 -1
- package/src/gep/policyCheck.js +1 -1
- package/src/gep/prompt.js +1 -1
- package/src/gep/recallVerifier.js +1 -1
- package/src/gep/reflection.js +1 -1
- package/src/gep/selector.js +1 -1
- package/src/gep/skillDistiller.js +1 -1
- package/src/gep/solidify.js +1 -1
- package/src/gep/strategy.js +1 -1
- package/src/gep/validator/index.js +46 -1
- package/src/gep/validator/sandboxExecutor.js +10 -1
- package/src/gep/validator/stakeBootstrap.js +3 -0
- package/src/gep/workspaceKeychain.js +1 -1
- package/src/ops/lifecycle.js +79 -10
- package/src/ops/skills_monitor.js +2 -1
- package/src/proxy/index.js +7 -1
- package/src/proxy/lifecycle/manager.js +77 -4
- package/src/proxy/mailbox/store.js +52 -2
- package/src/proxy/server/settings.js +16 -2
- package/src/proxy/sync/inbound.js +14 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evomap/evolver",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.88.0",
|
|
4
4
|
"description": "A GEP-powered self-evolution engine for AI agents. Features automated log analysis and Genome Evolution Protocol (GEP) for auditable, reusable evolution assets.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -249,8 +249,18 @@ if (!OPTS.skipObfuscate) {
|
|
|
249
249
|
};
|
|
250
250
|
|
|
251
251
|
const MAX_OBF_ATTEMPTS_RAW = process.env.OBF_MAX_ATTEMPTS;
|
|
252
|
+
// Default 12 (was 4). The obfuscator's new.target -> #target mangling is
|
|
253
|
+
// non-deterministic ACROSS PROCESSES, not just across seeds: the same seed
|
|
254
|
+
// + same input can pass in one node process and fail in another (Set
|
|
255
|
+
// iteration / internal-state timing). So perturbing the seed per attempt is
|
|
256
|
+
// not the real lever — re-running the obfuscate call is. The v1.87.4 deploy
|
|
257
|
+
// hit 4/4 consecutive failures with the default of 4 and aborted the npm
|
|
258
|
+
// publish + binary upload. At an observed per-attempt failure rate that can
|
|
259
|
+
// run well above the historical ~5% for some bundles, 4 retries is too few;
|
|
260
|
+
// 12 drives the all-fail probability to negligible while costing only extra
|
|
261
|
+
// attempts on the rare unlucky run. Override with OBF_MAX_ATTEMPTS.
|
|
252
262
|
const MAX_OBF_ATTEMPTS = MAX_OBF_ATTEMPTS_RAW === undefined
|
|
253
|
-
?
|
|
263
|
+
? 12
|
|
254
264
|
: parseInt(MAX_OBF_ATTEMPTS_RAW, 10);
|
|
255
265
|
if (!Number.isInteger(MAX_OBF_ATTEMPTS) || MAX_OBF_ATTEMPTS < 1) {
|
|
256
266
|
console.error(` ERROR: OBF_MAX_ATTEMPTS must be a positive integer; got ${JSON.stringify(MAX_OBF_ATTEMPTS_RAW)}.`);
|
|
@@ -189,7 +189,9 @@ function copyHookScripts(destDir, evolverRoot) {
|
|
|
189
189
|
// closes the per-file hole.
|
|
190
190
|
assertNotSymlink(dest, `hook destination ${name}`);
|
|
191
191
|
fs.copyFileSync(src, dest);
|
|
192
|
-
|
|
192
|
+
// NOTE(windows): fs.chmodSync is a no-op on Windows; hook scripts remain
|
|
193
|
+
// executable via file extension association (.js), not Unix mode bits.
|
|
194
|
+
try { fs.chmodSync(dest, 0o755); } catch { /* best-effort; no-op on Windows */ }
|
|
193
195
|
copied.push(dest);
|
|
194
196
|
}
|
|
195
197
|
return copied;
|
|
@@ -59,12 +59,36 @@ function findEvolverRoot() {
|
|
|
59
59
|
// `evolver-session-start.js`'s `additionalContext`. Restrict to trusted,
|
|
60
60
|
// user/system-scoped install roots.
|
|
61
61
|
try {
|
|
62
|
+
// Windows: `npm install -g` puts packages under %APPDATA%\npm\node_modules
|
|
63
|
+
// (most common), %ProgramFiles%\nodejs\node_modules (system-wide installer),
|
|
64
|
+
// or %ProgramFiles(x86)%\nodejs\node_modules. Build the extra Windows paths
|
|
65
|
+
// conditionally so the POSIX base list stays intact.
|
|
66
|
+
const _winPaths = process.platform === 'win32'
|
|
67
|
+
? [
|
|
68
|
+
path.join(
|
|
69
|
+
process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'),
|
|
70
|
+
'npm', 'node_modules'
|
|
71
|
+
),
|
|
72
|
+
...(process.env.ProgramFiles
|
|
73
|
+
? [path.join(process.env.ProgramFiles, 'nodejs', 'node_modules')]
|
|
74
|
+
: []),
|
|
75
|
+
...(process.env['ProgramFiles(x86)']
|
|
76
|
+
? [path.join(process.env['ProgramFiles(x86)'], 'nodejs', 'node_modules')]
|
|
77
|
+
: []),
|
|
78
|
+
]
|
|
79
|
+
: [];
|
|
80
|
+
|
|
62
81
|
const pkgJson = require.resolve('@evomap/evolver/package.json', {
|
|
82
|
+
// Do NOT include process.cwd() — a hostile workspace can plant its own
|
|
83
|
+
// node_modules/@evomap/evolver to gain control over the memory graph path
|
|
84
|
+
// (prompt-injection surface: evolver-session-start.js additionalContext).
|
|
85
|
+
// Only trust user/system-scoped install roots.
|
|
63
86
|
paths: [
|
|
64
87
|
path.join(os.homedir(), '.npm-global', 'lib', 'node_modules'),
|
|
65
88
|
path.join(os.homedir(), '.local', 'lib', 'node_modules'),
|
|
66
89
|
'/usr/lib/node_modules',
|
|
67
90
|
'/usr/local/lib/node_modules',
|
|
91
|
+
..._winPaths,
|
|
68
92
|
],
|
|
69
93
|
});
|
|
70
94
|
if (pkgJson && isEvolverPackageJson(pkgJson)) {
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
const fs = require('fs');
|
|
9
9
|
const path = require('path');
|
|
10
10
|
const os = require('os');
|
|
11
|
+
const https = require('https');
|
|
12
|
+
const http = require('http');
|
|
11
13
|
const { spawnSync } = require('child_process');
|
|
12
14
|
// 10 MB — prevents RangeError on large child process output (e.g. git log/diff
|
|
13
15
|
// on large repos). See GHSA reports / issue #451.
|
|
@@ -116,6 +118,9 @@ function recordToHub(outcome) {
|
|
|
116
118
|
const nodeId = process.env.EVOMAP_NODE_ID || process.env.A2A_NODE_ID;
|
|
117
119
|
if (!hubUrl || !apiKey) return false;
|
|
118
120
|
|
|
121
|
+
// Use Node.js built-in http/https instead of curl so this works on all
|
|
122
|
+
// platforms, including Windows where curl may not be available or may be
|
|
123
|
+
// an older, incompatible version bundled with some environments.
|
|
119
124
|
try {
|
|
120
125
|
const payload = JSON.stringify({
|
|
121
126
|
gene_id: outcome.geneId || 'ad_hoc',
|
|
@@ -125,22 +130,39 @@ function recordToHub(outcome) {
|
|
|
125
130
|
summary: outcome.summary,
|
|
126
131
|
sender_id: nodeId || undefined,
|
|
127
132
|
});
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
133
|
+
|
|
134
|
+
const endpoint = hubUrl.replace(/\/+$/, '') + '/a2a/evolution/record';
|
|
135
|
+
let parsedUrl;
|
|
136
|
+
try { parsedUrl = new URL(endpoint); } catch { return false; }
|
|
137
|
+
|
|
138
|
+
const isHttps = parsedUrl.protocol === 'https:';
|
|
139
|
+
const transport = isHttps ? https : http;
|
|
140
|
+
|
|
141
|
+
return new Promise((resolve) => {
|
|
142
|
+
const req = transport.request(
|
|
143
|
+
{
|
|
144
|
+
hostname: parsedUrl.hostname,
|
|
145
|
+
port: parsedUrl.port || (isHttps ? 443 : 80),
|
|
146
|
+
path: parsedUrl.pathname + (parsedUrl.search || ''),
|
|
147
|
+
method: 'POST',
|
|
148
|
+
headers: {
|
|
149
|
+
'Content-Type': 'application/json',
|
|
150
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
151
|
+
'Content-Length': Buffer.byteLength(payload),
|
|
152
|
+
},
|
|
153
|
+
timeout: 8000,
|
|
154
|
+
},
|
|
155
|
+
(res) => {
|
|
156
|
+
// Drain the response to free the socket; we only care about status.
|
|
157
|
+
res.resume();
|
|
158
|
+
resolve(res.statusCode >= 200 && res.statusCode < 300);
|
|
159
|
+
}
|
|
160
|
+
);
|
|
161
|
+
req.on('error', () => resolve(false));
|
|
162
|
+
req.on('timeout', () => { req.destroy(); resolve(false); });
|
|
163
|
+
req.write(payload);
|
|
164
|
+
req.end();
|
|
141
165
|
});
|
|
142
|
-
if (res.status !== 0 || res.error) return false;
|
|
143
|
-
return true;
|
|
144
166
|
} catch {
|
|
145
167
|
return false;
|
|
146
168
|
}
|
|
@@ -227,78 +249,88 @@ function main() {
|
|
|
227
249
|
process.stdin.on('data', chunk => { inputData += chunk; });
|
|
228
250
|
process.stdin.on('end', () => {
|
|
229
251
|
if (handled) return;
|
|
230
|
-
|
|
231
|
-
|
|
252
|
+
// recordToHub is async (uses Node.js http/https); wrap the rest of the
|
|
253
|
+
// handler in an immediately-invoked async function so we can await it
|
|
254
|
+
// while still honouring the watchdog timeout and the `handled` guard.
|
|
255
|
+
(async () => {
|
|
256
|
+
try {
|
|
257
|
+
const diffInfo = getGitDiffStats();
|
|
232
258
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
259
|
+
if (!diffInfo.hasChanges) {
|
|
260
|
+
// No git diff means no signal source — session-end derives the
|
|
261
|
+
// outcome (status/score/signals/summary) entirely from the diff, so
|
|
262
|
+
// there is nothing meaningful to record. This is expected in a
|
|
263
|
+
// non-git workspace or a repo with no changes this session. Rather
|
|
264
|
+
// than fabricate an empty outcome (which would pollute the memory
|
|
265
|
+
// graph), record nothing — but leave a log breadcrumb so the user
|
|
266
|
+
// can tell "evolver ran but had nothing to record" apart from
|
|
267
|
+
// "evolver never fired".
|
|
268
|
+
const reason = diffInfo.isRepo
|
|
269
|
+
? 'no changes detected this session'
|
|
270
|
+
: 'not a git workspace';
|
|
271
|
+
appendEvolutionLog(`[Evolution] Session end: nothing recorded (${reason}).`);
|
|
272
|
+
finish({});
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
249
275
|
|
|
250
|
-
|
|
251
|
-
|
|
276
|
+
const signals = detectSignals(diffInfo.diffSnippet);
|
|
277
|
+
if (signals.length === 0) signals.push('stable_success_plateau');
|
|
252
278
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
279
|
+
const hasErrors = signals.includes('log_error') || signals.includes('test_failure');
|
|
280
|
+
const status = hasErrors ? 'failed' : 'success';
|
|
281
|
+
const score = hasErrors ? 0.3 : 0.8;
|
|
256
282
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
283
|
+
const outcome = {
|
|
284
|
+
geneId: 'ad_hoc',
|
|
285
|
+
signals,
|
|
286
|
+
status,
|
|
287
|
+
score,
|
|
288
|
+
summary: `Session end: ${diffInfo.summary}. Signals: [${signals.join(', ')}]`,
|
|
289
|
+
};
|
|
264
290
|
|
|
265
|
-
|
|
266
|
-
|
|
291
|
+
const evolverRoot = findEvolverRoot();
|
|
292
|
+
const graphPath = findMemoryGraph(evolverRoot);
|
|
267
293
|
|
|
268
|
-
|
|
269
|
-
|
|
294
|
+
// Local first: recordToHub is async with an 8s socket timeout, but
|
|
295
|
+
// the 7s watchdog (setTimeout below) will process.exit(0) before a
|
|
296
|
+
// slow hub returns — so anything sequenced after `await recordToHub`
|
|
297
|
+
// can be silently skipped. recordToLocal is the reliable offline
|
|
298
|
+
// fallback and must run regardless of hub latency.
|
|
299
|
+
const localOk = graphPath ? recordToLocal(graphPath, outcome) : false;
|
|
300
|
+
const hubOk = await recordToHub(outcome);
|
|
270
301
|
|
|
271
|
-
|
|
272
|
-
|
|
302
|
+
const target = hubOk ? 'Hub' : localOk ? 'local memory' : 'nowhere (no Hub or local path)';
|
|
303
|
+
const msg = `[Evolution] Session outcome recorded to ${target}: ${outcome.summary}`;
|
|
273
304
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
305
|
+
// Stop hook output schema (per Claude Code docs):
|
|
306
|
+
// - decision: "approve" | "block"
|
|
307
|
+
// - reason: string (shown when decision is set)
|
|
308
|
+
// - systemMessage: string (notification displayed to user)
|
|
309
|
+
// - continue: boolean
|
|
310
|
+
// - stopReason: string
|
|
311
|
+
//
|
|
312
|
+
// Earlier versions emitted `followup_message`, `stopMessage`, and
|
|
313
|
+
// `additionalContext` together. `followup_message` is the field that
|
|
314
|
+
// re-injects the receipt into Claude's next inference round, which
|
|
315
|
+
// caused the agent to "respond" to its own evolution log line —
|
|
316
|
+
// visible to users as an unexplained extra reasoning turn after
|
|
317
|
+
// every task. The evolver is supposed to be observational, so we
|
|
318
|
+
// now use `systemMessage` only — that surfaces the receipt to the
|
|
319
|
+
// user without forcing another inference round.
|
|
320
|
+
//
|
|
321
|
+
// Cursor compatibility: Cursor's Claude Code-compatible runtime
|
|
322
|
+
// currently treats `systemMessage` as a user prompt for the next
|
|
323
|
+
// inference round. When we detect Cursor, omit systemMessage too.
|
|
324
|
+
// The receipt is always appended to ~/.evolver/logs/evolution.log
|
|
325
|
+
// so it is never silently lost; users can opt back in to the inline
|
|
326
|
+
// notification with EVOLVER_HOOK_VERBOSE=1.
|
|
327
|
+
appendEvolutionLog(msg);
|
|
297
328
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
329
|
+
finish(isCursorHost() ? {} : { systemMessage: msg });
|
|
330
|
+
} catch (e) {
|
|
331
|
+
finish({});
|
|
332
|
+
}
|
|
333
|
+
})();
|
|
302
334
|
});
|
|
303
335
|
|
|
304
336
|
watchdog = setTimeout(() => finish({}), 7000);
|
|
@@ -10,6 +10,100 @@ const os = require('os');
|
|
|
10
10
|
const { findEvolverRoot, findMemoryGraph, resolveProjectDir, resolveWorkspaceId, isGitWorkspace } = require('./_runtimePaths');
|
|
11
11
|
const { filterRelevantOutcomes } = require('./_memoryFiltering');
|
|
12
12
|
|
|
13
|
+
// Auto-restart guard: if the evolver daemon is not running when a new agent
|
|
14
|
+
// session starts, attempt a background restart. This covers the "idle-death"
|
|
15
|
+
// scenario: the user closed the machine (macOS sleep), the process died due to
|
|
16
|
+
// event-loop exhaustion or OOM, and now the next agent session finds it gone.
|
|
17
|
+
// We delegate to lifecycle.js restart() which is idempotent (no-op if already
|
|
18
|
+
// running), detached (does not block session startup), and captures output in
|
|
19
|
+
// the existing evolver log.
|
|
20
|
+
//
|
|
21
|
+
// Guard-rails:
|
|
22
|
+
// - Only runs when EVOLVER_SESSION_AUTO_RESTART is not "0" or "false".
|
|
23
|
+
// - Skips gracefully if lifecycle.js cannot be found (non-daemon setups,
|
|
24
|
+
// npx one-shot mode, etc.).
|
|
25
|
+
// - Execution errors are swallowed: this must never cause session-start to
|
|
26
|
+
// error out or delay the LLM context injection.
|
|
27
|
+
function _maybeRestartDaemon(evolverRoot) {
|
|
28
|
+
try {
|
|
29
|
+
var autoRestart = String(process.env.EVOLVER_SESSION_AUTO_RESTART || '1').toLowerCase().trim();
|
|
30
|
+
if (autoRestart === '0' || autoRestart === 'false') return;
|
|
31
|
+
|
|
32
|
+
var lifecyclePath = evolverRoot
|
|
33
|
+
? path.join(evolverRoot, 'src', 'ops', 'lifecycle.js')
|
|
34
|
+
: null;
|
|
35
|
+
if (!lifecyclePath || !fs.existsSync(lifecyclePath)) return;
|
|
36
|
+
|
|
37
|
+
// Check if daemon is running by looking for the PID file / lock file.
|
|
38
|
+
// R12: index.js:getLockFilePath honors EVOLVER_LOCK_DIR. If that env is
|
|
39
|
+
// set the lock file lives at <EVOLVER_LOCK_DIR>/evolver.pid (basename
|
|
40
|
+
// differs from the default!); otherwise fall back to the canonical
|
|
41
|
+
// ~/.evomap/instance.lock. We replicate the logic inline rather than
|
|
42
|
+
// importing index.js, since pulling the daemon module into the hook
|
|
43
|
+
// would load far more than we need.
|
|
44
|
+
var lockFile = process.env.EVOLVER_LOCK_DIR
|
|
45
|
+
? path.join(process.env.EVOLVER_LOCK_DIR, 'evolver.pid')
|
|
46
|
+
: path.join(os.homedir(), '.evomap', 'instance.lock');
|
|
47
|
+
// R1: PID-reuse defense. process.kill(pid, 0) only proves SOME process
|
|
48
|
+
// owns that PID -- after macOS sleep / OOM, the kernel may have reused
|
|
49
|
+
// the slain daemon's PID for an unrelated process (Chrome tab, shell).
|
|
50
|
+
// Mirror index.js:_lockIsStaleByLease (search for STALE_LOCK_TTL_MS
|
|
51
|
+
// around line 373): a lease-aware daemon refreshes the lock mtime on a
|
|
52
|
+
// timer, so if mtime is older than the TTL the daemon is dead/wedged
|
|
53
|
+
// regardless of kill(0). Constants inlined to keep index.js out of the
|
|
54
|
+
// hook's require graph.
|
|
55
|
+
var STALE_LOCK_TTL_MS = process.platform === 'win32' ? 3 * 60_000 : 5 * 60_000;
|
|
56
|
+
var daemonRunning = false;
|
|
57
|
+
try {
|
|
58
|
+
if (fs.existsSync(lockFile)) {
|
|
59
|
+
var raw = fs.readFileSync(lockFile, 'utf8').trim();
|
|
60
|
+
var payload = raw && raw[0] === '{' ? JSON.parse(raw) : { pid: parseInt(raw, 10) };
|
|
61
|
+
if (payload && payload.pid > 0) {
|
|
62
|
+
try { process.kill(payload.pid, 0); daemonRunning = true; } catch (e) {
|
|
63
|
+
// EPERM = process exists but owned by a different user; still a live daemon.
|
|
64
|
+
if (e && e.code === 'EPERM') daemonRunning = true;
|
|
65
|
+
}
|
|
66
|
+
// Lease staleness overrides kill(0)=alive. Only trust mtime when
|
|
67
|
+
// the payload came from a lease-aware daemon (matches index.js's
|
|
68
|
+
// _lockIsStaleByLease guard) so we never falsely steal an older
|
|
69
|
+
// pre-lease daemon's lock.
|
|
70
|
+
if (daemonRunning && payload.lease === true) {
|
|
71
|
+
try {
|
|
72
|
+
var ageMs = Date.now() - fs.statSync(lockFile).mtimeMs;
|
|
73
|
+
if (ageMs > STALE_LOCK_TTL_MS) daemonRunning = false;
|
|
74
|
+
} catch (_) { /* stat failed: leave running flag as-is */ }
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
} catch (_) { /* lock file unreadable or absent: assume not running */ }
|
|
79
|
+
|
|
80
|
+
if (daemonRunning) return; // already alive, nothing to do
|
|
81
|
+
|
|
82
|
+
// Daemon appears dead. Spawn lifecycle.js start in the background so
|
|
83
|
+
// this session-start script exits immediately (< 50 ms) and does not
|
|
84
|
+
// block the LLM from getting context.
|
|
85
|
+
var { spawn } = require('child_process');
|
|
86
|
+
var child = spawn(
|
|
87
|
+
process.execPath,
|
|
88
|
+
[lifecyclePath, 'start'],
|
|
89
|
+
{
|
|
90
|
+
detached: true,
|
|
91
|
+
stdio: 'ignore',
|
|
92
|
+
cwd: evolverRoot,
|
|
93
|
+
env: Object.assign({}, process.env),
|
|
94
|
+
}
|
|
95
|
+
);
|
|
96
|
+
child.unref();
|
|
97
|
+
// Best-effort: log a single-line note to stderr so the session transcript
|
|
98
|
+
// shows that a restart was attempted, without affecting stdout JSON output.
|
|
99
|
+
try {
|
|
100
|
+
process.stderr.write('[evolver-session-start] Daemon was not running; attempted background restart (PID ' + child.pid + ').\n');
|
|
101
|
+
} catch (_) {}
|
|
102
|
+
} catch (_) {
|
|
103
|
+
// Never let this helper block or crash the session-start script.
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
13
107
|
// One-line notice shown (throttled) when the workspace is not a git repo.
|
|
14
108
|
// Evolver derives every outcome from the git diff, so in a non-git folder the
|
|
15
109
|
// session-end hook records nothing — silently, unless we say so here. We surface
|
|
@@ -167,6 +261,12 @@ function main() {
|
|
|
167
261
|
}
|
|
168
262
|
|
|
169
263
|
const evolverRoot = findEvolverRoot();
|
|
264
|
+
|
|
265
|
+
// Attempt to restart the daemon in the background if it has died since the
|
|
266
|
+
// last session (idle-death / macOS sleep / OOM). Fire-and-forget: errors are
|
|
267
|
+
// swallowed and this never delays the JSON output below.
|
|
268
|
+
_maybeRestartDaemon(evolverRoot);
|
|
269
|
+
|
|
170
270
|
const graphPath = findMemoryGraph(evolverRoot);
|
|
171
271
|
|
|
172
272
|
// Scope to the current workspace BEFORE trimming to the most-recent window,
|
package/src/config.js
CHANGED
|
@@ -9,6 +9,35 @@ function envInt(key, fallback) {
|
|
|
9
9
|
return isNaN(n) ? fallback : n;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
// Strict variant for timing / timeout / interval values that must be
|
|
13
|
+
// positive. Rejects: NaN (e.g. "5min" silently parses to 5 -- here 5 is
|
|
14
|
+
// accepted but a suffix-only "ms" becomes NaN), 0 (which would turn an
|
|
15
|
+
// interval into a hot loop or zero out a timeout signal), and negatives
|
|
16
|
+
// (setTimeout clamps to 1 ms). Also rejects values >= 2^31 because
|
|
17
|
+
// setTimeout silently downgrades those to 1 ms in Node. Misconfigured
|
|
18
|
+
// values fall back to the default with a one-time warning so the user
|
|
19
|
+
// is not silently running a broken setup.
|
|
20
|
+
const _envPositiveIntWarned = new Set();
|
|
21
|
+
function envPositiveInt(key, fallback) {
|
|
22
|
+
const v = process.env[key];
|
|
23
|
+
if (v === undefined || v === '') return fallback;
|
|
24
|
+
const n = parseInt(v, 10);
|
|
25
|
+
const valid = Number.isFinite(n) && n > 0 && n < 2 ** 31;
|
|
26
|
+
if (!valid) {
|
|
27
|
+
if (!_envPositiveIntWarned.has(key)) {
|
|
28
|
+
_envPositiveIntWarned.add(key);
|
|
29
|
+
try {
|
|
30
|
+
console.warn(
|
|
31
|
+
'[config] ' + key + '=' + JSON.stringify(v) + ' is not a positive integer; ' +
|
|
32
|
+
'falling back to ' + fallback + '. Set a value in (0, 2^31) ms.'
|
|
33
|
+
);
|
|
34
|
+
} catch (_) {}
|
|
35
|
+
}
|
|
36
|
+
return fallback;
|
|
37
|
+
}
|
|
38
|
+
return n;
|
|
39
|
+
}
|
|
40
|
+
|
|
12
41
|
function envFloat(key, fallback) {
|
|
13
42
|
const v = process.env[key];
|
|
14
43
|
if (v === undefined || v === '') return fallback;
|
|
@@ -23,14 +52,19 @@ function envStr(key, fallback) {
|
|
|
23
52
|
|
|
24
53
|
// --- Network & A2A ---
|
|
25
54
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
const
|
|
55
|
+
// Hot-path timers / intervals use envPositiveInt: a misconfigured 0,
|
|
56
|
+
// negative, or non-numeric value would otherwise turn the heartbeat
|
|
57
|
+
// loop into a hot spin (setTimeout(0)) or zero out a timeout signal
|
|
58
|
+
// (AbortSignal.timeout(0) immediately aborts every request). Falls back
|
|
59
|
+
// to the default with a one-time warning when the env var is invalid.
|
|
60
|
+
const HELLO_TIMEOUT_MS = envPositiveInt('EVOLVER_HELLO_TIMEOUT_MS', 15000);
|
|
61
|
+
const HEARTBEAT_TIMEOUT_MS = envPositiveInt('EVOLVER_HEARTBEAT_TIMEOUT_MS', 10000);
|
|
62
|
+
const HEARTBEAT_INTERVAL_MS = envPositiveInt('HEARTBEAT_INTERVAL_MS', 360000);
|
|
63
|
+
const HEARTBEAT_FIRST_DELAY_MS = envPositiveInt('EVOLVER_HEARTBEAT_FIRST_DELAY_MS', 30000);
|
|
64
|
+
const EVENT_POLL_TIMEOUT_MS = envPositiveInt('EVOLVER_EVENT_POLL_TIMEOUT_MS', 60000);
|
|
65
|
+
const HTTP_TRANSPORT_TIMEOUT_MS = envPositiveInt('EVOLVER_HTTP_TRANSPORT_TIMEOUT_MS', 15000);
|
|
66
|
+
const SECRET_CACHE_TTL_MS = envPositiveInt('EVOLVER_SECRET_CACHE_TTL_MS', 60000);
|
|
67
|
+
const HUB_SEARCH_TIMEOUT_MS = envPositiveInt('EVOLVER_HUB_SEARCH_TIMEOUT_MS', 8000);
|
|
34
68
|
|
|
35
69
|
// Hub URL resolution (since v1.69.7).
|
|
36
70
|
//
|
|
@@ -234,6 +268,7 @@ module.exports = {
|
|
|
234
268
|
VALIDATOR_BATCH_TIMEOUT_MS,
|
|
235
269
|
// Helpers
|
|
236
270
|
envInt,
|
|
271
|
+
envPositiveInt,
|
|
237
272
|
envFloat,
|
|
238
273
|
envStr,
|
|
239
274
|
};
|