@evomap/evolver 1.91.2 → 1.92.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 (66) hide show
  1. package/README.md +13 -7
  2. package/index.js +150 -71
  3. package/package.json +2 -1
  4. package/src/adapters/scripts/_runtimePaths.js +10 -1
  5. package/src/adapters/scripts/evolver-session-end.js +10 -1
  6. package/src/canonicalIdentityLock.js +455 -0
  7. package/src/evolve/guards.js +1 -1
  8. package/src/evolve/pipeline/collect.js +1 -1
  9. package/src/evolve/pipeline/dispatch.js +1 -1
  10. package/src/evolve/pipeline/enrich.js +1 -1
  11. package/src/evolve/pipeline/hub.js +1 -1
  12. package/src/evolve/pipeline/select.js +1 -1
  13. package/src/evolve/pipeline/signals.js +1 -1
  14. package/src/evolve/utils.js +1 -1
  15. package/src/evolve.js +1 -1
  16. package/src/gep/a2aProtocol.js +1 -5175
  17. package/src/gep/antiAbuseTelemetry.js +1 -233
  18. package/src/gep/assetStore.js +56 -12
  19. package/src/gep/autoDistillConv.js +1 -205
  20. package/src/gep/autoDistillLlm.js +1 -315
  21. package/src/gep/candidateEval.js +1 -92
  22. package/src/gep/candidates.js +1 -1
  23. package/src/gep/contentHash.js +1 -30
  24. package/src/gep/conversationDistiller.js +1 -270
  25. package/src/gep/conversationSniffer.js +1 -266
  26. package/src/gep/crypto.js +1 -93
  27. package/src/gep/curriculum.js +1 -1
  28. package/src/gep/deviceId.js +1 -218
  29. package/src/gep/envFingerprint.js +1 -118
  30. package/src/gep/epigenetics.js +1 -31
  31. package/src/gep/execBridge.js +1 -712
  32. package/src/gep/explore.js +1 -289
  33. package/src/gep/hash.js +1 -15
  34. package/src/gep/hubFetch.js +1 -672
  35. package/src/gep/hubReview.js +1 -212
  36. package/src/gep/hubSearch.js +1 -544
  37. package/src/gep/hubVerify.js +1 -306
  38. package/src/gep/learningSignals.js +1 -1
  39. package/src/gep/memoryGraph.js +1 -1449
  40. package/src/gep/memoryGraphAdapter.js +1 -203
  41. package/src/gep/mutation.js +1 -1
  42. package/src/gep/narrativeMemory.js +1 -1
  43. package/src/gep/openPRRegistry.js +1 -205
  44. package/src/gep/personality.js +1 -1
  45. package/src/gep/policyCheck.js +1 -599
  46. package/src/gep/prompt.js +1 -1
  47. package/src/gep/recallInject.js +1 -409
  48. package/src/gep/recallVerifier.js +1 -318
  49. package/src/gep/reflection.js +1 -1
  50. package/src/gep/savingsCore.js +1 -1
  51. package/src/gep/schemas/gene.js +2 -0
  52. package/src/gep/selector.js +1 -1
  53. package/src/gep/skillDistiller.js +1 -1294
  54. package/src/gep/solidify.js +1 -1
  55. package/src/gep/strategy.js +1 -136
  56. package/src/gep/syncAsset.js +1 -0
  57. package/src/gep/tokenSavings.js +1 -1
  58. package/src/gep/trajectoryExport.js +1 -3841
  59. package/src/gep/workspaceKeychain.js +1 -174
  60. package/src/proxy/extensions/traceControl.js +1 -123
  61. package/src/proxy/index.js +136 -8
  62. package/src/proxy/inject.js +1 -52
  63. package/src/proxy/lifecycle/manager.js +279 -18
  64. package/src/proxy/mailbox/state.js +60 -29
  65. package/src/proxy/trace/extractor.js +1 -2355
  66. package/src/proxy/trace/usage.js +1 -105
