@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.
- 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 +202 -1
- package/src/adapters/scripts/evolver-session-end.js +160 -98
- package/src/adapters/scripts/evolver-session-start.js +227 -43
- 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 +233 -6
- 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
|
@@ -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
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
124
|
-
additionalContext:
|
|
300
|
+
agent_message: out,
|
|
301
|
+
additionalContext: out,
|
|
125
302
|
}));
|
|
126
303
|
}
|
|
127
304
|
|
|
128
|
-
|
|
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
|
-
|
|
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
|
};
|