@evomap/evolver 1.87.3 → 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.
Files changed (65) hide show
  1. package/index.js +848 -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 +202 -1
  6. package/src/adapters/scripts/evolver-session-end.js +160 -98
  7. package/src/adapters/scripts/evolver-session-start.js +227 -43
  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/candidateEval.js +1 -1
  22. package/src/gep/candidates.js +1 -1
  23. package/src/gep/contentHash.js +1 -1
  24. package/src/gep/crypto.js +1 -1
  25. package/src/gep/curriculum.js +1 -1
  26. package/src/gep/deviceId.js +1 -1
  27. package/src/gep/envFingerprint.js +1 -1
  28. package/src/gep/epigenetics.js +1 -1
  29. package/src/gep/explore.js +1 -1
  30. package/src/gep/featureFlags.js +4 -0
  31. package/src/gep/gitOps.js +7 -2
  32. package/src/gep/hash.js +1 -1
  33. package/src/gep/hubFetch.js +1 -1
  34. package/src/gep/hubReview.js +1 -1
  35. package/src/gep/hubSearch.js +1 -1
  36. package/src/gep/hubVerify.js +1 -1
  37. package/src/gep/idleScheduler.js +233 -6
  38. package/src/gep/learningSignals.js +1 -1
  39. package/src/gep/mailboxTransport.js +34 -0
  40. package/src/gep/memoryGraph.js +1 -1
  41. package/src/gep/memoryGraphAdapter.js +1 -1
  42. package/src/gep/mutation.js +1 -1
  43. package/src/gep/narrativeMemory.js +1 -1
  44. package/src/gep/openPRRegistry.js +1 -1
  45. package/src/gep/paths.js +16 -2
  46. package/src/gep/personality.js +1 -1
  47. package/src/gep/policyCheck.js +1 -1
  48. package/src/gep/prompt.js +1 -1
  49. package/src/gep/recallVerifier.js +1 -1
  50. package/src/gep/reflection.js +1 -1
  51. package/src/gep/selector.js +1 -1
  52. package/src/gep/skillDistiller.js +1 -1
  53. package/src/gep/solidify.js +1 -1
  54. package/src/gep/strategy.js +1 -1
  55. package/src/gep/validator/index.js +46 -1
  56. package/src/gep/validator/sandboxExecutor.js +10 -1
  57. package/src/gep/validator/stakeBootstrap.js +3 -0
  58. package/src/gep/workspaceKeychain.js +1 -1
  59. package/src/ops/lifecycle.js +79 -10
  60. package/src/ops/skills_monitor.js +2 -1
  61. package/src/proxy/index.js +7 -1
  62. package/src/proxy/lifecycle/manager.js +77 -4
  63. package/src/proxy/mailbox/store.js +52 -2
  64. package/src/proxy/server/settings.js +16 -2
  65. package/src/proxy/sync/inbound.js +14 -1
@@ -7,17 +7,170 @@ const fs = require('fs');
7
7
  const path = require('path');
8
8
  const os = require('os');
9
9
 
10
- const { findEvolverRoot, findMemoryGraph } = require('./_runtimePaths');
10
+ const { findEvolverRoot, findMemoryGraph, resolveProjectDir, resolveWorkspaceId, isGitWorkspace } = require('./_runtimePaths');
11
11
  const { filterRelevantOutcomes } = require('./_memoryFiltering');
12
12
 