@@ -0,0 +1,455 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const crypto = require('crypto');
6
+ const { spawnSync } = require('child_process');
7
+
8
+ const PRIVATE_DIR_MODE = 0o700;
9
+ const PRIVATE_FILE_MODE = 0o600;
10
+ const DEFAULT_LOCK_WAIT_MS = 10;
11
+ const DEFAULT_LOCK_TIMEOUT_MS = 10_000;
12
+ const UNKNOWN_OWNER_STALE_MS = 60_000;
13
+ const heldLocks = new Map();
14
+ let lockWaitMs = DEFAULT_LOCK_WAIT_MS;
15
+ let lockTimeoutMs = DEFAULT_LOCK_TIMEOUT_MS;
16
+ let beforeAbandonedLockUnlinkForTesting = null;
17
+ let processStartIdentityReaderForTesting = null;
18
+
19
+ function sleepSync(ms) {
20
+ try {
21
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
22
+ } catch {
23
+ const end = Date.now() + ms;
24
+ while (Date.now() < end) {}
25
+ }
26
+ }
27
+
28
+ function processIsAlive(pid) {
29
+ if (!Number.isInteger(pid) || pid <= 0) return null;
30
+ try {
31
+ process.kill(pid, 0);
32
+ return true;
33
+ } catch (e) {
34
+ if (e && e.code === 'ESRCH') return false;
35
+ if (e && e.code === 'EPERM') return true;
36
+ return null;
37
+ }
38
+ }
39
+
40
+ function readLinuxProcessStartIdentity(pid) {
41
+ let bootId;
42
+ let statContents;
43
+ try {
44
+ bootId = fs.readFileSync('/proc/sys/kernel/random/boot_id', 'utf8').trim();
45
+ statContents = fs.readFileSync(`/proc/${pid}/stat`, 'utf8').trim();
46
+ } catch {
47
+ return null;
48
+ }
49
+ if (!/^[a-f0-9-]{36}$/i.test(bootId)) return null;
50
+
51
+ // A process name may contain spaces and parentheses, so fields after comm
52
+ // must be parsed from the final closing parenthesis. starttime is field 22.
53
+ const closeParen = statContents.lastIndexOf(')');
54
+ const openParen = statContents.indexOf('(');
55
+ if (openParen <= 0 || closeParen <= openParen) return null;
56
+ const statPid = Number(statContents.slice(0, openParen).trim());
57
+ if (statPid !== pid) return null;
58
+ const fieldsAfterComm = statContents.slice(closeParen + 1).trim().split(/\s+/);
59
+ const startTimeTicks = fieldsAfterComm[19];
60
+ if (!/^\d+$/.test(startTimeTicks || '')) return null;
61
+ return `linux:${bootId}:${startTimeTicks}`;
62
+ }
63
+
64
+ function runDarwinIdentityCommand(file, args) {
65
+ const result = spawnSync(file, args, {
66
+ encoding: 'utf8',
67
+ env: { ...process.env, LANG: 'C', LC_ALL: 'C', TZ: 'UTC' },
68
+ maxBuffer: 16 * 1024,
69
+ timeout: 1_000,
70
+ windowsHide: true,
71
+ });
72
+ if (result.error || result.status !== 0 || typeof result.stdout !== 'string') return null;
73
+ return result.stdout.trim();
74
+ }
75
+
76
+ function readDarwinProcessStartIdentity(pid) {
77
+ const bootOutput = runDarwinIdentityCommand('/usr/sbin/sysctl', ['-n', 'kern.boottime']);
78
+ const bootMatch = /\{\s*sec\s*=\s*(\d+)\s*,\s*usec\s*=\s*(\d+)\s*\}/.exec(bootOutput || '');
79
+ if (!bootMatch) return null;
80
+
81
+ const processOutput = runDarwinIdentityCommand('/bin/ps', [
82
+ '-o', 'pid=',
83
+ '-o', 'lstart=',
84
+ '-p', String(pid),
85
+ ]);
86
+ const processMatch = /^(\d+)\s+(.+)$/.exec(processOutput || '');
87
+ if (!processMatch || Number(processMatch[1]) !== pid) return null;
88
+ const startedAt = processMatch[2].trim().replace(/\s+/g, ' ');
89
+ if (!startedAt) return null;
90
+ return `darwin:${bootMatch[1]}.${bootMatch[2]}:${startedAt}`;
91
+ }
92
+
93
+ function readProcessStartIdentity(pid) {
94
+ if (!Number.isInteger(pid) || pid <= 0) return null;
95
+ if (processStartIdentityReaderForTesting) {
96
+ const value = processStartIdentityReaderForTesting(pid);
97
+ return typeof value === 'string' && value ? value : null;
98
+ }
99
+ if (process.platform === 'linux') return readLinuxProcessStartIdentity(pid);
100
+ if (process.platform === 'darwin') return readDarwinProcessStartIdentity(pid);
101
+ return null;
102
+ }
103
+
104
+ function ownerIsProvablyDead(owner) {
105
+ const alive = processIsAlive(owner.pid);
106
+ if (alive === false) return true;
107
+ if (alive !== true || !owner.processStartIdentity) return false;
108
+ const currentIdentity = readProcessStartIdentity(owner.pid);
109
+ return Boolean(currentIdentity && currentIdentity !== owner.processStartIdentity);
110
+ }
111
+
112
+ function readOwner(ownerFile) {
113
+ try {
114
+ const parsed = JSON.parse(fs.readFileSync(ownerFile, 'utf8'));
115
+ if (!parsed || typeof parsed !== 'object') return null;
116
+ const pid = Number(parsed.pid);
117
+ const token = typeof parsed.token === 'string' ? parsed.token : '';
118
+ const processStartIdentity = typeof parsed.processStartIdentity === 'string'
119
+ ? parsed.processStartIdentity
120
+ : '';
121
+ if (!Number.isInteger(pid) || pid <= 0 || !token) return null;
122
+ return { pid, token, processStartIdentity };
123
+ } catch {
124
+ return null;
125
+ }
126
+ }
127
+
128
+ function discoverOwner(lockDir) {
129
+ let names;
130
+ try {
131
+ names = fs.readdirSync(lockDir);
132
+ } catch {
133
+ return null;
134
+ }
135
+ if (names.length !== 1) return null;
136
+ const match = /^owner\.([a-zA-Z0-9-]+)\.json$/.exec(names[0]);
137
+ if (!match) return null;
138
+ const token = match[1];
139
+ const ownerFile = path.join(lockDir, names[0]);
140
+ let contents;
141
+ try {
142
+ contents = fs.readFileSync(ownerFile);
143
+ } catch {
144
+ return null;
145
+ }
146
+ const owner = readOwner(ownerFile);
147
+ if (!owner || owner.token !== token) {
148
+ return { kind: 'malformed', owner: null, ownerFile, token, contents };
149
+ }
150
+ return { kind: 'valid', owner, ownerFile, token, contents };
151
+ }
152
+
153
+ function readLockIdentity(lockDir) {
154
+ try {
155
+ const stat = fs.statSync(lockDir, { bigint: true });
156
+ return {
157
+ dev: String(stat.dev),
158
+ ino: String(stat.ino),
159
+ mtimeNs: String(stat.mtimeNs),
160
+ mtimeMs: Number(stat.mtimeMs),
161
+ };
162
+ } catch (e) {
163
+ if (e && e.code === 'ENOENT') return null;
164
+ throw e;
165
+ }
166
+ }
167
+
168
+ function sameOwner(left, right) {
169
+ return Boolean(
170
+ left &&
171
+ right &&
172
+ left.pid === right.pid &&
173
+ left.token === right.token &&
174
+ left.processStartIdentity === right.processStartIdentity
175
+ );
176
+ }
177
+
178
+ function sameLockIdentity(left, right) {
179
+ return Boolean(
180
+ left &&
181
+ right &&
182
+ left.dev === right.dev &&
183
+ left.ino === right.ino &&
184
+ left.mtimeNs === right.mtimeNs
185
+ );
186
+ }
187
+
188
+ function sameDiscoveredOwner(left, right) {
189
+ if (!left || !right || left.kind !== right.kind) return false;
190
+ if (left.ownerFile !== right.ownerFile || left.token !== right.token) return false;
191
+ if (!Buffer.isBuffer(left.contents) || !Buffer.isBuffer(right.contents)) return false;
192
+ if (!left.contents.equals(right.contents)) return false;
193
+ if (left.kind === 'valid') return sameOwner(left.owner, right.owner);
194
+ return true;
195
+ }
196
+
197
+ function currentAbandonedSnapshotMatches(lockDir, expected) {
198
+ const identity = readLockIdentity(lockDir);
199
+ if (!sameLockIdentity(identity, expected && expected.identity)) return false;
200
+ if (expected.kind === 'empty') {
201
+ try {
202
+ return fs.readdirSync(lockDir).length === 0;
203
+ } catch {
204
+ return false;
205
+ }
206
+ }
207
+ return sameDiscoveredOwner(discoverOwner(lockDir), expected);
208
+ }
209
+
210
+ function removeAbandonedLock(lockDir, expected) {
211
+ if (!expected || !currentAbandonedSnapshotMatches(lockDir, expected)) return false;
212
+
213
+ if (expected.kind === 'valid') {
214
+ if (!ownerIsProvablyDead(expected.owner)) return false;
215
+ } else if (Date.now() - expected.identity.mtimeMs <= UNKNOWN_OWNER_STALE_MS) {
216
+ return false;
217
+ }
218
+
219
+ // Reject replacements that happened after stale classification. The
220
+ // tokenized unlink below also protects the final post-check ABA window.
221
+ if (beforeAbandonedLockUnlinkForTesting) {
222
+ beforeAbandonedLockUnlinkForTesting({ lockDir, expected });
223
+ }
224
+
225
+ if (!currentAbandonedSnapshotMatches(lockDir, expected)) return false;
226
+ if (expected.kind === 'empty') {
227
+ try {
228
+ fs.rmdirSync(lockDir);
229
+ return true;
230
+ } catch (e) {
231
+ return Boolean(e && e.code === 'ENOENT');
232
+ }
233
+ }
234
+
235
+ try {
236
+ // Tokenized owner paths are the deletion CAS. If a successor replaced the
237
+ // stale directory after the final check, this exact old path is absent;
238
+ // it can never resolve to the successor's different owner token.
239
+ fs.unlinkSync(expected.ownerFile);
240
+ } catch {
241
+ return false;
242
+ }
243
+ const emptiedIdentity = readLockIdentity(lockDir);
244
+ try {
245
+ fs.rmdirSync(lockDir);
246
+ return true;
247
+ } catch (e) {
248
+ if (e && e.code === 'ENOENT') return true;
249
+ // If an unexpected directory entry prevented removal, restore the stale
250
+ // owner marker when this is still the same directory. That keeps a later
251
+ // safe recovery attempt possible instead of leaving an ownerless wedge.
252
+ try {
253
+ const after = readLockIdentity(lockDir);
254
+ if (sameLockIdentity(after, emptiedIdentity) && fs.readdirSync(lockDir).length === 0) {
255
+ fs.writeFileSync(expected.ownerFile, expected.contents, {
256
+ mode: PRIVATE_FILE_MODE,
257
+ flag: 'wx',
258
+ });
259
+ }
260
+ } catch { /* the failed removal remains authoritative */ }
261
+ return false;
262
+ }
263
+ }
264
+
265
+ function abandonedLockSnapshot(lockDir) {
266
+ const identity = readLockIdentity(lockDir);
267
+ if (!identity) return null;
268
+ const discovered = discoverOwner(lockDir);
269
+ if (!discovered) {
270
+ try {
271
+ if (fs.readdirSync(lockDir).length !== 0) return null;
272
+ } catch {
273
+ return null;
274
+ }
275
+ if (Date.now() - identity.mtimeMs <= UNKNOWN_OWNER_STALE_MS) return null;
276
+ return { kind: 'empty', identity };
277
+ }
278
+ if (discovered.kind === 'malformed') {
279
+ if (Date.now() - identity.mtimeMs <= UNKNOWN_OWNER_STALE_MS) return null;
280
+ return { ...discovered, identity };
281
+ }
282
+ const { owner } = discovered;
283
+ if (!ownerIsProvablyDead(owner)) return null;
284
+ return { ...discovered, identity };
285
+ }
286
+
287
+ function prepareOwnerFile(lockDir, token) {
288
+ const preparedOwnerFile = `${lockDir}.owner.${token}.tmp`;
289
+ const processStartIdentity = readProcessStartIdentity(process.pid);
290
+ let descriptor = null;
291
+ try {
292
+ fs.writeFileSync(preparedOwnerFile, JSON.stringify({
293
+ pid: process.pid,
294
+ token,
295
+ processStartIdentity,
296
+ }), {
297
+ encoding: 'utf8',
298
+ mode: PRIVATE_FILE_MODE,
299
+ flag: 'wx',
300
+ });
301
+ descriptor = fs.openSync(preparedOwnerFile, 'r');
302
+ fs.fdatasyncSync(descriptor);
303
+ fs.closeSync(descriptor);
304
+ descriptor = null;
305
+ const owner = readOwner(preparedOwnerFile);
306
+ if (
307
+ !owner ||
308
+ owner.token !== token ||
309
+ owner.pid !== process.pid ||
310
+ owner.processStartIdentity !== (processStartIdentity || '')
311
+ ) {
312
+ const err = new Error('failed to prepare canonical identity lock owner');
313
+ err.code = 'CANONICAL_IDENTITY_LOCK_OWNER_INVALID';
314
+ throw err;
315
+ }
316
+ return preparedOwnerFile;
317
+ } catch (e) {
318
+ if (descriptor !== null) {
319
+ try { fs.closeSync(descriptor); } catch { /* preparation error remains primary */ }
320
+ }
321
+ try { fs.unlinkSync(preparedOwnerFile); } catch { /* preparation error remains primary */ }
322
+ throw e;
323
+ }
324
+ }
325
+
326
+ function releaseCanonicalIdentityLock(lockDir, ownerFile, token) {
327
+ const owner = readOwner(ownerFile);
328
+ if (!owner || owner.token !== token) {
329
+ const err = new Error('canonical identity lock ownership was lost');
330
+ err.code = 'CANONICAL_IDENTITY_LOCK_LOST';
331
+ throw err;
332
+ }
333
+
334
+ const releaseDir = `${lockDir}.release.${token}`;
335
+ fs.renameSync(lockDir, releaseDir);
336
+ const releaseOwnerFile = path.join(releaseDir, path.basename(ownerFile));
337
+ const movedOwner = readOwner(releaseOwnerFile);
338
+ if (!movedOwner || movedOwner.token !== token) {
339
+ try { fs.renameSync(releaseDir, lockDir); } catch { /* ownership error remains primary */ }
340
+ const err = new Error('canonical identity lock ownership was lost');
341
+ err.code = 'CANONICAL_IDENTITY_LOCK_LOST';
342
+ throw err;
343
+ }
344
+ fs.unlinkSync(releaseOwnerFile);
345
+ fs.rmdirSync(releaseDir);
346
+ }
347
+
348
+ function acquireCanonicalIdentityLock(nodeIdFile) {
349
+ const canonicalFile = path.resolve(nodeIdFile);
350
+ const lockDir = `${canonicalFile}.tuple.lock`;
351
+ const token = `${process.pid}-${crypto.randomBytes(12).toString('hex')}`;
352
+ const ownerFile = path.join(lockDir, `owner.${token}.json`);
353
+ const startedAt = Date.now();
354
+ let preparedOwnerFile = null;
355
+
356
+ fs.mkdirSync(path.dirname(canonicalFile), { recursive: true, mode: PRIVATE_DIR_MODE });
357
+ preparedOwnerFile = prepareOwnerFile(lockDir, token);
358
+ try {
359
+ while (true) {
360
+ try {
361
+ fs.mkdirSync(lockDir, { mode: PRIVATE_DIR_MODE });
362
+ try {
363
+ fs.renameSync(preparedOwnerFile, ownerFile);
364
+ preparedOwnerFile = null;
365
+ } catch (e) {
366
+ try { fs.rmdirSync(lockDir); } catch { /* publication error remains primary */ }
367
+ throw e;
368
+ }
369
+ return function releaseLock() {
370
+ releaseCanonicalIdentityLock(lockDir, ownerFile, token);
371
+ };
372
+ } catch (e) {
373
+ if (!e || e.code !== 'EEXIST') throw e;
374
+ const abandoned = abandonedLockSnapshot(lockDir);
375
+ if (abandoned) {
376
+ if (removeAbandonedLock(lockDir, abandoned)) continue;
377
+ }
378
+ if (Date.now() - startedAt > lockTimeoutMs) {
379
+ const err = new Error('timed out waiting for canonical identity lock');
380
+ err.code = 'CANONICAL_IDENTITY_LOCK_TIMEOUT';
381
+ throw err;
382
+ }
383
+ sleepSync(lockWaitMs);
384
+ }
385
+ }
386
+ } finally {
387
+ if (preparedOwnerFile) {
388
+ try { fs.unlinkSync(preparedOwnerFile); } catch { /* acquisition error remains primary */ }
389
+ }
390
+ }
391
+ }
392
+
393
+ function _setCanonicalIdentityLockTimingForTesting(options) {
394
+ const next = options || {};
395
+ lockWaitMs = Number.isFinite(next.waitMs) && next.waitMs >= 0
396
+ ? Number(next.waitMs)
397
+ : DEFAULT_LOCK_WAIT_MS;
398
+ lockTimeoutMs = Number.isFinite(next.timeoutMs) && next.timeoutMs >= 0
399
+ ? Number(next.timeoutMs)
400
+ : DEFAULT_LOCK_TIMEOUT_MS;
401
+ }
402
+
403
+ function _resetCanonicalIdentityLockTimingForTesting() {
404
+ lockWaitMs = DEFAULT_LOCK_WAIT_MS;
405
+ lockTimeoutMs = DEFAULT_LOCK_TIMEOUT_MS;
406
+ }
407
+
408
+ function _setBeforeAbandonedLockUnlinkForTesting(callback) {
409
+ beforeAbandonedLockUnlinkForTesting = typeof callback === 'function' ? callback : null;
410
+ }
411
+
412
+ function _setProcessStartIdentityReaderForTesting(callback) {
413
+ processStartIdentityReaderForTesting = typeof callback === 'function' ? callback : null;
414
+ }
415
+
416
+ /**
417
+ * Run a synchronous canonical identity/credential operation under the shared
418
+ * cross-process lock. Nested calls in the same process reuse the lock.
419
+ *
420
+ * @template T
421
+ * @param {string} nodeIdFile
422
+ * @param {() => T} operation
423
+ * @returns {T}
424
+ */
425
+ function withCanonicalIdentityLock(nodeIdFile, operation) {
426
+ if (typeof operation !== 'function') throw new TypeError('operation must be a function');
427
+ const key = path.resolve(nodeIdFile);
428
+ const held = heldLocks.get(key);
429
+ if (held) {
430
+ held.depth += 1;
431
+ try {
432
+ return operation();
433
+ } finally {
434
+ held.depth -= 1;
435
+ }
436
+ }
437
+
438
+ const release = acquireCanonicalIdentityLock(key);
439
+ heldLocks.set(key, { depth: 1 });
440
+ try {
441
+ return operation();
442
+ } finally {
443
+ heldLocks.delete(key);
444
+ release();
445
+ }
446
+ }
447
+
448
+ module.exports = {
449
+ acquireCanonicalIdentityLock,
450
+ withCanonicalIdentityLock,
451
+ _setCanonicalIdentityLockTimingForTesting,
452
+ _resetCanonicalIdentityLockTimingForTesting,
453
+ _setBeforeAbandonedLockUnlinkForTesting,
454
+ _setProcessStartIdentityReaderForTesting,
455
+ };