@evomap/evolver 1.87.4 → 1.88.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/index.js +934 -33
  2. package/package.json +1 -1
  3. package/scripts/build_binaries.js +11 -1
  4. package/src/adapters/hookAdapter.js +3 -1
  5. package/src/adapters/scripts/_runtimePaths.js +24 -0
  6. package/src/adapters/scripts/evolver-session-end.js +110 -78
  7. package/src/adapters/scripts/evolver-session-start.js +100 -0
  8. package/src/config.js +43 -8
  9. package/src/evolve/guards.js +1 -1
  10. package/src/evolve/pipeline/collect.js +1 -1
  11. package/src/evolve/pipeline/dispatch.js +1 -1
  12. package/src/evolve/pipeline/enrich.js +1 -1
  13. package/src/evolve/pipeline/hub.js +1 -1
  14. package/src/evolve/pipeline/select.js +1 -1
  15. package/src/evolve/pipeline/signals.js +1 -1
  16. package/src/evolve/utils.js +1 -1
  17. package/src/evolve.js +1 -1
  18. package/src/forceUpdate.js +42 -21
  19. package/src/gep/a2aProtocol.js +1 -1
  20. package/src/gep/assetStore.js +40 -0
  21. package/src/gep/autoDistillConv.js +1 -0
  22. package/src/gep/autoDistillLlm.js +1 -0
  23. package/src/gep/bridge.js +69 -2
  24. package/src/gep/candidateEval.js +1 -1
  25. package/src/gep/candidates.js +1 -1
  26. package/src/gep/contentHash.js +1 -1
  27. package/src/gep/conversationSniffer.js +1 -0
  28. package/src/gep/crypto.js +1 -1
  29. package/src/gep/curriculum.js +1 -1
  30. package/src/gep/deviceId.js +1 -1
  31. package/src/gep/envFingerprint.js +1 -1
  32. package/src/gep/epigenetics.js +1 -1
  33. package/src/gep/execBridge.js +1 -0
  34. package/src/gep/explore.js +1 -1
  35. package/src/gep/featureFlags.js +4 -0
  36. package/src/gep/gitOps.js +7 -2
  37. package/src/gep/hash.js +1 -1
  38. package/src/gep/hubFetch.js +1 -1
  39. package/src/gep/hubReview.js +1 -1
  40. package/src/gep/hubSearch.js +1 -1
  41. package/src/gep/hubVerify.js +1 -1
  42. package/src/gep/idleScheduler.js +78 -0
  43. package/src/gep/learningSignals.js +1 -1
  44. package/src/gep/mailboxTransport.js +34 -0
  45. package/src/gep/memoryGraph.js +1 -1
  46. package/src/gep/memoryGraphAdapter.js +1 -1
  47. package/src/gep/mutation.js +1 -1
  48. package/src/gep/narrativeMemory.js +1 -1
  49. package/src/gep/openPRRegistry.js +1 -1
  50. package/src/gep/paths.js +16 -2
  51. package/src/gep/personality.js +1 -1
  52. package/src/gep/policyCheck.js +1 -1
  53. package/src/gep/prompt.js +1 -1
  54. package/src/gep/recallVerifier.js +1 -1
  55. package/src/gep/reflection.js +1 -1
  56. package/src/gep/selector.js +1 -1
  57. package/src/gep/skillDistiller.js +1 -1
  58. package/src/gep/solidify.js +1 -1
  59. package/src/gep/strategy.js +1 -1
  60. package/src/gep/validator/index.js +46 -1
  61. package/src/gep/validator/sandboxExecutor.js +10 -1
  62. package/src/gep/validator/stakeBootstrap.js +3 -0
  63. package/src/gep/workspaceKeychain.js +1 -1
  64. package/src/ops/lifecycle.js +79 -10
  65. package/src/ops/skills_monitor.js +2 -1
  66. package/src/proxy/index.js +31 -6
  67. package/src/proxy/lifecycle/manager.js +77 -4
  68. package/src/proxy/mailbox/store.js +52 -2
  69. package/src/proxy/server/settings.js +16 -2
  70. 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.87.4",