13
- function readLastN(filePath, n) {
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) {
14
28
  try {
15
- const content = fs.readFileSync(filePath, 'utf8');
16
- const lines = content.trim().split('\n').filter(Boolean);
17
- return lines.slice(-n).map(line => {
18
- try { return JSON.parse(line); } catch { return null; }
19
- }).filter(Boolean);
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
+
107
+ // One-line notice shown (throttled) when the workspace is not a git repo.
108
+ // Evolver derives every outcome from the git diff, so in a non-git folder the
109
+ // session-end hook records nothing — silently, unless we say so here. We surface
110
+ // it in session-start's additionalContext (injected as opening context, which
111
+ // does NOT trigger an extra inference round, unlike a stop-hook systemMessage).
112
+ const NON_GIT_NOTICE =
113
+ '[Evolver] This folder is not a git repository, so evolution memory is inactive ' +
114
+ '(outcomes are derived from git diffs). Run `git init` here, or open a git project, ' +
115
+ 'to enable recall and recording.';
116
+ const NON_GIT_NOTICE_TTL_MS = 30 * 60 * 1000; // once per 30 min per folder
117
+
118
+ // Return up to `n` of the current workspace's most-recent entries, in
119
+ // chronological (oldest-first) order.
120
+ //
121
+ // Why scan from the end: a plain tail-N-then-filter read would let outcomes
122
+ // from other projects (which share the user-level fallback graph on npm-global
123
+ // installs) crowd this workspace's entries out of the window — we must scope
124
+ // to the workspace BEFORE trimming. But parsing the ENTIRE file to do that is
125
+ // wasteful: the graph can reach ~100 MB before rotation, and JSON-parsing every
126
+ // line on each session start is real CPU/memory cost (Bugbot PR #555 round-3).
127
+ //
128
+ // So we read the file (cheap; the previous readLastN read it whole too) but
129
+ // JSON-parse lines lazily from the newest end, keeping only workspace matches,
130
+ // and stop as soon as we have `n`. Parse count is bounded by where this
131
+ // workspace's n-th-most-recent entry sits, not by total file size.
132
+ function readRecentWorkspaceEntries(filePath, currentId, currentDir, n) {
133
+ let lines;
134
+ try {
135
+ lines = fs.readFileSync(filePath, 'utf8').trim().split('\n');
20
136
  } catch { return []; }
137
+ const out = [];
138
+ for (let i = lines.length - 1; i >= 0 && out.length < n; i--) {
139
+ const line = lines[i];
140
+ if (!line) continue;
141
+ let entry;
142
+ try { entry = JSON.parse(line); } catch { continue; }
143
+ if (belongsToWorkspace(entry, currentId, currentDir)) out.push(entry);
144
+ }
145
+ return out.reverse(); // newest-collected-first -> chronological
146
+ }
147
+
148
+ // Does this memory-graph entry belong to the current workspace?
149
+ //
150
+ // The session-end writer stamps two tags: `workspace_id` (forge-resistant,
151
+ // preferred) and `cwd` (backward-compat). We scope reads so that one project
152
+ // never sees another's outcomes through the shared user-level fallback graph
153
+ // (~/.evolver/memory/evolution/memory_graph.jsonl) — the cross-project
154
+ // disclosure / prompt-injection surface Bugbot flagged on the writer side
155
+ // (PR #105 round-2), which the reader never enforced until now.
156
+ //
157
+ // Rules, in order:
158
+ // - currentId known + entry.workspace_id present -> must match exactly.
159
+ // - currentId unknown OR entry has neither tag (pre-hardening / Hub-sourced
160
+ // entries) -> do NOT exclude; falling back to "show it" preserves prior
161
+ // behavior and avoids hiding all memory when ids can't be resolved.
162
+ // - As a softer fallback, when the entry has no workspace_id but does carry a
163
+ // cwd, match that against the current project dir.
164
+ function belongsToWorkspace(entry, currentId, currentDir) {
165
+ if (entry && typeof entry.workspace_id === 'string' && entry.workspace_id) {
166
+ if (currentId) return entry.workspace_id === currentId;
167
+ return true; // can't compare — don't hide it
168
+ }
169
+ if (entry && typeof entry.cwd === 'string' && entry.cwd) {
170
+ if (currentDir) return entry.cwd === currentDir;
171
+ return true;
172
+ }
173
+ return true; // untagged (legacy / Hub) — never excluded
21
174
  }
22
175
 
23
176
  function formatOutcome(entry) {
@@ -46,18 +199,14 @@ function getDedupStatePath() {
46
199
  return path.join(dir, 'session-start-state.json');
47
200
  }
48
201
 
49
- function shouldSkipInjection() {
50
- // Only apply dedup when explicitly enabled (set by Kiro adapter) OR when
51
- // we detect a per-prompt-firing platform via PROMPT_SUBMIT heuristic in
52
- // stdin. The stdin is drained in main(), so we rely on env flag here.
53
- const dedupEnabled = String(process.env.EVOLVER_SESSION_START_DEDUP || '').toLowerCase() === '1'
54
- || String(process.env.EVOLVER_SESSION_START_DEDUP || '').toLowerCase() === 'true';
55
- if (!dedupEnabled) return false;
56
-
57
- const ttlMs = Number(process.env.EVOLVER_SESSION_START_DEDUP_TTL_MS) || (30 * 60 * 1000);
58
- const key = process.cwd();
202
+ // TTL throttle keyed by an arbitrary string, persisted in session-start-state
203
+ // .json. Returns true if `key` fired within the last `ttlMs` (caller should
204
+ // suppress); otherwise records "now" for `key` and returns false. Best-effort:
205
+ // a state read/write failure just means no throttling (fail open). Shared by
206
+ // the Kiro per-prompt dedup and the non-git notice so both age out of the same
207
+ // file (entries older than 24h are pruned on write).
208
+ function throttled(key, ttlMs) {
59
209
  const statePath = getDedupStatePath();
60
-
61
210
  let state = {};
62
211
  try {
63
212
  if (fs.existsSync(statePath)) {
@@ -67,9 +216,7 @@ function shouldSkipInjection() {
67
216
 
68
217
  const now = Date.now();
69
218
  const last = state[key];
70
- if (typeof last === 'number' && now - last < ttlMs) {
71
- return true;
72
- }
219
+ if (typeof last === 'number' && now - last < ttlMs) return true;
73
220
 
74
221
  state[key] = now;
75
222
  try {
@@ -82,47 +229,84 @@ function shouldSkipInjection() {
82
229
  fs.writeFileSync(tmp, JSON.stringify(state), 'utf8');
83
230
  fs.renameSync(tmp, statePath);
84
231
  } catch { /* best-effort */ }
85
-
86
232
  return false;
87
233
  }
88
234
 
235
+ function shouldSkipInjection() {
236
+ // Only apply dedup when explicitly enabled (set by Kiro adapter) OR when
237
+ // we detect a per-prompt-firing platform via PROMPT_SUBMIT heuristic in
238
+ // stdin. The stdin is drained in main(), so we rely on env flag here.
239
+ const dedupEnabled = String(process.env.EVOLVER_SESSION_START_DEDUP || '').toLowerCase() === '1'
240
+ || String(process.env.EVOLVER_SESSION_START_DEDUP || '').toLowerCase() === 'true';
241
+ if (!dedupEnabled) return false;
242
+
243
+ const ttlMs = Number(process.env.EVOLVER_SESSION_START_DEDUP_TTL_MS) || (30 * 60 * 1000);
244
+ return throttled(process.cwd(), ttlMs);
245
+ }
246
+
89
247
  function main() {
90
248
  if (shouldSkipInjection()) {
91
249
  process.stdout.write(JSON.stringify({}));
92
250
  return;
93
251
  }
94
252
 
253
+ const currentDir = resolveProjectDir();
254
+
255
+ // Non-git notice: evolver records nothing in a non-git folder (outcomes come
256
+ // from git diffs), so tell the user — once per folder per TTL — instead of
257
+ // failing silently. Emitted regardless of whether any memory exists below.
258
+ const parts = [];
259
+ if (!isGitWorkspace(currentDir) && !throttled('nongit:' + currentDir, NON_GIT_NOTICE_TTL_MS)) {
260
+ parts.push(NON_GIT_NOTICE);
261
+ }
262
+
95
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
+
96
270
  const graphPath = findMemoryGraph(evolverRoot);
97
271
 
98
- if (!graphPath) {
99
- process.stdout.write(JSON.stringify({}));
100
- return;
272
+ // Scope to the current workspace BEFORE trimming to the most-recent window,
273
+ // so other projects sharing the user-level fallback graph can't crowd this
274
+ // workspace's outcomes out of view. When the workspace id can't be resolved,
275
+ // belongsToWorkspace() falls back to "show it" — no regression vs. the old
276
+ // unscoped behavior.
277
+ if (graphPath) {
278
+ const currentId = resolveWorkspaceId(evolverRoot, currentDir);
279
+ const recent = readRecentWorkspaceEntries(graphPath, currentId, currentDir, 5);
280
+ const filtered = filterRelevantOutcomes(recent);
281
+ if (filtered.length > 0) {
282
+ const successCount = filtered.filter(e => e.outcome && e.outcome.status === 'success').length;
283
+ const failCount = filtered.filter(e => e.outcome && e.outcome.status === 'failed').length;
284
+ parts.push([
285
+ `[Evolution Memory] Recent ${filtered.length} outcomes (${successCount} success, ${failCount} failed):`,
286
+ ...filtered.map(formatOutcome),
287
+ '',
288
+ 'Use successful approaches. Avoid repeating failed patterns.',
289
+ ].join('\n'));
290
+ }
101
291
  }
102
292
 
103
- const entries = readLastN(graphPath, 5);
104
- const filtered = filterRelevantOutcomes(entries);
105
-
106
- if (filtered.length === 0) {
293
+ if (parts.length === 0) {
107
294
  process.stdout.write(JSON.stringify({}));
108
295
  return;
109
296
  }
110
297
 
111
- const successCount = filtered.filter(e => e.outcome && e.outcome.status === 'success').length;
112
- const failCount = filtered.filter(e => e.outcome && e.outcome.status === 'failed').length;
113
-
114
- const lines = filtered.map(formatOutcome);
115
- const summary = [
116
- `[Evolution Memory] Recent ${filtered.length} outcomes (${successCount} success, ${failCount} failed):`,
117
- ...lines,
118
- '',
119
- 'Use successful approaches. Avoid repeating failed patterns.',
120
- ].join('\n');
121
-
298
+ const out = parts.join('\n\n');
122
299
  process.stdout.write(JSON.stringify({
123
- agent_message: summary,
124
- additionalContext: summary,
300
+ agent_message: out,
301
+ additionalContext: out,
125
302
  }));
126
303
  }
127
304
 
128
- main();
305
+ // Run as a hook when invoked directly; expose pure helpers for unit tests when
306
+ // required as a module. Guarding on require.main keeps the direct-execution
307
+ // behavior (the hosts run `node evolver-session-start.js`) unchanged.
308
+ if (require.main === module) {
309
+ main();
310
+ } else {
311
+ module.exports = { belongsToWorkspace };
312
+ }
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
  };