@evomap/evolver-core 2.0.0-beta.1 → 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.
Files changed (137) hide show
  1. package/assets/gep/genes.jsonl +5 -0
  2. package/dist/algo/candidateAssembly.js +23 -14
  3. package/dist/algo/capabilityCandidates.d.ts +2 -0
  4. package/dist/algo/capabilityCandidates.js +5 -0
  5. package/dist/algo/conversationSniffer.d.ts +18 -0
  6. package/dist/algo/conversationSniffer.js +149 -0
  7. package/dist/algo/cycleEngine.d.ts +11 -0
  8. package/dist/algo/cycleEngine.js +14 -6
  9. package/dist/algo/cycleFailureClassifier.d.ts +1 -1
  10. package/dist/algo/cycleFailureClassifier.js +13 -5
  11. package/dist/algo/geneIntake.d.ts +14 -4
  12. package/dist/algo/geneIntake.js +37 -9
  13. package/dist/algo/geneSelection.d.ts +18 -1
  14. package/dist/algo/geneSelection.js +42 -18
  15. package/dist/algo/index.d.ts +2 -0
  16. package/dist/algo/index.js +2 -0
  17. package/dist/algo/memoryGraph.d.ts +62 -0
  18. package/dist/algo/memoryGraph.js +86 -0
  19. package/dist/algo/orchestrator.d.ts +3 -0
  20. package/dist/algo/orchestrator.js +14 -2
  21. package/dist/assetstore/assetSidecarRecords.d.ts +28 -0
  22. package/dist/assetstore/assetSidecarRecords.js +376 -0
  23. package/dist/assetstore/assetSidecarRecovery.d.ts +48 -0
  24. package/dist/assetstore/assetSidecarRecovery.js +288 -0
  25. package/dist/assetstore/assetStoreHealth.d.ts +75 -0
  26. package/dist/assetstore/assetStoreHealth.js +277 -0
  27. package/dist/assetstore/assetStoreLayout.d.ts +2 -0
  28. package/dist/assetstore/assetStoreLayout.js +6 -0
  29. package/dist/assetstore/assetStoreStorage.d.ts +42 -0
  30. package/dist/assetstore/assetStoreStorage.js +327 -0
  31. package/dist/assetstore/assetSyncLedger.d.ts +118 -0
  32. package/dist/assetstore/assetSyncLedger.js +745 -0
  33. package/dist/assetstore/index.d.ts +5 -1
  34. package/dist/assetstore/index.js +5 -1
  35. package/dist/assetstore/localJsonl.d.ts +12 -3
  36. package/dist/assetstore/localJsonl.js +131 -39
  37. package/dist/assetstore/pendingSignals.js +3 -1
  38. package/dist/assetstore/provenance.d.ts +27 -4
  39. package/dist/assetstore/provenance.js +107 -56
  40. package/dist/assetstore/provider.d.ts +26 -0
  41. package/dist/assetstore/provider.js +50 -0
  42. package/dist/assetstore/reviewFilter.js +5 -2
  43. package/dist/assetstore/reviewLedger.d.ts +14 -2
  44. package/dist/assetstore/reviewLedger.js +78 -43
  45. package/dist/assetstore/seedGenes.d.ts +3 -0
  46. package/dist/assetstore/seedGenes.js +134 -0
  47. package/dist/benchmark/antiGeneBenchmark.d.ts +2 -0
  48. package/dist/benchmark/antiGeneBenchmark.js +11 -1
  49. package/dist/benchmark/antiGeneImpact.d.ts +16 -0
  50. package/dist/benchmark/antiGeneImpact.js +34 -0
  51. package/dist/benchmark/antiGeneRollout.d.ts +2 -0
  52. package/dist/benchmark/antiGeneRollout.js +13 -1
  53. package/dist/benchmark/index.d.ts +2 -1
  54. package/dist/benchmark/index.js +2 -1
  55. package/dist/benchmark/triggerShift.d.ts +62 -0
  56. package/dist/benchmark/triggerShift.js +106 -0
  57. package/dist/events/eventArchive.d.ts +65 -0
  58. package/dist/events/eventArchive.js +343 -0
  59. package/dist/events/eventStore.js +2 -12
  60. package/dist/events/ingest.d.ts +1 -1
  61. package/dist/events/ingest.js +9 -0
  62. package/dist/events/paths.d.ts +10 -10
  63. package/dist/events/paths.js +20 -20
  64. package/dist/events/public.d.ts +3 -1
  65. package/dist/events/public.js +2 -1
  66. package/dist/events/retention.d.ts +24 -1
  67. package/dist/events/retention.js +133 -37
  68. package/dist/exec/autoExec.d.ts +6 -1
  69. package/dist/exec/autoExec.js +34 -0
  70. package/dist/exec/autonomousCycle.d.ts +2 -0
  71. package/dist/exec/autonomousCycle.js +5 -0
  72. package/dist/exec/claudeBridge.d.ts +21 -3
  73. package/dist/exec/claudeBridge.js +413 -26
  74. package/dist/exec/openPrRegistry.d.ts +8 -2
  75. package/dist/exec/openPrRegistry.js +32 -22
  76. package/dist/exec/prompt.js +9 -0
  77. package/dist/exec/runnerRegistry.d.ts +90 -18
  78. package/dist/exec/runnerRegistry.js +603 -42
  79. package/dist/exec/selfPrObfuscation.d.ts +2 -1
  80. package/dist/exec/selfPrObfuscation.js +19 -6
  81. package/dist/hub/agentDirectory.d.ts +90 -0
  82. package/dist/hub/agentDirectory.js +104 -0
  83. package/dist/hub/bindings.js +23 -3
  84. package/dist/hub/capability.d.ts +14 -2
  85. package/dist/hub/fake.d.ts +4 -2
  86. package/dist/hub/fake.js +3 -2
  87. package/dist/hub/index.d.ts +1 -0
  88. package/dist/hub/index.js +1 -0
  89. package/dist/hub/sanitize.js +3 -2
  90. package/dist/index.d.ts +2 -1
  91. package/dist/index.js +2 -1
  92. package/dist/issueReporter/index.d.ts +156 -0
  93. package/dist/issueReporter/index.js +1688 -0
  94. package/dist/mailbox/catalog.js +3 -0
  95. package/dist/mailbox/ipcServer.js +2 -2
  96. package/dist/mailbox/store.d.ts +14 -0
  97. package/dist/mailbox/store.js +44 -2
  98. package/dist/material/consumer.js +9 -6
  99. package/dist/material/index.d.ts +1 -0
  100. package/dist/material/index.js +1 -0
  101. package/dist/material/materialArchive.d.ts +81 -0
  102. package/dist/material/materialArchive.js +466 -0
  103. package/dist/material/materialStore.d.ts +1 -0
  104. package/dist/material/materialStore.js +6 -13
  105. package/dist/ops/savingsCore.js +1 -2
  106. package/dist/ops/selfUpdate.d.ts +10 -1
  107. package/dist/ops/selfUpdate.js +64 -15
  108. package/dist/personality/schema.d.ts +12 -12
  109. package/dist/schema/common.d.ts +1 -1
  110. package/dist/schema/common.js +1 -1
  111. package/dist/schema/material.d.ts +5 -5
  112. package/dist/strategy/constraintAblation.d.ts +64 -0
  113. package/dist/strategy/constraintAblation.js +3074 -0
  114. package/dist/strategy/index.d.ts +2 -1
  115. package/dist/strategy/index.js +2 -1
  116. package/dist/trace/trajectoryExport.js +2 -2
  117. package/dist/util/fetchPort.d.ts +1 -0
  118. package/dist/util/fetchPort.js +11 -0
  119. package/dist/util/fileLock.d.ts +24 -5
  120. package/dist/util/fileLock.js +288 -72
  121. package/dist/util/index.d.ts +1 -0
  122. package/dist/util/index.js +1 -0
  123. package/dist/wire/geneHints.d.ts +42 -1
  124. package/dist/wire/geneHints.js +52 -1
  125. package/dist/wire/index.d.ts +14 -2
  126. package/dist/wire/index.js +1 -1
  127. package/dist/workflow/dsl.d.ts +24 -3
  128. package/dist/workflow/dsl.js +4 -0
  129. package/dist/workflow/engine.d.ts +5 -1
  130. package/dist/workflow/engine.js +3 -0
  131. package/dist/workflow/index.d.ts +3 -1
  132. package/dist/workflow/index.js +3 -1
  133. package/dist/workflow/runtime.d.ts +110 -0
  134. package/dist/workflow/runtime.js +1298 -0
  135. package/dist/workflow/stateStore.d.ts +172 -0
  136. package/dist/workflow/stateStore.js +1044 -0
  137. package/package.json +6 -1
