@evomap/evolver-core 2.0.0-beta.2 → 2.0.0-beta.4
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/dist/algo/candidateAssembly.js +9 -6
- package/dist/algo/cycleEngine.d.ts +11 -0
- package/dist/algo/cycleEngine.js +12 -6
- package/dist/algo/cycleFailureClassifier.d.ts +1 -1
- package/dist/algo/geneSelection.d.ts +12 -1
- package/dist/algo/geneSelection.js +24 -8
- package/dist/algo/index.d.ts +1 -0
- package/dist/algo/index.js +1 -0
- package/dist/algo/memoryGraph.d.ts +62 -0
- package/dist/algo/memoryGraph.js +86 -0
- package/dist/algo/orchestrator.d.ts +3 -0
- package/dist/algo/orchestrator.js +14 -2
- package/dist/assetstore/assetSidecarRecords.d.ts +23 -0
- package/dist/assetstore/assetSidecarRecords.js +142 -0
- package/dist/assetstore/assetSidecarRecovery.d.ts +48 -0
- package/dist/assetstore/assetSidecarRecovery.js +288 -0
- package/dist/assetstore/assetStoreHealth.d.ts +75 -0
- package/dist/assetstore/assetStoreHealth.js +277 -0
- package/dist/assetstore/assetStoreLayout.d.ts +2 -0
- package/dist/assetstore/assetStoreLayout.js +6 -0
- package/dist/assetstore/assetStoreStorage.d.ts +42 -0
- package/dist/assetstore/assetStoreStorage.js +318 -0
- package/dist/assetstore/assetSyncLedger.d.ts +5 -1
- package/dist/assetstore/assetSyncLedger.js +44 -64
- package/dist/assetstore/index.d.ts +2 -0
- package/dist/assetstore/index.js +2 -0
- package/dist/assetstore/localJsonl.d.ts +1 -0
- package/dist/assetstore/localJsonl.js +36 -32
- package/dist/assetstore/provenance.d.ts +13 -0
- package/dist/assetstore/provenance.js +60 -84
- package/dist/assetstore/provider.d.ts +2 -0
- package/dist/assetstore/reviewFilter.js +3 -1
- package/dist/assetstore/reviewLedger.d.ts +8 -2
- package/dist/assetstore/reviewLedger.js +71 -45
- package/dist/benchmark/index.d.ts +2 -1
- package/dist/benchmark/index.js +2 -1
- package/dist/benchmark/triggerShift.d.ts +62 -0
- package/dist/benchmark/triggerShift.js +106 -0
- package/dist/events/ingest.d.ts +1 -1
- package/dist/events/ingest.js +2 -0
- package/dist/events/paths.d.ts +1 -1
- package/dist/events/paths.js +2 -2
- package/dist/exec/autoExec.d.ts +6 -1
- package/dist/exec/autoExec.js +31 -0
- package/dist/exec/autonomousCycle.d.ts +2 -0
- package/dist/exec/autonomousCycle.js +5 -0
- package/dist/exec/claudeBridge.d.ts +7 -2
- package/dist/exec/claudeBridge.js +92 -14
- package/dist/exec/prompt.js +9 -0
- package/dist/exec/runnerRegistry.d.ts +56 -12
- package/dist/exec/runnerRegistry.js +272 -22
- package/dist/hub/bindings.js +12 -2
- package/dist/ops/savingsCore.js +1 -2
- package/dist/ops/selfUpdate.d.ts +10 -1
- package/dist/ops/selfUpdate.js +64 -15
- package/dist/util/fileLock.d.ts +19 -2
- package/dist/util/fileLock.js +166 -31
- package/package.json +5 -1
package/dist/util/fileLock.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import {
|
|
2
|
+
import { closeSync, constants, fstatSync, lstatSync, openSync, readSync, renameSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
3
3
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
4
4
|
export function syncSleep(ms) {
|
|
5
5
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
@@ -7,11 +7,21 @@ export function syncSleep(ms) {
|
|
|
7
7
|
const ownedLocks = new Map();
|
|
8
8
|
export class LockTimeoutError extends Error {
|
|
9
9
|
code = 'LOCK_TIMEOUT';
|
|
10
|
-
constructor(
|
|
11
|
-
super(
|
|
10
|
+
constructor(_lockPath) {
|
|
11
|
+
super('获取文件锁超时');
|
|
12
12
|
this.name = 'LockTimeoutError';
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
|
+
export class UnsafeLockPathError extends Error {
|
|
16
|
+
reason;
|
|
17
|
+
code = 'UNSAFE_LOCK_PATH';
|
|
18
|
+
constructor(reason) {
|
|
19
|
+
super(`不安全的文件锁路径: ${reason}`);
|
|
20
|
+
this.reason = reason;
|
|
21
|
+
this.name = 'UnsafeLockPathError';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export const MAX_LOCK_OWNER_BYTES = 4096;
|
|
15
25
|
function lockKey(lockPath) {
|
|
16
26
|
return resolve(lockPath);
|
|
17
27
|
}
|
|
@@ -24,6 +34,73 @@ function serializeOwner(owner) {
|
|
|
24
34
|
function isErrno(error, code) {
|
|
25
35
|
return typeof error === 'object' && error !== null && error.code === code;
|
|
26
36
|
}
|
|
37
|
+
function noFollowFlag() {
|
|
38
|
+
return constants['O_NOFOLLOW'] ?? 0;
|
|
39
|
+
}
|
|
40
|
+
function assertRegularOwnerStat(stat) {
|
|
41
|
+
if (stat.isSymbolicLink())
|
|
42
|
+
throw new UnsafeLockPathError('symlink');
|
|
43
|
+
if (!stat.isFile())
|
|
44
|
+
throw new UnsafeLockPathError('not_regular_file');
|
|
45
|
+
if (stat.size > BigInt(MAX_LOCK_OWNER_BYTES))
|
|
46
|
+
throw new UnsafeLockPathError('owner_too_large');
|
|
47
|
+
}
|
|
48
|
+
function currentOwnerStat(path) {
|
|
49
|
+
try {
|
|
50
|
+
const stat = lstatSync(path, { bigint: true });
|
|
51
|
+
assertRegularOwnerStat(stat);
|
|
52
|
+
return stat;
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (isErrno(error, 'ENOENT'))
|
|
56
|
+
return null;
|
|
57
|
+
if (isErrno(error, 'EACCES') || isErrno(error, 'EPERM')) {
|
|
58
|
+
throw new UnsafeLockPathError('permission_denied');
|
|
59
|
+
}
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function readOwnerBounded(path) {
|
|
64
|
+
const before = currentOwnerStat(path);
|
|
65
|
+
if (before === null)
|
|
66
|
+
return null;
|
|
67
|
+
let fd;
|
|
68
|
+
try {
|
|
69
|
+
fd = openSync(path, constants.O_RDONLY | noFollowFlag());
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
if (isErrno(error, 'ENOENT'))
|
|
73
|
+
return null;
|
|
74
|
+
if (isErrno(error, 'ELOOP'))
|
|
75
|
+
throw new UnsafeLockPathError('symlink');
|
|
76
|
+
if (isErrno(error, 'EACCES') || isErrno(error, 'EPERM')) {
|
|
77
|
+
throw new UnsafeLockPathError('permission_denied');
|
|
78
|
+
}
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
const opened = fstatSync(fd, { bigint: true });
|
|
83
|
+
assertRegularOwnerStat(opened);
|
|
84
|
+
const current = currentOwnerStat(path);
|
|
85
|
+
if (current === null || current.dev !== opened.dev || current.ino !== opened.ino) {
|
|
86
|
+
throw new UnsafeLockPathError('path_changed');
|
|
87
|
+
}
|
|
88
|
+
const buffer = Buffer.alloc(MAX_LOCK_OWNER_BYTES + 1);
|
|
89
|
+
let total = 0;
|
|
90
|
+
while (total < buffer.length) {
|
|
91
|
+
const read = readSync(fd, buffer, total, buffer.length - total, total);
|
|
92
|
+
if (read === 0)
|
|
93
|
+
break;
|
|
94
|
+
total += read;
|
|
95
|
+
}
|
|
96
|
+
if (total > MAX_LOCK_OWNER_BYTES)
|
|
97
|
+
throw new UnsafeLockPathError('owner_too_large');
|
|
98
|
+
return buffer.subarray(0, total).toString('utf8');
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
closeSync(fd);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
27
104
|
/** True if pid is a live process (cross-platform via signal 0; EPERM = exists but owned by another user). */
|
|
28
105
|
function pidAlive(pid) {
|
|
29
106
|
if (!Number.isInteger(pid) || pid <= 0)
|
|
@@ -73,10 +150,13 @@ function parseOwner(raw) {
|
|
|
73
150
|
/** Owner recorded in the lock file; null if missing/unparseable. */
|
|
74
151
|
function lockOwner(lockPath) {
|
|
75
152
|
try {
|
|
76
|
-
|
|
153
|
+
const raw = readOwnerBounded(lockPath);
|
|
154
|
+
return raw === null ? null : parseOwner(raw);
|
|
77
155
|
}
|
|
78
156
|
catch (error) {
|
|
79
|
-
|
|
157
|
+
// A valid owner can release and another waiter can acquire between lstat/open/fstat. Treat that snapshot as
|
|
158
|
+
// transiently unavailable: callers remain conservative (no steal/delete) and the acquire loop retries.
|
|
159
|
+
if (error instanceof UnsafeLockPathError && error.reason === 'path_changed')
|
|
80
160
|
return null;
|
|
81
161
|
throw error;
|
|
82
162
|
}
|
|
@@ -100,17 +180,30 @@ function removeFileIfExists(path) {
|
|
|
100
180
|
}
|
|
101
181
|
}
|
|
102
182
|
function createLockFile(lockPath, owner, trackOwnership) {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
183
|
+
for (let permissionAttempt = 0; permissionAttempt < 2; permissionAttempt++) {
|
|
184
|
+
try {
|
|
185
|
+
writeFileSync(lockPath, serializeOwner(owner), { flag: 'wx', mode: 0o600 });
|
|
186
|
+
if (trackOwnership)
|
|
187
|
+
ownedLocks.set(lockKey(lockPath), { owner, releasePending: false });
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
if (isErrno(error, 'EEXIST'))
|
|
192
|
+
return false;
|
|
193
|
+
if (isErrno(error, 'EACCES') || isErrno(error, 'EPERM')) {
|
|
194
|
+
// Windows can report EPERM instead of EEXIST during contention. A regular owner is busy; if the owner
|
|
195
|
+
// disappeared before lstat, retry create once to distinguish that release race from a persistent ACL
|
|
196
|
+
// failure. Unsafe/inaccessible paths still throw from currentOwnerStat and remain fail closed.
|
|
197
|
+
if (currentOwnerStat(lockPath) !== null)
|
|
198
|
+
return false;
|
|
199
|
+
if (permissionAttempt === 0)
|
|
200
|
+
continue;
|
|
201
|
+
throw new UnsafeLockPathError('permission_denied');
|
|
202
|
+
}
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
113
205
|
}
|
|
206
|
+
throw new UnsafeLockPathError('permission_denied');
|
|
114
207
|
}
|
|
115
208
|
function tryAcquireMutationGuard(lockPath) {
|
|
116
209
|
const guardPath = mutationGuardPath(lockPath);
|
|
@@ -185,6 +278,12 @@ function reclaimStaleLock(lockPath) {
|
|
|
185
278
|
* synchronous critical sections (append-only writes).
|
|
186
279
|
*/
|
|
187
280
|
export function acquireLock(lockPath, opts = {}) {
|
|
281
|
+
const pending = ownedLocks.get(lockKey(lockPath));
|
|
282
|
+
if (pending?.releasePending) {
|
|
283
|
+
const released = releaseLock(lockPath);
|
|
284
|
+
if (!released.released)
|
|
285
|
+
throw new LockReleaseError(released.reason);
|
|
286
|
+
}
|
|
188
287
|
const maxTries = opts.maxTries ?? 300;
|
|
189
288
|
const waitMs = opts.waitMs ?? 10;
|
|
190
289
|
for (let i = 0; i < maxTries; i++) {
|
|
@@ -200,28 +299,64 @@ export function acquireLock(lockPath, opts = {}) {
|
|
|
200
299
|
}
|
|
201
300
|
syncSleep(waitMs);
|
|
202
301
|
}
|
|
203
|
-
throw new LockTimeoutError(
|
|
302
|
+
throw new LockTimeoutError();
|
|
204
303
|
}
|
|
205
304
|
export function releaseLock(lockPath) {
|
|
206
305
|
const key = lockKey(lockPath);
|
|
207
|
-
const
|
|
208
|
-
if (
|
|
209
|
-
return;
|
|
306
|
+
const owned = ownedLocks.get(key);
|
|
307
|
+
if (owned === undefined)
|
|
308
|
+
return { released: true, reason: 'not_owned' };
|
|
309
|
+
const failed = (reason) => {
|
|
310
|
+
owned.releasePending = true;
|
|
311
|
+
return { released: false, reason };
|
|
312
|
+
};
|
|
313
|
+
// Release must not use lockOwner(): that helper intentionally folds path_changed into a conservative null
|
|
314
|
+
// for acquire retries. Here, an inconclusive read marks a pending release so the next acquire can retry or
|
|
315
|
+
// surface LOCK_RELEASE_FAILED instead of silently timing out behind this process's own PID.
|
|
316
|
+
let raw;
|
|
210
317
|
try {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
318
|
+
raw = readOwnerBounded(lockPath);
|
|
319
|
+
}
|
|
320
|
+
catch (error) {
|
|
321
|
+
return failed(error instanceof UnsafeLockPathError ? error.reason : 'release_failed');
|
|
322
|
+
}
|
|
323
|
+
if (raw === null) {
|
|
324
|
+
ownedLocks.delete(key);
|
|
325
|
+
return { released: true, reason: 'missing' };
|
|
326
|
+
}
|
|
327
|
+
const current = parseOwner(raw);
|
|
328
|
+
if (current === null)
|
|
329
|
+
return failed('invalid_owner');
|
|
330
|
+
if (!sameOwner(current, owned.owner)) {
|
|
331
|
+
ownedLocks.delete(key);
|
|
332
|
+
return { released: true, reason: 'ownership_changed' };
|
|
333
|
+
}
|
|
334
|
+
const releasedPath = sidecarPath(lockPath, 'released');
|
|
335
|
+
try {
|
|
336
|
+
renameSync(lockPath, releasedPath);
|
|
337
|
+
}
|
|
338
|
+
catch (error) {
|
|
339
|
+
if (isErrno(error, 'ENOENT')) {
|
|
340
|
+
ownedLocks.delete(key);
|
|
341
|
+
return { released: true, reason: 'missing' };
|
|
221
342
|
}
|
|
343
|
+
return failed('release_failed');
|
|
344
|
+
}
|
|
345
|
+
ownedLocks.delete(key);
|
|
346
|
+
try {
|
|
222
347
|
removeFileIfExists(releasedPath);
|
|
223
348
|
}
|
|
224
|
-
|
|
225
|
-
|
|
349
|
+
catch {
|
|
350
|
+
return { released: true, reason: 'released_with_cleanup_error' };
|
|
351
|
+
}
|
|
352
|
+
return { released: true, reason: 'released' };
|
|
353
|
+
}
|
|
354
|
+
export class LockReleaseError extends Error {
|
|
355
|
+
reason;
|
|
356
|
+
code = 'LOCK_RELEASE_FAILED';
|
|
357
|
+
constructor(reason) {
|
|
358
|
+
super(`文件锁释放未完成: ${reason}`);
|
|
359
|
+
this.reason = reason;
|
|
360
|
+
this.name = 'LockReleaseError';
|
|
226
361
|
}
|
|
227
362
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evomap/evolver-core",
|
|
3
|
-
"version": "2.0.0-beta.
|
|
3
|
+
"version": "2.0.0-beta.4",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "hub-无关核心: 算法引擎/原材料/mailbox/资产库/workflow",
|
|
@@ -17,6 +17,10 @@
|
|
|
17
17
|
"zod": "^3.24.1",
|
|
18
18
|
"ulid": "^2.3.0"
|
|
19
19
|
},
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/EvoMap/evolver.git"
|
|
23
|
+
},
|
|
20
24
|
"publishConfig": {
|
|
21
25
|
"access": "public",
|
|
22
26
|
"tag": "v2-beta"
|