3
+ "version": "1.88.1",
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
- ? 4
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
- try { fs.chmodSync(dest, 0o755); } catch { /* windows */ }
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
- // Argv-array form avoids shell interpretation of apiKey, payload, or the
129
- // hub URL. Values cannot break out through shell metacharacters.
130
- const res = spawnSync('curl', [
131
- '-s', '-m', '8', '-X', 'POST',
132
- '-H', 'Content-Type: application/json',
133
- '-H', `Authorization: Bearer ${apiKey}`,
134
- '-d', payload,
135
- `${hubUrl.replace(/\/+$/, '')}/a2a/evolution/record`,
136
- ], {
137
- timeout: 10000,
138
- stdio: ['pipe', 'pipe', 'pipe'],
139
- maxBuffer: MAX_EXEC_BUFFER,
140
- shell: false,
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
- try {
231
- const diffInfo = getGitDiffStats();
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
- if (!diffInfo.hasChanges) {
234
- // No git diff means no signal source — session-end derives the
235
- // outcome (status/score/signals/summary) entirely from the diff, so
236
- // there is nothing meaningful to record. This is expected in a
237
- // non-git workspace or a repo with no changes this session. Rather
238
- // than fabricate an empty outcome (which would pollute the memory
239
- // graph), record nothing — but leave a log breadcrumb so the user
240
- // can tell "evolver ran but had nothing to record" apart from
241
- // "evolver never fired". (Previously this branch was fully silent.)
242
- const reason = diffInfo.isRepo
243
- ? 'no changes detected this session'
244
- : 'not a git workspace';
245
- appendEvolutionLog(`[Evolution] Session end: nothing recorded (${reason}).`);
246
- finish({});
247
- return;
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
- const signals = detectSignals(diffInfo.diffSnippet);
251
- if (signals.length === 0) signals.push('stable_success_plateau');
276
+ const signals = detectSignals(diffInfo.diffSnippet);
277
+ if (signals.length === 0) signals.push('stable_success_plateau');
252
278
 
253
- const hasErrors = signals.includes('log_error') || signals.includes('test_failure');
254
- const status = hasErrors ? 'failed' : 'success';
255
- const score = hasErrors ? 0.3 : 0.8;
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
- const outcome = {
258
- geneId: 'ad_hoc',
259
- signals,
260
- status,
261
- score,
262
- summary: `Session end: ${diffInfo.summary}. Signals: [${signals.join(', ')}]`,
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
- const evolverRoot = findEvolverRoot();
266
- const graphPath = findMemoryGraph(evolverRoot);
291
+ const evolverRoot = findEvolverRoot();
292
+ const graphPath = findMemoryGraph(evolverRoot);
267
293
 
268
- const hubOk = recordToHub(outcome);
269
- const localOk = graphPath ? recordToLocal(graphPath, outcome) : false;
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
- const target = hubOk ? 'Hub' : localOk ? 'local memory' : 'nowhere (no Hub or local path)';
272
- const msg = `[Evolution] Session outcome recorded to ${target}: ${outcome.summary}`;
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
- // Stop hook output schema (per Claude Code docs):
275
- // - decision: "approve" | "block"
276
- // - reason: string (shown when decision is set)
277
- // - systemMessage: string (notification displayed to user)
278
- // - continue: boolean
279
- // - stopReason: string
280
- //
281
- // Earlier versions emitted `followup_message`, `stopMessage`, and
282
- // `additionalContext` together. `followup_message` is the field that
283
- // re-injects the receipt into Claude's next inference round, which
284
- // caused the agent to "respond" to its own evolution log line —
285
- // visible to users as an unexplained extra reasoning turn after
286
- // every task. The evolver is supposed to be observational, so we
287
- // now use `systemMessage` only — that surfaces the receipt to the
288
- // user without forcing another inference round.
289
- //
290
- // Cursor compatibility: Cursor's Claude Code-compatible runtime
291
- // currently treats `systemMessage` as a user prompt for the next
292
- // inference round. When we detect Cursor, omit systemMessage too.
293
- // The receipt is always appended to ~/.evolver/logs/evolution.log
294
- // so it is never silently lost; users can opt back in to the inline
295
- // notification with EVOLVER_HOOK_VERBOSE=1.
296
- appendEvolutionLog(msg);
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
- finish(isCursorHost() ? {} : { systemMessage: msg });
299
- } catch (e) {
300
- finish({});
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
- const HELLO_TIMEOUT_MS = envInt('EVOLVER_HELLO_TIMEOUT_MS', 15000);
27
- const HEARTBEAT_TIMEOUT_MS = envInt('EVOLVER_HEARTBEAT_TIMEOUT_MS', 10000);
28
- const HEARTBEAT_INTERVAL_MS = envInt('HEARTBEAT_INTERVAL_MS', 360000);
29
- const HEARTBEAT_FIRST_DELAY_MS = envInt('EVOLVER_HEARTBEAT_FIRST_DELAY_MS', 30000);
30
- const EVENT_POLL_TIMEOUT_MS = envInt('EVOLVER_EVENT_POLL_TIMEOUT_MS', 60000);
31
- const HTTP_TRANSPORT_TIMEOUT_MS = envInt('EVOLVER_HTTP_TRANSPORT_TIMEOUT_MS', 15000);
32
- const SECRET_CACHE_TTL_MS = envInt('EVOLVER_SECRET_CACHE_TTL_MS', 60000);
33
- const HUB_SEARCH_TIMEOUT_MS = envInt('EVOLVER_HUB_SEARCH_TIMEOUT_MS', 8000);
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
  };