@@ -1,2 +1,3 @@
1
1
  export * from './strategyPoint.js';
2
- export * from './experiment.js';
2
+ export * from './experiment.js';
3
+ export * from './constraintAblation.js';
@@ -1,2 +1,3 @@
1
1
  export * from './strategyPoint.js';
2
- export * from './experiment.js';
2
+ export * from './experiment.js';
3
+ export * from './constraintAblation.js';
@@ -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
+ }
@@ -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(lockPath: string);
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. Acquisition stays atomic (O_EXCL), and stale reclaim moves the old lock aside
17
- * under a mutation guard before deletion so waiters cannot delete each other's newly-created locks.
18
- * A live owner's lock (including this process's own) is never stolen.
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 declare function releaseLock(lockPath: string): void;
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
+ }
@@ -1,17 +1,29 @@
1
1
  import { randomUUID } from 'node:crypto';
2
- import { readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
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(lockPath) {
11
- super(`获取文件锁超时: ${lockPath}`);
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
- /** Owner recorded in the lock file; null if missing/unparseable. */
74
- function lockOwner(lockPath) {
165
+ /** Snapshot of the lock inode and its owner payload; null if the path disappeared. */
166
+ function lockSnapshot(lockPath) {
75
167
  try {
76
- return parseOwner(readFileSync(lockPath, 'utf8'));
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
- if (isErrno(error, 'ENOENT'))
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 createLockFile(lockPath, owner, trackOwnership) {
230
+ function pathExists(path) {
231
+ return existsSync(path);
232
+ }
233
+ function restoreMovedFile(lockPath, movedPath, moved) {
103
234
  try {
104
- writeFileSync(lockPath, serializeOwner(owner), { flag: 'wx', mode: 0o600 });
105
- if (trackOwnership)
106
- ownedLocks.set(lockKey(lockPath), owner);
107
- return true;
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 owner = lockOwner(guardPath);
121
- if (owner === null || owner.pid === process.pid || pidAlive(owner.pid))
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
- const guardPath = mutationGuardPath(lockPath);
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 owner = lockOwner(lockPath);
156
- if (owner === null || owner.pid === process.pid || pidAlive(owner.pid))
332
+ const snapshot = lockSnapshot(lockPath);
333
+ if (snapshot === null || !reclaimableSnapshot(lockPath, snapshot))
157
334
  return false;
158
- const stalePath = sidecarPath(lockPath, 'stale');
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. Acquisition stays atomic (O_EXCL), and stale reclaim moves the old lock aside
181
- * under a mutation guard before deletion so waiters cannot delete each other's newly-created locks.
182
- * A live owner's lock (including this process's own) is never stolen.
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 = lockOwner(lockPath);
196
- if (current !== null && current.pid !== process.pid && !pidAlive(current.pid)) {
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(lockPath);
383
+ throw new LockTimeoutError();
204
384
  }
205
385
  export function releaseLock(lockPath) {
206
386
  const key = lockKey(lockPath);
207
- const owner = ownedLocks.get(key);
208
- if (owner === undefined)
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
- if (!sameOwner(lockOwner(lockPath), owner))
212
- return;
213
- const releasedPath = sidecarPath(lockPath, 'released');
214
- try {
215
- renameSync(lockPath, releasedPath);
216
- }
217
- catch (error) {
218
- if (isErrno(error, 'ENOENT'))
219
- return;
220
- throw error;
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
- finally {
225
- ownedLocks.delete(key);
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
  }
@@ -1,2 +1,3 @@
1
1
  export * from './fileLock.js';
2
+ export * from './fetchPort.js';
2
3
  export * from './version.js';
@@ -1,2 +1,3 @@
1
1
  export * from './fileLock.js';
2
+ export * from './fetchPort.js';
2
3
  export * from './version.js';
@@ -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>;