@evomap/evolver-core 2.0.0-beta.0 → 2.0.0-beta.10
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/assets/gep/genes.jsonl +5 -0
- package/dist/algo/candidateAssembly.js +23 -14
- package/dist/algo/capabilityCandidates.d.ts +2 -0
- package/dist/algo/capabilityCandidates.js +5 -0
- package/dist/algo/conversationSniffer.d.ts +18 -0
- package/dist/algo/conversationSniffer.js +149 -0
- package/dist/algo/cycleEngine.d.ts +11 -0
- package/dist/algo/cycleEngine.js +14 -6
- package/dist/algo/cycleFailureClassifier.d.ts +1 -1
- package/dist/algo/cycleFailureClassifier.js +13 -5
- package/dist/algo/geneIntake.d.ts +14 -4
- package/dist/algo/geneIntake.js +37 -9
- package/dist/algo/geneSelection.d.ts +18 -1
- package/dist/algo/geneSelection.js +42 -18
- package/dist/algo/index.d.ts +2 -0
- package/dist/algo/index.js +2 -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 +28 -0
- package/dist/assetstore/assetSidecarRecords.js +376 -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 +327 -0
- package/dist/assetstore/assetSyncLedger.d.ts +118 -0
- package/dist/assetstore/assetSyncLedger.js +745 -0
- package/dist/assetstore/index.d.ts +5 -1
- package/dist/assetstore/index.js +5 -1
- package/dist/assetstore/localJsonl.d.ts +12 -3
- package/dist/assetstore/localJsonl.js +131 -39
- package/dist/assetstore/pendingSignals.js +3 -1
- package/dist/assetstore/provenance.d.ts +27 -4
- package/dist/assetstore/provenance.js +107 -56
- package/dist/assetstore/provider.d.ts +26 -0
- package/dist/assetstore/provider.js +50 -0
- package/dist/assetstore/reviewFilter.js +5 -2
- package/dist/assetstore/reviewLedger.d.ts +14 -2
- package/dist/assetstore/reviewLedger.js +78 -43
- package/dist/assetstore/seedGenes.d.ts +3 -0
- package/dist/assetstore/seedGenes.js +134 -0
- package/dist/benchmark/antiGeneBenchmark.d.ts +2 -0
- package/dist/benchmark/antiGeneBenchmark.js +11 -1
- package/dist/benchmark/antiGeneImpact.d.ts +16 -0
- package/dist/benchmark/antiGeneImpact.js +34 -0
- package/dist/benchmark/antiGeneRollout.d.ts +2 -0
- package/dist/benchmark/antiGeneRollout.js +13 -1
- 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/eventArchive.d.ts +65 -0
- package/dist/events/eventArchive.js +343 -0
- package/dist/events/eventStore.js +2 -12
- package/dist/events/ingest.d.ts +1 -1
- package/dist/events/ingest.js +9 -0
- package/dist/events/paths.d.ts +10 -10
- package/dist/events/paths.js +20 -20
- package/dist/events/public.d.ts +3 -1
- package/dist/events/public.js +2 -1
- package/dist/events/retention.d.ts +24 -1
- package/dist/events/retention.js +133 -37
- package/dist/exec/autoExec.d.ts +6 -1
- package/dist/exec/autoExec.js +34 -0
- package/dist/exec/autonomousCycle.d.ts +2 -0
- package/dist/exec/autonomousCycle.js +5 -0
- package/dist/exec/claudeBridge.d.ts +21 -3
- package/dist/exec/claudeBridge.js +413 -26
- package/dist/exec/openPrRegistry.d.ts +8 -2
- package/dist/exec/openPrRegistry.js +32 -22
- package/dist/exec/prompt.js +9 -0
- package/dist/exec/runnerRegistry.d.ts +90 -18
- package/dist/exec/runnerRegistry.js +603 -42
- package/dist/exec/selfPrObfuscation.d.ts +2 -1
- package/dist/exec/selfPrObfuscation.js +19 -6
- package/dist/hub/agentDirectory.d.ts +90 -0
- package/dist/hub/agentDirectory.js +104 -0
- package/dist/hub/bindings.js +23 -3
- package/dist/hub/capability.d.ts +14 -2
- package/dist/hub/fake.d.ts +4 -2
- package/dist/hub/fake.js +3 -2
- package/dist/hub/index.d.ts +1 -0
- package/dist/hub/index.js +1 -0
- package/dist/hub/sanitize.js +3 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/issueReporter/index.d.ts +156 -0
- package/dist/issueReporter/index.js +1688 -0
- package/dist/mailbox/catalog.js +3 -0
- package/dist/mailbox/ipcServer.js +2 -2
- package/dist/mailbox/store.d.ts +14 -0
- package/dist/mailbox/store.js +58 -6
- package/dist/material/consumer.js +9 -6
- package/dist/material/index.d.ts +1 -0
- package/dist/material/index.js +1 -0
- package/dist/material/materialArchive.d.ts +81 -0
- package/dist/material/materialArchive.js +466 -0
- package/dist/material/materialStore.d.ts +1 -0
- package/dist/material/materialStore.js +6 -13
- package/dist/ops/savingsCore.js +1 -2
- package/dist/ops/selfUpdate.d.ts +10 -1
- package/dist/ops/selfUpdate.js +64 -15
- package/dist/personality/schema.d.ts +12 -12
- package/dist/schema/common.d.ts +1 -1
- package/dist/schema/common.js +1 -1
- package/dist/schema/material.d.ts +5 -5
- package/dist/strategy/constraintAblation.d.ts +64 -0
- package/dist/strategy/constraintAblation.js +3074 -0
- package/dist/strategy/index.d.ts +2 -1
- package/dist/strategy/index.js +2 -1
- package/dist/trace/trajectoryExport.js +2 -2
- package/dist/util/fetchPort.d.ts +1 -0
- package/dist/util/fetchPort.js +11 -0
- package/dist/util/fileLock.d.ts +24 -5
- package/dist/util/fileLock.js +288 -72
- package/dist/util/index.d.ts +1 -0
- package/dist/util/index.js +1 -0
- package/dist/wire/geneHints.d.ts +42 -1
- package/dist/wire/geneHints.js +52 -1
- package/dist/wire/index.d.ts +14 -2
- package/dist/wire/index.js +1 -1
- package/dist/workflow/dsl.d.ts +24 -3
- package/dist/workflow/dsl.js +4 -0
- package/dist/workflow/engine.d.ts +5 -1
- package/dist/workflow/engine.js +3 -0
- package/dist/workflow/index.d.ts +3 -1
- package/dist/workflow/index.js +3 -1
- package/dist/workflow/runtime.d.ts +110 -0
- package/dist/workflow/runtime.js +1298 -0
- package/dist/workflow/stateStore.d.ts +172 -0
- package/dist/workflow/stateStore.js +1044 -0
- package/package.json +6 -1
package/dist/strategy/index.d.ts
CHANGED
package/dist/strategy/index.js
CHANGED
|
@@ -1558,7 +1558,7 @@ function runtimeSessionId(input) {
|
|
|
1558
1558
|
return explicit;
|
|
1559
1559
|
const sourcePath = stringValue(input.sourcePath);
|
|
1560
1560
|
const base = sourcePath.split(/[\\/]/).filter(Boolean).pop() ?? '';
|
|
1561
|
-
return base.replace(/\.jsonl?$/i, '') || 'unknown';
|
|
1561
|
+
return redactString(base.replace(/\.jsonl?$/i, '')) || 'unknown';
|
|
1562
1562
|
}
|
|
1563
1563
|
function nativeCallHasField(call, key) {
|
|
1564
1564
|
return Object.prototype.hasOwnProperty.call(call, key);
|
|
@@ -1704,7 +1704,7 @@ export function buildCodingTrajectoryFromSessionLog(input) {
|
|
|
1704
1704
|
session_id: sessionId,
|
|
1705
1705
|
source_kind: 'runtime_session',
|
|
1706
1706
|
source_agent: sourceAgent,
|
|
1707
|
-
source_path: sourcePath,
|
|
1707
|
+
source_path: redactString(sourcePath),
|
|
1708
1708
|
...(sessionModel ? { session_model: sessionModel } : {}),
|
|
1709
1709
|
...(provider !== sourceAgent ? { session_provider: provider } : {}),
|
|
1710
1710
|
...(sessionTools !== undefined ? { session_tools: redactRuntimeStructuredValue(sessionTools) } : {}),
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function isFetchForbiddenPort(port: number): boolean;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// https://fetch.spec.whatwg.org/#port-blocking
|
|
2
|
+
const FETCH_FORBIDDEN_PORTS = new Set([
|
|
3
|
+
1, 7, 9, 11, 13, 15, 17, 19, 20, 21, 22, 23, 25, 37, 42, 43, 53, 69, 77, 79, 87, 95,
|
|
4
|
+
101, 102, 103, 104, 109, 110, 111, 113, 115, 117, 119, 123, 135, 137, 139, 143, 161, 179,
|
|
5
|
+
389, 427, 465, 512, 513, 514, 515, 526, 530, 531, 532, 540, 548, 554, 556, 563, 587, 601,
|
|
6
|
+
636, 989, 990, 993, 995, 1719, 1720, 1723, 2049, 3659, 4045, 4190, 5060, 5061, 6000,
|
|
7
|
+
6566, 6665, 6666, 6667, 6668, 6669, 6679, 6697, 10080,
|
|
8
|
+
]);
|
|
9
|
+
export function isFetchForbiddenPort(port) {
|
|
10
|
+
return Number.isInteger(port) && FETCH_FORBIDDEN_PORTS.has(port);
|
|
11
|
+
}
|
package/dist/util/fileLock.d.ts
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
export declare function syncSleep(ms: number): void;
|
|
2
2
|
export declare class LockTimeoutError extends Error {
|
|
3
3
|
readonly code = "LOCK_TIMEOUT";
|
|
4
|
-
constructor(
|
|
4
|
+
constructor(_lockPath?: string);
|
|
5
5
|
}
|
|
6
|
+
export type UnsafeLockPathReason = 'symlink' | 'not_regular_file' | 'owner_too_large' | 'path_changed' | 'permission_denied' | 'invalid_owner';
|
|
7
|
+
export declare class UnsafeLockPathError extends Error {
|
|
8
|
+
readonly reason: UnsafeLockPathReason;
|
|
9
|
+
readonly code = "UNSAFE_LOCK_PATH";
|
|
10
|
+
constructor(reason: UnsafeLockPathReason);
|
|
11
|
+
}
|
|
12
|
+
export declare const MAX_LOCK_OWNER_BYTES = 4096;
|
|
6
13
|
export interface AcquireLockOptions {
|
|
7
14
|
maxTries?: number;
|
|
8
15
|
waitMs?: number;
|
|
@@ -13,12 +20,24 @@ export interface AcquireLockOptions {
|
|
|
13
20
|
* The lock file records the owner pid and token. If a waiter finds the lock held by a pid that is no longer
|
|
14
21
|
* alive (the owner crashed without releaseLock), it reclaims the stale lock instead of spinning
|
|
15
22
|
* until timeout — otherwise one crashed process would deadlock every future writer until the file
|
|
16
|
-
* is removed by hand.
|
|
17
|
-
*
|
|
18
|
-
*
|
|
23
|
+
* is removed by hand. Empty or truncated locks are reclaimed only after the same inode and contents
|
|
24
|
+
* remain malformed for a grace period, so a live creator can finish publishing its owner payload.
|
|
25
|
+
* Acquisition stays atomic (O_EXCL), and stale reclaim moves the old lock aside under a mutation
|
|
26
|
+
* guard, then verifies the inode snapshot before deletion. A live owner's lock (including this
|
|
27
|
+
* process's own) is never stolen.
|
|
19
28
|
*
|
|
20
29
|
* NOTE: still synchronous (blocks the event loop while waiting) by design — it guards short
|
|
21
30
|
* synchronous critical sections (append-only writes).
|
|
22
31
|
*/
|
|
23
32
|
export declare function acquireLock(lockPath: string, opts?: AcquireLockOptions): void;
|
|
24
|
-
export
|
|
33
|
+
export type ReleaseLockReason = 'released' | 'missing' | 'ownership_changed' | 'not_owned' | 'released_with_cleanup_error' | UnsafeLockPathReason | 'release_failed';
|
|
34
|
+
export interface ReleaseLockResult {
|
|
35
|
+
released: boolean;
|
|
36
|
+
reason: ReleaseLockReason;
|
|
37
|
+
}
|
|
38
|
+
export declare function releaseLock(lockPath: string): ReleaseLockResult;
|
|
39
|
+
export declare class LockReleaseError extends Error {
|
|
40
|
+
readonly reason: ReleaseLockReason;
|
|
41
|
+
readonly code = "LOCK_RELEASE_FAILED";
|
|
42
|
+
constructor(reason: ReleaseLockReason);
|
|
43
|
+
}
|
package/dist/util/fileLock.js
CHANGED
|
@@ -1,17 +1,29 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import {
|
|
2
|
+
import { closeSync, constants, existsSync, fstatSync, linkSync, 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);
|
|
6
6
|
}
|
|
7
|
+
const MALFORMED_LOCK_GRACE_NS = 1000000000n;
|
|
7
8
|
const ownedLocks = new Map();
|
|
9
|
+
const malformedLockObservations = new Map();
|
|
8
10
|
export class LockTimeoutError extends Error {
|
|
9
11
|
code = 'LOCK_TIMEOUT';
|
|
10
|
-
constructor(
|
|
11
|
-
super(
|
|
12
|
+
constructor(_lockPath) {
|
|
13
|
+
super('获取文件锁超时');
|
|
12
14
|
this.name = 'LockTimeoutError';
|
|
13
15
|
}
|
|
14
16
|
}
|
|
17
|
+
export class UnsafeLockPathError extends Error {
|
|
18
|
+
reason;
|
|
19
|
+
code = 'UNSAFE_LOCK_PATH';
|
|
20
|
+
constructor(reason) {
|
|
21
|
+
super(`不安全的文件锁路径: ${reason}`);
|
|
22
|
+
this.reason = reason;
|
|
23
|
+
this.name = 'UnsafeLockPathError';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export const MAX_LOCK_OWNER_BYTES = 4096;
|
|
15
27
|
function lockKey(lockPath) {
|
|
16
28
|
return resolve(lockPath);
|
|
17
29
|
}
|
|
@@ -24,6 +36,86 @@ function serializeOwner(owner) {
|
|
|
24
36
|
function isErrno(error, code) {
|
|
25
37
|
return typeof error === 'object' && error !== null && error.code === code;
|
|
26
38
|
}
|
|
39
|
+
function noFollowFlag() {
|
|
40
|
+
return constants['O_NOFOLLOW'] ?? 0;
|
|
41
|
+
}
|
|
42
|
+
function assertRegularOwnerStat(stat) {
|
|
43
|
+
if (stat.isSymbolicLink())
|
|
44
|
+
throw new UnsafeLockPathError('symlink');
|
|
45
|
+
if (!stat.isFile())
|
|
46
|
+
throw new UnsafeLockPathError('not_regular_file');
|
|
47
|
+
if (stat.size > BigInt(MAX_LOCK_OWNER_BYTES))
|
|
48
|
+
throw new UnsafeLockPathError('owner_too_large');
|
|
49
|
+
}
|
|
50
|
+
function currentOwnerStat(path) {
|
|
51
|
+
try {
|
|
52
|
+
const stat = lstatSync(path, { bigint: true });
|
|
53
|
+
assertRegularOwnerStat(stat);
|
|
54
|
+
return stat;
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
if (isErrno(error, 'ENOENT'))
|
|
58
|
+
return null;
|
|
59
|
+
if (isErrno(error, 'EACCES') || isErrno(error, 'EPERM')) {
|
|
60
|
+
throw new UnsafeLockPathError('permission_denied');
|
|
61
|
+
}
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function readOwnerFileBounded(path) {
|
|
66
|
+
const before = currentOwnerStat(path);
|
|
67
|
+
if (before === null)
|
|
68
|
+
return null;
|
|
69
|
+
let fd;
|
|
70
|
+
try {
|
|
71
|
+
fd = openSync(path, constants.O_RDONLY | noFollowFlag());
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
if (isErrno(error, 'ENOENT'))
|
|
75
|
+
return null;
|
|
76
|
+
if (isErrno(error, 'ELOOP'))
|
|
77
|
+
throw new UnsafeLockPathError('symlink');
|
|
78
|
+
if (isErrno(error, 'EACCES') || isErrno(error, 'EPERM')) {
|
|
79
|
+
throw new UnsafeLockPathError('permission_denied');
|
|
80
|
+
}
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
const opened = fstatSync(fd, { bigint: true });
|
|
85
|
+
assertRegularOwnerStat(opened);
|
|
86
|
+
const current = currentOwnerStat(path);
|
|
87
|
+
if (current === null || current.dev !== opened.dev || current.ino !== opened.ino) {
|
|
88
|
+
throw new UnsafeLockPathError('path_changed');
|
|
89
|
+
}
|
|
90
|
+
const buffer = Buffer.alloc(MAX_LOCK_OWNER_BYTES + 1);
|
|
91
|
+
let total = 0;
|
|
92
|
+
while (total < buffer.length) {
|
|
93
|
+
const read = readSync(fd, buffer, total, buffer.length - total, total);
|
|
94
|
+
if (read === 0)
|
|
95
|
+
break;
|
|
96
|
+
total += read;
|
|
97
|
+
}
|
|
98
|
+
if (total > MAX_LOCK_OWNER_BYTES)
|
|
99
|
+
throw new UnsafeLockPathError('owner_too_large');
|
|
100
|
+
const settled = fstatSync(fd, { bigint: true });
|
|
101
|
+
assertRegularOwnerStat(settled);
|
|
102
|
+
const settledPath = currentOwnerStat(path);
|
|
103
|
+
if (settledPath === null
|
|
104
|
+
|| settledPath.dev !== settled.dev
|
|
105
|
+
|| settledPath.ino !== settled.ino
|
|
106
|
+
|| settledPath.size !== settled.size
|
|
107
|
+
|| settledPath.mtimeNs !== settled.mtimeNs) {
|
|
108
|
+
throw new UnsafeLockPathError('path_changed');
|
|
109
|
+
}
|
|
110
|
+
return { raw: buffer.subarray(0, total).toString('utf8'), stat: settled };
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
closeSync(fd);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function readOwnerBounded(path) {
|
|
117
|
+
return readOwnerFileBounded(path)?.raw ?? null;
|
|
118
|
+
}
|
|
27
119
|
/** True if pid is a live process (cross-platform via signal 0; EPERM = exists but owned by another user). */
|
|
28
120
|
function pidAlive(pid) {
|
|
29
121
|
if (!Number.isInteger(pid) || pid <= 0)
|
|
@@ -70,17 +162,53 @@ function parseOwner(raw) {
|
|
|
70
162
|
return null;
|
|
71
163
|
return parseJsonOwner(trimmed) ?? parseLegacyOwner(trimmed);
|
|
72
164
|
}
|
|
73
|
-
/**
|
|
74
|
-
function
|
|
165
|
+
/** Snapshot of the lock inode and its owner payload; null if the path disappeared. */
|
|
166
|
+
function lockSnapshot(lockPath) {
|
|
75
167
|
try {
|
|
76
|
-
|
|
168
|
+
const read = readOwnerFileBounded(lockPath);
|
|
169
|
+
if (read === null)
|
|
170
|
+
return null;
|
|
171
|
+
return {
|
|
172
|
+
owner: parseOwner(read.raw),
|
|
173
|
+
dev: read.stat.dev,
|
|
174
|
+
ino: read.stat.ino,
|
|
175
|
+
size: read.stat.size,
|
|
176
|
+
mtimeNs: read.stat.mtimeNs,
|
|
177
|
+
};
|
|
77
178
|
}
|
|
78
179
|
catch (error) {
|
|
79
|
-
|
|
180
|
+
// A valid owner can release and another waiter can acquire between lstat/open/fstat. Treat that snapshot as
|
|
181
|
+
// transiently unavailable: callers remain conservative (no steal/delete) and the acquire loop retries.
|
|
182
|
+
if (error instanceof UnsafeLockPathError && error.reason === 'path_changed')
|
|
80
183
|
return null;
|
|
81
184
|
throw error;
|
|
82
185
|
}
|
|
83
186
|
}
|
|
187
|
+
function sameInode(left, right) {
|
|
188
|
+
return left !== null && right !== null && left.dev === right.dev && left.ino === right.ino;
|
|
189
|
+
}
|
|
190
|
+
function sameSnapshot(left, right) {
|
|
191
|
+
return sameInode(left, right)
|
|
192
|
+
&& left?.size === right?.size
|
|
193
|
+
&& left?.mtimeNs === right?.mtimeNs;
|
|
194
|
+
}
|
|
195
|
+
function forgetMalformedObservation(lockPath) {
|
|
196
|
+
malformedLockObservations.delete(lockKey(lockPath));
|
|
197
|
+
}
|
|
198
|
+
function reclaimableSnapshot(lockPath, snapshot) {
|
|
199
|
+
if (snapshot.owner !== null) {
|
|
200
|
+
forgetMalformedObservation(lockPath);
|
|
201
|
+
return snapshot.owner.pid !== process.pid && !pidAlive(snapshot.owner.pid);
|
|
202
|
+
}
|
|
203
|
+
const key = lockKey(lockPath);
|
|
204
|
+
const now = process.hrtime.bigint();
|
|
205
|
+
const observed = malformedLockObservations.get(key);
|
|
206
|
+
if (observed === undefined || !sameSnapshot(observed.snapshot, snapshot)) {
|
|
207
|
+
malformedLockObservations.set(key, { snapshot, firstSeenAtNs: now });
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
return now - observed.firstSeenAtNs >= MALFORMED_LOCK_GRACE_NS;
|
|
211
|
+
}
|
|
84
212
|
function sameOwner(parsed, owner) {
|
|
85
213
|
return parsed?.pid === owner.pid && parsed.token === owner.token;
|
|
86
214
|
}
|
|
@@ -99,73 +227,112 @@ function removeFileIfExists(path) {
|
|
|
99
227
|
throw error;
|
|
100
228
|
}
|
|
101
229
|
}
|
|
102
|
-
function
|
|
230
|
+
function pathExists(path) {
|
|
231
|
+
return existsSync(path);
|
|
232
|
+
}
|
|
233
|
+
function restoreMovedFile(lockPath, movedPath, moved) {
|
|
103
234
|
try {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
return
|
|
235
|
+
linkSync(movedPath, lockPath);
|
|
236
|
+
removeFileIfExists(movedPath);
|
|
237
|
+
forgetMalformedObservation(lockPath);
|
|
238
|
+
return;
|
|
108
239
|
}
|
|
109
240
|
catch (error) {
|
|
110
|
-
if (isErrno(error, 'EEXIST'))
|
|
241
|
+
if (!isErrno(error, 'EEXIST'))
|
|
242
|
+
throw error;
|
|
243
|
+
}
|
|
244
|
+
const current = lockSnapshot(lockPath);
|
|
245
|
+
if (sameInode(current, moved)) {
|
|
246
|
+
removeFileIfExists(movedPath);
|
|
247
|
+
forgetMalformedObservation(lockPath);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
throw new Error(`文件锁在安全回收期间被替换,已保留候选文件: ${movedPath}`);
|
|
251
|
+
}
|
|
252
|
+
function removeUnchangedLock(lockPath, expected, kind) {
|
|
253
|
+
const movedPath = sidecarPath(lockPath, kind);
|
|
254
|
+
try {
|
|
255
|
+
renameSync(lockPath, movedPath);
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
if (isErrno(error, 'ENOENT'))
|
|
111
259
|
return false;
|
|
112
260
|
throw error;
|
|
113
261
|
}
|
|
262
|
+
const moved = lockSnapshot(movedPath);
|
|
263
|
+
if (!sameSnapshot(moved, expected)) {
|
|
264
|
+
if (moved !== null)
|
|
265
|
+
restoreMovedFile(lockPath, movedPath, moved);
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
removeFileIfExists(movedPath);
|
|
269
|
+
forgetMalformedObservation(lockPath);
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
272
|
+
function removeFileIfOwned(lockPath, owner) {
|
|
273
|
+
const snapshot = lockSnapshot(lockPath);
|
|
274
|
+
if (snapshot === null || !sameOwner(snapshot.owner, owner))
|
|
275
|
+
return false;
|
|
276
|
+
return removeUnchangedLock(lockPath, snapshot, 'released');
|
|
277
|
+
}
|
|
278
|
+
function createLockFile(lockPath, owner, trackOwnership) {
|
|
279
|
+
const guardPath = mutationGuardPath(lockPath);
|
|
280
|
+
if (trackOwnership && pathExists(guardPath))
|
|
281
|
+
return false;
|
|
282
|
+
for (let permissionAttempt = 0; permissionAttempt < 2; permissionAttempt++) {
|
|
283
|
+
try {
|
|
284
|
+
writeFileSync(lockPath, serializeOwner(owner), { flag: 'wx', mode: 0o600 });
|
|
285
|
+
if (trackOwnership && pathExists(guardPath)) {
|
|
286
|
+
removeFileIfOwned(lockPath, owner);
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
if (trackOwnership)
|
|
290
|
+
ownedLocks.set(lockKey(lockPath), { owner, releasePending: false });
|
|
291
|
+
forgetMalformedObservation(lockPath);
|
|
292
|
+
return true;
|
|
293
|
+
}
|
|
294
|
+
catch (error) {
|
|
295
|
+
if (isErrno(error, 'EEXIST'))
|
|
296
|
+
return false;
|
|
297
|
+
if (isErrno(error, 'EACCES') || isErrno(error, 'EPERM')) {
|
|
298
|
+
// Windows can report EPERM instead of EEXIST during contention. A regular owner is busy; if the owner
|
|
299
|
+
// disappeared before lstat, retry create once to distinguish that release race from a persistent ACL
|
|
300
|
+
// failure. Unsafe/inaccessible paths still throw from currentOwnerStat and remain fail closed.
|
|
301
|
+
if (currentOwnerStat(lockPath) !== null)
|
|
302
|
+
return false;
|
|
303
|
+
if (permissionAttempt === 0)
|
|
304
|
+
continue;
|
|
305
|
+
throw new UnsafeLockPathError('permission_denied');
|
|
306
|
+
}
|
|
307
|
+
throw error;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
throw new UnsafeLockPathError('permission_denied');
|
|
114
311
|
}
|
|
115
312
|
function tryAcquireMutationGuard(lockPath) {
|
|
116
313
|
const guardPath = mutationGuardPath(lockPath);
|
|
117
314
|
const guard = newOwner();
|
|
118
315
|
if (createLockFile(guardPath, guard, false))
|
|
119
316
|
return guard;
|
|
120
|
-
const
|
|
121
|
-
if (
|
|
317
|
+
const snapshot = lockSnapshot(guardPath);
|
|
318
|
+
if (snapshot === null || !reclaimableSnapshot(guardPath, snapshot))
|
|
319
|
+
return null;
|
|
320
|
+
if (!removeUnchangedLock(guardPath, snapshot, 'stale'))
|
|
122
321
|
return null;
|
|
123
|
-
const stalePath = sidecarPath(guardPath, 'stale');
|
|
124
|
-
try {
|
|
125
|
-
renameSync(guardPath, stalePath);
|
|
126
|
-
}
|
|
127
|
-
catch (error) {
|
|
128
|
-
if (isErrno(error, 'ENOENT'))
|
|
129
|
-
return null;
|
|
130
|
-
throw error;
|
|
131
|
-
}
|
|
132
|
-
removeFileIfExists(stalePath);
|
|
133
322
|
return createLockFile(guardPath, guard, false) ? guard : null;
|
|
134
323
|
}
|
|
135
324
|
function releaseMutationGuard(lockPath, guard) {
|
|
136
|
-
|
|
137
|
-
if (!sameOwner(lockOwner(guardPath), guard))
|
|
138
|
-
return;
|
|
139
|
-
const releasedPath = sidecarPath(guardPath, 'released');
|
|
140
|
-
try {
|
|
141
|
-
renameSync(guardPath, releasedPath);
|
|
142
|
-
}
|
|
143
|
-
catch (error) {
|
|
144
|
-
if (isErrno(error, 'ENOENT'))
|
|
145
|
-
return;
|
|
146
|
-
throw error;
|
|
147
|
-
}
|
|
148
|
-
removeFileIfExists(releasedPath);
|
|
325
|
+
removeFileIfOwned(mutationGuardPath(lockPath), guard);
|
|
149
326
|
}
|
|
150
327
|
function reclaimStaleLock(lockPath) {
|
|
151
328
|
const guard = tryAcquireMutationGuard(lockPath);
|
|
152
329
|
if (guard === null)
|
|
153
330
|
return false;
|
|
154
331
|
try {
|
|
155
|
-
const
|
|
156
|
-
if (
|
|
332
|
+
const snapshot = lockSnapshot(lockPath);
|
|
333
|
+
if (snapshot === null || !reclaimableSnapshot(lockPath, snapshot))
|
|
157
334
|
return false;
|
|
158
|
-
|
|
159
|
-
try {
|
|
160
|
-
renameSync(lockPath, stalePath);
|
|
161
|
-
}
|
|
162
|
-
catch (error) {
|
|
163
|
-
if (isErrno(error, 'ENOENT'))
|
|
164
|
-
return false;
|
|
165
|
-
throw error;
|
|
166
|
-
}
|
|
167
|
-
removeFileIfExists(stalePath);
|
|
168
|
-
return true;
|
|
335
|
+
return removeUnchangedLock(lockPath, snapshot, 'stale');
|
|
169
336
|
}
|
|
170
337
|
finally {
|
|
171
338
|
releaseMutationGuard(lockPath, guard);
|
|
@@ -177,14 +344,22 @@ function reclaimStaleLock(lockPath) {
|
|
|
177
344
|
* The lock file records the owner pid and token. If a waiter finds the lock held by a pid that is no longer
|
|
178
345
|
* alive (the owner crashed without releaseLock), it reclaims the stale lock instead of spinning
|
|
179
346
|
* until timeout — otherwise one crashed process would deadlock every future writer until the file
|
|
180
|
-
* is removed by hand.
|
|
181
|
-
*
|
|
182
|
-
*
|
|
347
|
+
* is removed by hand. Empty or truncated locks are reclaimed only after the same inode and contents
|
|
348
|
+
* remain malformed for a grace period, so a live creator can finish publishing its owner payload.
|
|
349
|
+
* Acquisition stays atomic (O_EXCL), and stale reclaim moves the old lock aside under a mutation
|
|
350
|
+
* guard, then verifies the inode snapshot before deletion. A live owner's lock (including this
|
|
351
|
+
* process's own) is never stolen.
|
|
183
352
|
*
|
|
184
353
|
* NOTE: still synchronous (blocks the event loop while waiting) by design — it guards short
|
|
185
354
|
* synchronous critical sections (append-only writes).
|
|
186
355
|
*/
|
|
187
356
|
export function acquireLock(lockPath, opts = {}) {
|
|
357
|
+
const pending = ownedLocks.get(lockKey(lockPath));
|
|
358
|
+
if (pending?.releasePending) {
|
|
359
|
+
const released = releaseLock(lockPath);
|
|
360
|
+
if (!released.released)
|
|
361
|
+
throw new LockReleaseError(released.reason);
|
|
362
|
+
}
|
|
188
363
|
const maxTries = opts.maxTries ?? 300;
|
|
189
364
|
const waitMs = opts.waitMs ?? 10;
|
|
190
365
|
for (let i = 0; i < maxTries; i++) {
|
|
@@ -192,36 +367,77 @@ export function acquireLock(lockPath, opts = {}) {
|
|
|
192
367
|
if (createLockFile(lockPath, owner, true)) {
|
|
193
368
|
return;
|
|
194
369
|
}
|
|
195
|
-
const current =
|
|
196
|
-
if (current !== null &&
|
|
370
|
+
const current = lockSnapshot(lockPath);
|
|
371
|
+
if (current !== null && reclaimableSnapshot(lockPath, current)) {
|
|
197
372
|
if (!reclaimStaleLock(lockPath))
|
|
198
373
|
syncSleep(waitMs);
|
|
199
374
|
continue;
|
|
200
375
|
}
|
|
376
|
+
if (current === null && pathExists(mutationGuardPath(lockPath))) {
|
|
377
|
+
const guard = tryAcquireMutationGuard(lockPath);
|
|
378
|
+
if (guard !== null)
|
|
379
|
+
releaseMutationGuard(lockPath, guard);
|
|
380
|
+
}
|
|
201
381
|
syncSleep(waitMs);
|
|
202
382
|
}
|
|
203
|
-
throw new LockTimeoutError(
|
|
383
|
+
throw new LockTimeoutError();
|
|
204
384
|
}
|
|
205
385
|
export function releaseLock(lockPath) {
|
|
206
386
|
const key = lockKey(lockPath);
|
|
207
|
-
const
|
|
208
|
-
if (
|
|
209
|
-
return;
|
|
387
|
+
const owned = ownedLocks.get(key);
|
|
388
|
+
if (owned === undefined)
|
|
389
|
+
return { released: true, reason: 'not_owned' };
|
|
390
|
+
const failed = (reason) => {
|
|
391
|
+
owned.releasePending = true;
|
|
392
|
+
return { released: false, reason };
|
|
393
|
+
};
|
|
394
|
+
// Release must not use lockOwner(): that helper intentionally folds path_changed into a conservative null
|
|
395
|
+
// for acquire retries. Here, an inconclusive read marks a pending release so the next acquire can retry or
|
|
396
|
+
// surface LOCK_RELEASE_FAILED instead of silently timing out behind this process's own PID.
|
|
397
|
+
let raw;
|
|
210
398
|
try {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
399
|
+
raw = readOwnerBounded(lockPath);
|
|
400
|
+
}
|
|
401
|
+
catch (error) {
|
|
402
|
+
return failed(error instanceof UnsafeLockPathError ? error.reason : 'release_failed');
|
|
403
|
+
}
|
|
404
|
+
if (raw === null) {
|
|
405
|
+
ownedLocks.delete(key);
|
|
406
|
+
return { released: true, reason: 'missing' };
|
|
407
|
+
}
|
|
408
|
+
const current = parseOwner(raw);
|
|
409
|
+
if (current === null)
|
|
410
|
+
return failed('invalid_owner');
|
|
411
|
+
if (!sameOwner(current, owned.owner)) {
|
|
412
|
+
ownedLocks.delete(key);
|
|
413
|
+
return { released: true, reason: 'ownership_changed' };
|
|
414
|
+
}
|
|
415
|
+
const releasedPath = sidecarPath(lockPath, 'released');
|
|
416
|
+
try {
|
|
417
|
+
renameSync(lockPath, releasedPath);
|
|
418
|
+
}
|
|
419
|
+
catch (error) {
|
|
420
|
+
if (isErrno(error, 'ENOENT')) {
|
|
421
|
+
ownedLocks.delete(key);
|
|
422
|
+
return { released: true, reason: 'missing' };
|
|
221
423
|
}
|
|
424
|
+
return failed('release_failed');
|
|
425
|
+
}
|
|
426
|
+
ownedLocks.delete(key);
|
|
427
|
+
try {
|
|
222
428
|
removeFileIfExists(releasedPath);
|
|
223
429
|
}
|
|
224
|
-
|
|
225
|
-
|
|
430
|
+
catch {
|
|
431
|
+
return { released: true, reason: 'released_with_cleanup_error' };
|
|
432
|
+
}
|
|
433
|
+
return { released: true, reason: 'released' };
|
|
434
|
+
}
|
|
435
|
+
export class LockReleaseError extends Error {
|
|
436
|
+
reason;
|
|
437
|
+
code = 'LOCK_RELEASE_FAILED';
|
|
438
|
+
constructor(reason) {
|
|
439
|
+
super(`文件锁释放未完成: ${reason}`);
|
|
440
|
+
this.reason = reason;
|
|
441
|
+
this.name = 'LockReleaseError';
|
|
226
442
|
}
|
|
227
443
|
}
|
package/dist/util/index.d.ts
CHANGED
package/dist/util/index.js
CHANGED
package/dist/wire/geneHints.d.ts
CHANGED
|
@@ -34,13 +34,45 @@ export interface GeneToolPolicy {
|
|
|
34
34
|
deny?: string[];
|
|
35
35
|
severity?: ToolPolicySeverity;
|
|
36
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* Provenance classification (V1 #302 classifyProvenance, aligned with TaskGenome Bench §3.1): a Gene's value
|
|
39
|
+
* depends on WHERE it came from, not on being short. The three tiers:
|
|
40
|
+
* evolved -- distilled from a real solve -> fail -> mutate -> pass trajectory; beats Skills (+8.7..+15.5pp)
|
|
41
|
+
* distilled -- transcribed from reference/teacher text with no real failing trajectory; WORSE than Skills (-3.2..-11.2pp)
|
|
42
|
+
* manual -- pure human transcription, no execution evidence at all
|
|
43
|
+
* The high-value payload of an evolved Gene is the corrective_insight that flipped the outcome.
|
|
44
|
+
*/
|
|
45
|
+
export declare const GENERATION_SOURCES: readonly ["evolved", "distilled", "manual"];
|
|
46
|
+
export type GenerationSource = (typeof GENERATION_SOURCES)[number];
|
|
47
|
+
export interface GeneGenerationHeuristics {
|
|
48
|
+
strategy_steps?: number;
|
|
49
|
+
avoid_count?: number;
|
|
50
|
+
validation_declared_count?: number;
|
|
51
|
+
validation_runnable_count?: number;
|
|
52
|
+
signals_extracted?: number;
|
|
53
|
+
preconditions_extracted?: number;
|
|
54
|
+
trajectory_depth?: number;
|
|
55
|
+
has_corrective_insight?: boolean;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The full provenance + quality metadata block (V1 `gene._source`). Lives on Gene as the v2-delta field
|
|
59
|
+
* `generation_meta`. `source` is the only required key; the rest is descriptive for reviewers / future
|
|
60
|
+
* governance. `quality_score` is a coarse [0,1] tier-anchored score (evolved 0.7 / distilled 0.4 / manual 0.3
|
|
61
|
+
* baseline + bonuses); `overcame_errors` is the mutation_log copy that the trajectory overcame.
|
|
62
|
+
*/
|
|
63
|
+
export interface GenerationMeta {
|
|
64
|
+
source: GenerationSource;
|
|
65
|
+
quality_score?: number;
|
|
66
|
+
quality_heuristics?: GeneGenerationHeuristics;
|
|
67
|
+
overcame_errors?: string[];
|
|
68
|
+
}
|
|
37
69
|
/**
|
|
38
70
|
* The v2-delta hint field names, in one place — the intake gate strips these before the gep-sdk schema check.
|
|
39
71
|
* IMPORTANT: when a gep-sdk gene-schema bump makes one of these first-class, REMOVE it from this list in the
|
|
40
72
|
* same change. Otherwise intakeGene keeps stripping a now-validated field before validateWire (fail-open — the
|
|
41
73
|
* field's new schema constraints go unchecked at intake) while asset_id still commits it.
|
|
42
74
|
*/
|
|
43
|
-
export declare const GENE_HINT_FIELDS: readonly ["routing_hint", "tool_policy"];
|
|
75
|
+
export declare const GENE_HINT_FIELDS: readonly ["routing_hint", "tool_policy", "generation_meta"];
|
|
44
76
|
/**
|
|
45
77
|
* Normalize an arbitrary routing_hint fragment to a strict { tier?, reasoning_level? } object, or null.
|
|
46
78
|
* Unknown enum values are dropped (a stray tier would fail the consumer's exhaustive match and route as if
|
|
@@ -60,5 +92,14 @@ export declare function normalizeRoutingHint(raw: unknown): GeneRoutingHint | nu
|
|
|
60
92
|
* defaults to 'warn' when a list is present; the whole object collapses to null when neither list survives.
|
|
61
93
|
*/
|
|
62
94
|
export declare function normalizeToolPolicy(raw: unknown): GeneToolPolicy | null;
|
|
95
|
+
/**
|
|
96
|
+
* Normalize an arbitrary generation_meta fragment to a strict { source, quality_score?, quality_heuristics?,
|
|
97
|
+
* overcame_errors? } object, or null. Lossy (mirrors normalizeRoutingHint / normalizeToolPolicy): unknown source
|
|
98
|
+
* values are dropped (the whole block collapses to null — a generation_meta with no recognized source carries no
|
|
99
|
+
* usable provenance signal); quality_score is clamped to [0,1]; heuristics keeps only its numeric/boolean fields;
|
|
100
|
+
* overcame_errors keeps only non-empty strings. A block with a valid source but all-else-empty still survives (source
|
|
101
|
+
* alone is a meaningful provenance tag); only a missing/unknown source yields null.
|
|
102
|
+
*/
|
|
103
|
+
export declare function normalizeGenerationMeta(raw: unknown): GenerationMeta | null;
|
|
63
104
|
/** Strip the v2-delta hint fields from a gene-shaped object (used to validate the gep-sdk-known core). */
|
|
64
105
|
export declare function stripGeneHints(gene: Record<string, unknown>): Record<string, unknown>;
|