@evomap/evolver-core 2.0.0-beta.2 → 2.0.0-beta.22

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 (240) hide show
  1. package/assets/gep/genes.jsonl +5 -5
  2. package/dist/algo/bans.d.ts +10 -1
  3. package/dist/algo/bans.js +56 -6
  4. package/dist/algo/candidateAssembly.d.ts +25 -2
  5. package/dist/algo/candidateAssembly.js +171 -26
  6. package/dist/algo/capabilityCandidates.js +10 -0
  7. package/dist/algo/conversationSniffer.js +25 -8
  8. package/dist/algo/cycleEngine.d.ts +63 -5
  9. package/dist/algo/cycleEngine.js +320 -42
  10. package/dist/algo/cycleFailureClassifier.d.ts +1 -1
  11. package/dist/algo/evolutionEvent.d.ts +26 -0
  12. package/dist/algo/evolutionEvent.js +32 -0
  13. package/dist/algo/exploration.d.ts +7 -0
  14. package/dist/algo/exploration.js +16 -3
  15. package/dist/algo/geneHealth.d.ts +36 -3
  16. package/dist/algo/geneHealth.js +47 -4
  17. package/dist/algo/geneIntake.d.ts +46 -1
  18. package/dist/algo/geneIntake.js +121 -10
  19. package/dist/algo/geneSelection.d.ts +126 -6
  20. package/dist/algo/geneSelection.js +479 -36
  21. package/dist/algo/index.d.ts +6 -1
  22. package/dist/algo/index.js +6 -1
  23. package/dist/algo/kautoProjection.d.ts +41 -0
  24. package/dist/algo/kautoProjection.js +95 -0
  25. package/dist/algo/kautoValidator.d.ts +68 -0
  26. package/dist/algo/kautoValidator.js +256 -0
  27. package/dist/algo/memoryGraph.d.ts +62 -0
  28. package/dist/algo/memoryGraph.js +86 -0
  29. package/dist/algo/orchestrator.d.ts +17 -1
  30. package/dist/algo/orchestrator.js +30 -4
  31. package/dist/algo/publishEligibility.d.ts +34 -0
  32. package/dist/algo/publishEligibility.js +52 -0
  33. package/dist/algo/solidify.d.ts +11 -2
  34. package/dist/algo/solidify.js +37 -7
  35. package/dist/algo/ucb1.d.ts +53 -0
  36. package/dist/algo/ucb1.js +156 -0
  37. package/dist/assetrepair/hubRejection.d.ts +12 -0
  38. package/dist/assetrepair/hubRejection.js +109 -0
  39. package/dist/assetrepair/index.d.ts +2 -0
  40. package/dist/assetrepair/index.js +2 -0
  41. package/dist/assetrepair/repair.d.ts +33 -0
  42. package/dist/assetrepair/repair.js +155 -0
  43. package/dist/assetstore/assetSidecarRecords.d.ts +28 -0
  44. package/dist/assetstore/assetSidecarRecords.js +384 -0
  45. package/dist/assetstore/assetSidecarRecovery.d.ts +48 -0
  46. package/dist/assetstore/assetSidecarRecovery.js +288 -0
  47. package/dist/assetstore/assetStoreHealth.d.ts +81 -0
  48. package/dist/assetstore/assetStoreHealth.js +319 -0
  49. package/dist/assetstore/assetStoreLayout.d.ts +2 -0
  50. package/dist/assetstore/assetStoreLayout.js +6 -0
  51. package/dist/assetstore/assetStoreStorage.d.ts +42 -0
  52. package/dist/assetstore/assetStoreStorage.js +336 -0
  53. package/dist/assetstore/assetSyncLedger.d.ts +91 -1
  54. package/dist/assetstore/assetSyncLedger.js +718 -59
  55. package/dist/assetstore/foreignJsonlSource.d.ts +48 -0
  56. package/dist/assetstore/foreignJsonlSource.js +150 -0
  57. package/dist/assetstore/index.d.ts +5 -0
  58. package/dist/assetstore/index.js +5 -0
  59. package/dist/assetstore/learningHistory.js +3 -3
  60. package/dist/assetstore/localAssetStoreSnapshot.d.ts +51 -0
  61. package/dist/assetstore/localAssetStoreSnapshot.js +329 -0
  62. package/dist/assetstore/localJsonl.d.ts +12 -1
  63. package/dist/assetstore/localJsonl.js +251 -42
  64. package/dist/assetstore/provenance.d.ts +93 -4
  65. package/dist/assetstore/provenance.js +368 -84
  66. package/dist/assetstore/provider.d.ts +63 -0
  67. package/dist/assetstore/provider.js +97 -6
  68. package/dist/assetstore/reviewFilter.d.ts +19 -1
  69. package/dist/assetstore/reviewFilter.js +39 -1
  70. package/dist/assetstore/reviewLedger.d.ts +8 -2
  71. package/dist/assetstore/reviewLedger.js +71 -45
  72. package/dist/assetstore/unionReadStore.d.ts +25 -0
  73. package/dist/assetstore/unionReadStore.js +119 -0
  74. package/dist/benchmark/antiGeneBenchmark.d.ts +2 -0
  75. package/dist/benchmark/antiGeneBenchmark.js +4 -3
  76. package/dist/benchmark/antiGeneRollout.d.ts +2 -0
  77. package/dist/benchmark/antiGeneRollout.js +4 -3
  78. package/dist/benchmark/index.d.ts +3 -1
  79. package/dist/benchmark/index.js +3 -1
  80. package/dist/benchmark/selectionFlatAbstention.d.ts +152 -0
  81. package/dist/benchmark/selectionFlatAbstention.js +481 -0
  82. package/dist/benchmark/triggerShift.d.ts +62 -0
  83. package/dist/benchmark/triggerShift.js +106 -0
  84. package/dist/bootstrap/envFingerprint.d.ts +9 -0
  85. package/dist/bootstrap/envFingerprint.js +5 -0
  86. package/dist/bootstrap/index.d.ts +3 -1
  87. package/dist/bootstrap/index.js +3 -1
  88. package/dist/bootstrap/lifecycleBootstrap.d.ts +111 -0
  89. package/dist/bootstrap/lifecycleBootstrap.js +433 -0
  90. package/dist/bootstrap/v1EnvCompat.d.ts +113 -0
  91. package/dist/bootstrap/v1EnvCompat.js +303 -0
  92. package/dist/events/eventArchive.d.ts +2 -0
  93. package/dist/events/eventArchive.js +13 -3
  94. package/dist/events/eventSchema.d.ts +7 -7
  95. package/dist/events/eventStore.d.ts +2 -0
  96. package/dist/events/eventStore.js +5 -1
  97. package/dist/events/ingest.d.ts +2 -1
  98. package/dist/events/ingest.js +15 -0
  99. package/dist/events/paths.d.ts +12 -10
  100. package/dist/events/paths.js +24 -20
  101. package/dist/events/public.d.ts +2 -2
  102. package/dist/events/public.js +2 -2
  103. package/dist/events/reports.d.ts +2 -0
  104. package/dist/events/reports.js +4 -0
  105. package/dist/exec/autoExec.d.ts +68 -4
  106. package/dist/exec/autoExec.js +392 -33
  107. package/dist/exec/autonomousCycle.d.ts +31 -4
  108. package/dist/exec/autonomousCycle.js +71 -13
  109. package/dist/exec/claudeBridge.d.ts +80 -15
  110. package/dist/exec/claudeBridge.js +845 -76
  111. package/dist/exec/executionBinding.d.ts +414 -0
  112. package/dist/exec/executionBinding.js +588 -0
  113. package/dist/exec/index.d.ts +1 -0
  114. package/dist/exec/index.js +1 -0
  115. package/dist/exec/openPrRegistry.d.ts +8 -2
  116. package/dist/exec/openPrRegistry.js +32 -22
  117. package/dist/exec/prompt.js +14 -1
  118. package/dist/exec/proofOfWork.d.ts +1 -1
  119. package/dist/exec/proofOfWork.js +2 -2
  120. package/dist/exec/runnerRegistry.d.ts +153 -36
  121. package/dist/exec/runnerRegistry.js +848 -65
  122. package/dist/exec/selfPr.js +1 -7
  123. package/dist/feedback/envelope.d.ts +61 -0
  124. package/dist/feedback/envelope.js +168 -0
  125. package/dist/feedback/index.d.ts +1 -0
  126. package/dist/feedback/index.js +1 -0
  127. package/dist/hooks/hooks.js +1 -0
  128. package/dist/hub/assetCallLog.d.ts +35 -1
  129. package/dist/hub/assetCallLog.js +124 -1
  130. package/dist/hub/bindings.d.ts +8 -1
  131. package/dist/hub/bindings.js +29 -8
  132. package/dist/hub/capability.d.ts +130 -4
  133. package/dist/hub/conversationDistiller.d.ts +19 -0
  134. package/dist/hub/conversationDistiller.js +115 -37
  135. package/dist/hub/fake.d.ts +3 -2
  136. package/dist/hub/fake.js +2 -1
  137. package/dist/hub/index.d.ts +1 -0
  138. package/dist/hub/index.js +1 -0
  139. package/dist/hub/questionGenerator.d.ts +5 -1
  140. package/dist/hub/questionGenerator.js +8 -6
  141. package/dist/hub/recipeCompose.d.ts +27 -0
  142. package/dist/hub/recipeCompose.js +90 -0
  143. package/dist/hub/sanitize.js +122 -7
  144. package/dist/index.d.ts +6 -1
  145. package/dist/index.js +7 -1
  146. package/dist/issueReporter/index.d.ts +156 -0
  147. package/dist/issueReporter/index.js +1688 -0
  148. package/dist/mailbox/dispatch.d.ts +1 -1
  149. package/dist/mailbox/dispatch.js +22 -6
  150. package/dist/mailbox/envelope.d.ts +7 -1
  151. package/dist/mailbox/envelope.js +9 -2
  152. package/dist/mailbox/ipcServer.d.ts +12 -2
  153. package/dist/mailbox/ipcServer.js +183 -13
  154. package/dist/mailbox/store.d.ts +89 -3
  155. package/dist/mailbox/store.js +895 -41
  156. package/dist/modelCompatibility.d.ts +164 -0
  157. package/dist/modelCompatibility.js +309 -0
  158. package/dist/observers/valueDigestObserver.d.ts +9 -0
  159. package/dist/observers/valueDigestObserver.js +35 -2
  160. package/dist/ops/cleanup.js +1 -1
  161. package/dist/ops/evolutionGraphProjection.d.ts +20 -0
  162. package/dist/ops/evolutionGraphProjection.js +315 -0
  163. package/dist/ops/index.d.ts +2 -1
  164. package/dist/ops/index.js +2 -1
  165. package/dist/ops/savingsCore.js +1 -2
  166. package/dist/ops/selfUpdate.d.ts +18 -1
  167. package/dist/ops/selfUpdate.js +88 -23
  168. package/dist/ops/valueOutreach.d.ts +3 -1
  169. package/dist/ops/valueOutreach.js +5 -1
  170. package/dist/personality/schema.d.ts +24 -24
  171. package/dist/schema/evolutionGraph.d.ts +784 -0
  172. package/dist/schema/evolutionGraph.js +187 -0
  173. package/dist/schema/index.d.ts +1 -0
  174. package/dist/schema/index.js +1 -0
  175. package/dist/schema/proofOfWork.d.ts +125 -6
  176. package/dist/schema/proofOfWork.js +102 -4
  177. package/dist/schema/signal.d.ts +3 -3
  178. package/dist/schema/signal.js +1 -1
  179. package/dist/shadow/shadowHub.js +1 -0
  180. package/dist/signals/curriculum.d.ts +55 -0
  181. package/dist/signals/curriculum.js +202 -0
  182. package/dist/signals/cycleHistoryFromEvents.js +17 -8
  183. package/dist/signals/expand.d.ts +15 -1
  184. package/dist/signals/expand.js +169 -1
  185. package/dist/signals/extractor.d.ts +2 -2
  186. package/dist/signals/extractor.js +31 -6
  187. package/dist/signals/index.d.ts +4 -1
  188. package/dist/signals/index.js +4 -1
  189. package/dist/signals/metaSignals.d.ts +4 -0
  190. package/dist/signals/metaSignals.js +42 -0
  191. package/dist/signals/scopeVocabulary.d.ts +75 -0
  192. package/dist/signals/scopeVocabulary.js +91 -0
  193. package/dist/signals/signalGate.js +1 -1
  194. package/dist/signals/taskDomain.d.ts +22 -0
  195. package/dist/signals/taskDomain.js +43 -0
  196. package/dist/strategy/constraintAblation.d.ts +64 -0
  197. package/dist/strategy/constraintAblation.js +2820 -0
  198. package/dist/strategy/constraintAblationPredicates.d.ts +31 -0
  199. package/dist/strategy/constraintAblationPredicates.js +339 -0
  200. package/dist/strategy/index.d.ts +2 -1
  201. package/dist/strategy/index.js +2 -1
  202. package/dist/trace/index.d.ts +3 -1
  203. package/dist/trace/index.js +3 -1
  204. package/dist/trace/learningTrace.d.ts +216 -0
  205. package/dist/trace/learningTrace.js +298 -0
  206. package/dist/trace/proxyTurns.d.ts +31 -0
  207. package/dist/trace/proxyTurns.js +137 -0
  208. package/dist/trace/trajectory.d.ts +8 -0
  209. package/dist/trace/trajectory.js +14 -2
  210. package/dist/util/fetchPort.d.ts +1 -0
  211. package/dist/util/fetchPort.js +11 -0
  212. package/dist/util/fileLock.d.ts +113 -7
  213. package/dist/util/fileLock.js +1035 -80
  214. package/dist/util/index.d.ts +3 -1
  215. package/dist/util/index.js +2 -1
  216. package/dist/verify/index.d.ts +2 -1
  217. package/dist/verify/index.js +1 -1
  218. package/dist/verify/sandboxRunner.d.ts +30 -0
  219. package/dist/verify/sandboxRunner.js +370 -27
  220. package/dist/verify/sandboxedValidation.d.ts +16 -2
  221. package/dist/verify/sandboxedValidation.js +200 -22
  222. package/dist/verify/validation.d.ts +35 -4
  223. package/dist/verify/validation.js +164 -16
  224. package/dist/wire/geneHints.d.ts +88 -16
  225. package/dist/wire/geneHints.js +124 -15
  226. package/dist/wire/index.d.ts +8 -3
  227. package/dist/wire/index.js +2 -2
  228. package/dist/wire/schemaGate.d.ts +21 -0
  229. package/dist/wire/schemaGate.js +119 -10
  230. package/dist/workflow/dsl.d.ts +24 -3
  231. package/dist/workflow/dsl.js +4 -0
  232. package/dist/workflow/engine.d.ts +5 -1
  233. package/dist/workflow/engine.js +3 -0
  234. package/dist/workflow/index.d.ts +3 -1
  235. package/dist/workflow/index.js +3 -1
  236. package/dist/workflow/runtime.d.ts +110 -0
  237. package/dist/workflow/runtime.js +1298 -0
  238. package/dist/workflow/stateStore.d.ts +172 -0
  239. package/dist/workflow/stateStore.js +1044 -0
  240. package/package.json +12 -4
@@ -1,33 +1,325 @@
1
1
  import { randomUUID } from 'node:crypto';
2
- import { readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
3
- import { basename, dirname, join, resolve } from 'node:path';
2
+ import { execFileSync } from 'node:child_process';
3
+ import { closeSync, constants, existsSync, fstatSync, fsyncSync, linkSync, lstatSync, openSync, readSync, readFileSync, renameSync, unlinkSync, writeFileSync, } from 'node:fs';
4
+ import { basename, dirname, join, resolve, win32 } from 'node:path';
4
5
  export function syncSleep(ms) {
5
6
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
6
7
  }
8
+ const MALFORMED_LOCK_GRACE_NS = 1000000000n;
9
+ const OBSERVED_PROCESS_IDENTITY_CACHE_NS = 5000000000n;
10
+ const PROCESS_IDENTITY_TIMEOUT_MS = 15_000;
11
+ const HOST_WINDOWS_SYSTEM_ROOT = process.env['SystemRoot']?.trim() || 'C:\\Windows';
7
12
  const ownedLocks = new Map();
13
+ const malformedLockObservations = new Map();
14
+ const observedProcessIdentities = new Map();
15
+ let currentProcessIdentity;
16
+ let fileLockTestHooks = {};
8
17
  export class LockTimeoutError extends Error {
9
18
  code = 'LOCK_TIMEOUT';
10
- constructor(lockPath) {
11
- super(`获取文件锁超时: ${lockPath}`);
19
+ constructor(_lockPath) {
20
+ super('获取文件锁超时');
12
21
  this.name = 'LockTimeoutError';
13
22
  }
14
23
  }
24
+ export class UnsafeLockPathError extends Error {
25
+ reason;
26
+ code = 'UNSAFE_LOCK_PATH';
27
+ constructor(reason) {
28
+ super(`不安全的文件锁路径: ${reason}`);
29
+ this.reason = reason;
30
+ this.name = 'UnsafeLockPathError';
31
+ }
32
+ }
33
+ export const MAX_LOCK_OWNER_BYTES = 4096;
15
34
  function lockKey(lockPath) {
16
35
  return resolve(lockPath);
17
36
  }
18
37
  function newOwner() {
19
- return { pid: process.pid, token: randomUUID() };
38
+ const processStart = processStartIdentity(process.pid);
39
+ if (processStart === null)
40
+ throw new UnsafeLockPathError('process_identity_unavailable');
41
+ return { pid: process.pid, token: randomUUID(), processStart };
20
42
  }
21
43
  function serializeOwner(owner) {
22
- return `${JSON.stringify({ v: 1, pid: owner.pid, token: owner.token })}\n`;
44
+ return `${JSON.stringify({
45
+ v: 2,
46
+ pid: owner.pid,
47
+ token: owner.token,
48
+ processStart: owner.processStart,
49
+ })}\n`;
50
+ }
51
+ function publicOwnerRecord(owner) {
52
+ return {
53
+ pid: owner.pid,
54
+ token: owner.token,
55
+ processStartIdentity: { ...owner.processStart },
56
+ };
23
57
  }
24
58
  function isErrno(error, code) {
25
59
  return typeof error === 'object' && error !== null && error.code === code;
26
60
  }
61
+ function validPositiveIntegerText(value) {
62
+ return /^[1-9]\d*$/.test(value);
63
+ }
64
+ function validIdentityText(value, maxLength) {
65
+ if (value.length === 0 || value.length > maxLength)
66
+ return false;
67
+ for (let index = 0; index < value.length; index += 1) {
68
+ const code = value.charCodeAt(index);
69
+ if (code <= 0x1f || code === 0x7f)
70
+ return false;
71
+ }
72
+ return true;
73
+ }
74
+ let cachedLinuxBootId;
75
+ function linuxBootId() {
76
+ if (cachedLinuxBootId !== undefined)
77
+ return cachedLinuxBootId;
78
+ try {
79
+ const value = readFileSync('/proc/sys/kernel/random/boot_id', 'utf8').trim().toLowerCase();
80
+ cachedLinuxBootId = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(value)
81
+ ? value
82
+ : null;
83
+ }
84
+ catch {
85
+ cachedLinuxBootId = null;
86
+ }
87
+ return cachedLinuxBootId;
88
+ }
89
+ function linuxProcessStartTicks(pid) {
90
+ try {
91
+ const raw = readFileSync(`/proc/${pid}/stat`, 'utf8');
92
+ const commandEnd = raw.lastIndexOf(')');
93
+ if (commandEnd < 0)
94
+ return null;
95
+ const fields = raw.slice(commandEnd + 1).trim().split(/\s+/);
96
+ const startTicks = fields[19];
97
+ return startTicks !== undefined && validPositiveIntegerText(startTicks) ? startTicks : null;
98
+ }
99
+ catch {
100
+ return null;
101
+ }
102
+ }
103
+ function linuxProcessStartIdentity(pid) {
104
+ const first = linuxProcessStartTicks(pid);
105
+ const bootId = linuxBootId();
106
+ const second = linuxProcessStartTicks(pid);
107
+ return first !== null && first === second && bootId !== null
108
+ ? { source: 'linux-proc', bootId, startTicks: first }
109
+ : null;
110
+ }
111
+ function trustedWindowsPowerShell() {
112
+ if (!win32.isAbsolute(HOST_WINDOWS_SYSTEM_ROOT)
113
+ || !validIdentityText(HOST_WINDOWS_SYSTEM_ROOT, 1024))
114
+ return null;
115
+ const executable = win32.join(HOST_WINDOWS_SYSTEM_ROOT, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
116
+ return existsSync(executable) ? executable : null;
117
+ }
118
+ function windowsProcessStartIdentity(pid) {
119
+ const powershell = trustedWindowsPowerShell();
120
+ if (powershell === null)
121
+ return null;
122
+ const script = [
123
+ `$process = Get-Process -Id ${pid} -ErrorAction Stop`,
124
+ '$ticks = $process.StartTime.ToUniversalTime().Ticks',
125
+ '[Console]::Out.Write($ticks.ToString([Globalization.CultureInfo]::InvariantCulture))',
126
+ ].join('; ');
127
+ try {
128
+ const startTimeTicks = execFileSync(powershell, ['-NoLogo', '-NoProfile', '-NonInteractive', '-Command', script], {
129
+ encoding: 'utf8',
130
+ stdio: ['ignore', 'pipe', 'ignore'],
131
+ timeout: PROCESS_IDENTITY_TIMEOUT_MS,
132
+ windowsHide: true,
133
+ }).trim();
134
+ return validPositiveIntegerText(startTimeTicks)
135
+ ? { source: 'windows-powershell', startTimeTicks }
136
+ : null;
137
+ }
138
+ catch {
139
+ return null;
140
+ }
141
+ }
142
+ function darwinProcessStartIdentity(pid) {
143
+ try {
144
+ const startTime = execFileSync('/bin/ps', ['-o', 'lstart=', '-p', String(pid)], {
145
+ encoding: 'utf8',
146
+ env: { ...process.env, LANG: 'C', LC_ALL: 'C' },
147
+ stdio: ['ignore', 'pipe', 'ignore'],
148
+ timeout: PROCESS_IDENTITY_TIMEOUT_MS,
149
+ }).trim().replace(/\s+/g, ' ');
150
+ return validIdentityText(startTime, 128)
151
+ ? { source: 'darwin-ps', startTime }
152
+ : null;
153
+ }
154
+ catch {
155
+ return null;
156
+ }
157
+ }
158
+ function nativeProcessStartIdentity(pid) {
159
+ if (!Number.isInteger(pid) || pid <= 0)
160
+ return null;
161
+ if (process.platform === 'linux')
162
+ return linuxProcessStartIdentity(pid);
163
+ if (process.platform === 'win32')
164
+ return windowsProcessStartIdentity(pid);
165
+ if (process.platform === 'darwin')
166
+ return darwinProcessStartIdentity(pid);
167
+ return null;
168
+ }
169
+ function processStartIdentity(pid, claimedIdentity) {
170
+ if (pid === process.pid) {
171
+ if (currentProcessIdentity === undefined) {
172
+ const resolved = (fileLockTestHooks.processStartIdentity ?? nativeProcessStartIdentity)(pid);
173
+ if (resolved !== null)
174
+ currentProcessIdentity = resolved;
175
+ return resolved;
176
+ }
177
+ return currentProcessIdentity;
178
+ }
179
+ const now = process.hrtime.bigint();
180
+ const cached = observedProcessIdentities.get(pid);
181
+ if (cached !== undefined
182
+ && claimedIdentity !== undefined
183
+ && sameProcessStartIdentity(cached.claimedIdentity, claimedIdentity)
184
+ && now < cached.expiresAtNs) {
185
+ return cached.identity;
186
+ }
187
+ const identity = (fileLockTestHooks.processStartIdentity ?? nativeProcessStartIdentity)(pid);
188
+ if (claimedIdentity !== undefined) {
189
+ observedProcessIdentities.set(pid, {
190
+ claimedIdentity,
191
+ identity,
192
+ expiresAtNs: now + OBSERVED_PROCESS_IDENTITY_CACHE_NS,
193
+ });
194
+ }
195
+ return identity;
196
+ }
197
+ function sameProcessStartIdentity(left, right) {
198
+ if (left.source !== right.source)
199
+ return false;
200
+ if (left.source === 'linux-proc' && right.source === 'linux-proc') {
201
+ return left.bootId === right.bootId && left.startTicks === right.startTicks;
202
+ }
203
+ if (left.source === 'windows-powershell' && right.source === 'windows-powershell') {
204
+ return left.startTimeTicks === right.startTimeTicks;
205
+ }
206
+ return left.source === 'darwin-ps'
207
+ && right.source === 'darwin-ps'
208
+ && left.startTime === right.startTime;
209
+ }
210
+ /** Fresh native process-start observation. Unlike lock acquisition, this does not use caches. */
211
+ export function readFileLockProcessStartIdentity(pid) {
212
+ if (!Number.isInteger(pid) || pid <= 0)
213
+ return null;
214
+ const identity = (fileLockTestHooks.processStartIdentity ?? nativeProcessStartIdentity)(pid);
215
+ return identity === null ? null : { ...identity };
216
+ }
217
+ export function sameFileLockProcessStartIdentity(left, right) {
218
+ return sameProcessStartIdentity(left, right);
219
+ }
220
+ export function parseFileLockProcessStartIdentity(value) {
221
+ return parseProcessStartIdentity(value) ?? undefined;
222
+ }
223
+ /**
224
+ * Conservative owner classification for crash recovery. PID reuse and unavailable identity
225
+ * remain distinct from a proven-dead PID so callers cannot reclaim ambiguous ownership.
226
+ */
227
+ export function inspectFileLockOwnerProcess(owner) {
228
+ if (!pidAlive(owner.pid))
229
+ return 'dead';
230
+ const current = readFileLockProcessStartIdentity(owner.pid);
231
+ if (current === null)
232
+ return 'unverifiable';
233
+ return sameProcessStartIdentity(owner.processStartIdentity, current) ? 'current' : 'pid_reused';
234
+ }
235
+ export function _setFileLockTestHooksForTest(hooks = {}) {
236
+ fileLockTestHooks = hooks;
237
+ currentProcessIdentity = undefined;
238
+ observedProcessIdentities.clear();
239
+ }
240
+ function noFollowFlag() {
241
+ return constants['O_NOFOLLOW'] ?? 0;
242
+ }
243
+ function assertRegularOwnerStat(stat) {
244
+ if (stat.isSymbolicLink())
245
+ throw new UnsafeLockPathError('symlink');
246
+ if (!stat.isFile())
247
+ throw new UnsafeLockPathError('not_regular_file');
248
+ if (stat.size > BigInt(MAX_LOCK_OWNER_BYTES))
249
+ throw new UnsafeLockPathError('owner_too_large');
250
+ }
251
+ function currentOwnerStat(path) {
252
+ try {
253
+ const stat = lstatSync(path, { bigint: true });
254
+ assertRegularOwnerStat(stat);
255
+ return stat;
256
+ }
257
+ catch (error) {
258
+ if (isErrno(error, 'ENOENT'))
259
+ return null;
260
+ if (isErrno(error, 'EACCES') || isErrno(error, 'EPERM')) {
261
+ throw new UnsafeLockPathError('permission_denied');
262
+ }
263
+ throw error;
264
+ }
265
+ }
266
+ function readOwnerFileBounded(path) {
267
+ const before = currentOwnerStat(path);
268
+ if (before === null)
269
+ return null;
270
+ let fd;
271
+ try {
272
+ fd = openSync(path, constants.O_RDONLY | noFollowFlag());
273
+ }
274
+ catch (error) {
275
+ if (isErrno(error, 'ENOENT'))
276
+ return null;
277
+ if (isErrno(error, 'ELOOP'))
278
+ throw new UnsafeLockPathError('symlink');
279
+ if (isErrno(error, 'EACCES') || isErrno(error, 'EPERM')) {
280
+ throw new UnsafeLockPathError('permission_denied');
281
+ }
282
+ throw error;
283
+ }
284
+ try {
285
+ const opened = fstatSync(fd, { bigint: true });
286
+ assertRegularOwnerStat(opened);
287
+ const current = currentOwnerStat(path);
288
+ if (current === null || current.dev !== opened.dev || current.ino !== opened.ino) {
289
+ throw new UnsafeLockPathError('path_changed');
290
+ }
291
+ const buffer = Buffer.alloc(MAX_LOCK_OWNER_BYTES + 1);
292
+ let total = 0;
293
+ while (total < buffer.length) {
294
+ const read = readSync(fd, buffer, total, buffer.length - total, total);
295
+ if (read === 0)
296
+ break;
297
+ total += read;
298
+ }
299
+ if (total > MAX_LOCK_OWNER_BYTES)
300
+ throw new UnsafeLockPathError('owner_too_large');
301
+ const settled = fstatSync(fd, { bigint: true });
302
+ assertRegularOwnerStat(settled);
303
+ const settledPath = currentOwnerStat(path);
304
+ if (settledPath === null
305
+ || settledPath.dev !== settled.dev
306
+ || settledPath.ino !== settled.ino
307
+ || settledPath.size !== settled.size
308
+ || settledPath.mtimeNs !== settled.mtimeNs) {
309
+ throw new UnsafeLockPathError('path_changed');
310
+ }
311
+ return { raw: buffer.subarray(0, total).toString('utf8'), stat: settled };
312
+ }
313
+ finally {
314
+ closeSync(fd);
315
+ }
316
+ }
27
317
  /** True if pid is a live process (cross-platform via signal 0; EPERM = exists but owned by another user). */
28
318
  function pidAlive(pid) {
29
319
  if (!Number.isInteger(pid) || pid <= 0)
30
320
  return false;
321
+ if (fileLockTestHooks.pidAlive)
322
+ return fileLockTestHooks.pidAlive(pid);
31
323
  try {
32
324
  process.kill(pid, 0);
33
325
  return true;
@@ -36,20 +328,57 @@ function pidAlive(pid) {
36
328
  return e.code === 'EPERM';
37
329
  }
38
330
  }
331
+ function parseProcessStartIdentity(value) {
332
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
333
+ return null;
334
+ const record = value;
335
+ if (record['source'] === 'linux-proc') {
336
+ const bootId = record['bootId'];
337
+ const startTicks = record['startTicks'];
338
+ return typeof bootId === 'string'
339
+ && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(bootId)
340
+ && typeof startTicks === 'string'
341
+ && validPositiveIntegerText(startTicks)
342
+ ? { source: 'linux-proc', bootId, startTicks }
343
+ : null;
344
+ }
345
+ if (record['source'] === 'windows-powershell') {
346
+ const startTimeTicks = record['startTimeTicks'];
347
+ return typeof startTimeTicks === 'string' && validPositiveIntegerText(startTimeTicks)
348
+ ? { source: 'windows-powershell', startTimeTicks }
349
+ : null;
350
+ }
351
+ if (record['source'] === 'darwin-ps') {
352
+ const startTime = record['startTime'];
353
+ return typeof startTime === 'string' && validIdentityText(startTime, 128)
354
+ ? { source: 'darwin-ps', startTime }
355
+ : null;
356
+ }
357
+ return null;
358
+ }
39
359
  function parseJsonOwner(raw) {
40
360
  try {
41
361
  const value = JSON.parse(raw);
42
- if (typeof value !== 'object' || value === null)
362
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
43
363
  return null;
44
364
  const record = value;
45
- if (typeof record.pid !== 'number'
46
- || !Number.isInteger(record.pid)
47
- || record.pid <= 0
48
- || typeof record.token !== 'string'
49
- || record.token.length === 0) {
365
+ if (typeof record['pid'] !== 'number'
366
+ || !Number.isInteger(record['pid'])
367
+ || record['pid'] <= 0
368
+ || typeof record['token'] !== 'string'
369
+ || record['token'].length === 0) {
50
370
  return null;
51
371
  }
52
- return { pid: record.pid, token: record.token };
372
+ if (record['v'] === 2) {
373
+ const processStart = parseProcessStartIdentity(record['processStart']);
374
+ return processStart === null
375
+ ? null
376
+ : { pid: record['pid'], token: record['token'], processStart };
377
+ }
378
+ if (record['v'] === 1 || record['v'] === undefined) {
379
+ return { pid: record['pid'], token: record['token'], processStart: null };
380
+ }
381
+ return null;
53
382
  }
54
383
  catch (error) {
55
384
  if (error instanceof SyntaxError)
@@ -62,7 +391,7 @@ function parseLegacyOwner(raw) {
62
391
  if (first === undefined)
63
392
  return null;
64
393
  const pid = Number(first);
65
- return Number.isInteger(pid) && pid > 0 ? { pid, token: null } : null;
394
+ return Number.isInteger(pid) && pid > 0 ? { pid, token: null, processStart: null } : null;
66
395
  }
67
396
  function parseOwner(raw) {
68
397
  const trimmed = raw.trim();
@@ -70,19 +399,160 @@ function parseOwner(raw) {
70
399
  return null;
71
400
  return parseJsonOwner(trimmed) ?? parseLegacyOwner(trimmed);
72
401
  }
73
- /** Owner recorded in the lock file; null if missing/unparseable. */
74
- function lockOwner(lockPath) {
402
+ function readLockSnapshot(lockPath) {
403
+ const read = readOwnerFileBounded(lockPath);
404
+ if (read === null)
405
+ return null;
406
+ return {
407
+ owner: parseOwner(read.raw),
408
+ dev: read.stat.dev,
409
+ ino: read.stat.ino,
410
+ size: read.stat.size,
411
+ mtimeNs: read.stat.mtimeNs,
412
+ };
413
+ }
414
+ /** Snapshot of the lock inode and its owner payload; null if the path disappeared or changed during inspection. */
415
+ function lockSnapshot(lockPath) {
75
416
  try {
76
- return parseOwner(readFileSync(lockPath, 'utf8'));
417
+ return readLockSnapshot(lockPath);
77
418
  }
78
419
  catch (error) {
79
- if (isErrno(error, 'ENOENT'))
420
+ // A valid owner can release and another waiter can acquire between lstat/open/fstat. Treat that snapshot as
421
+ // transiently unavailable: callers remain conservative (no steal/delete) and the acquire loop retries.
422
+ if (error instanceof UnsafeLockPathError && error.reason === 'path_changed')
80
423
  return null;
81
424
  throw error;
82
425
  }
83
426
  }
427
+ function sameInode(left, right) {
428
+ return left !== null && right !== null && left.dev === right.dev && left.ino === right.ino;
429
+ }
430
+ function sameSnapshot(left, right) {
431
+ return sameInode(left, right)
432
+ && left?.size === right?.size
433
+ && left?.mtimeNs === right?.mtimeNs;
434
+ }
435
+ function forgetMalformedObservation(lockPath) {
436
+ malformedLockObservations.delete(lockKey(lockPath));
437
+ }
438
+ function reclaimableSnapshot(lockPath, snapshot) {
439
+ if (snapshot.owner !== null) {
440
+ forgetMalformedObservation(lockPath);
441
+ if (!pidAlive(snapshot.owner.pid)) {
442
+ observedProcessIdentities.delete(snapshot.owner.pid);
443
+ return true;
444
+ }
445
+ if (snapshot.owner.processStart === null)
446
+ return false;
447
+ const liveProcessStart = processStartIdentity(snapshot.owner.pid, snapshot.owner.processStart);
448
+ return liveProcessStart !== null
449
+ && !sameProcessStartIdentity(snapshot.owner.processStart, liveProcessStart);
450
+ }
451
+ const key = lockKey(lockPath);
452
+ const now = process.hrtime.bigint();
453
+ const observed = malformedLockObservations.get(key);
454
+ if (observed === undefined || !sameSnapshot(observed.snapshot, snapshot)) {
455
+ malformedLockObservations.set(key, { snapshot, firstSeenAtNs: now });
456
+ return false;
457
+ }
458
+ return now - observed.firstSeenAtNs >= MALFORMED_LOCK_GRACE_NS;
459
+ }
84
460
  function sameOwner(parsed, owner) {
85
- return parsed?.pid === owner.pid && parsed.token === owner.token;
461
+ return parsed?.pid === owner.pid
462
+ && parsed.token === owner.token
463
+ && parsed.processStart !== null
464
+ && sameProcessStartIdentity(parsed.processStart, owner.processStart);
465
+ }
466
+ function guardianPath(lockPath) {
467
+ return join(dirname(lockPath), `.${basename(lockPath)}.guardian`);
468
+ }
469
+ function serializedOwnerRecord(owner) {
470
+ return {
471
+ pid: owner.pid,
472
+ token: owner.token,
473
+ processStart: owner.processStart,
474
+ };
475
+ }
476
+ function parseGuardianOwner(value) {
477
+ if (!value || typeof value !== 'object' || Array.isArray(value))
478
+ return null;
479
+ const record = value;
480
+ const keys = Object.keys(record).sort();
481
+ if (keys.length !== 3
482
+ || keys[0] !== 'pid'
483
+ || keys[1] !== 'processStart'
484
+ || keys[2] !== 'token'
485
+ || !Number.isSafeInteger(record['pid'])
486
+ || record['pid'] <= 0
487
+ || typeof record['token'] !== 'string'
488
+ || record['token'].length === 0
489
+ || record['token'].length > 128)
490
+ return null;
491
+ const processStart = parseProcessStartIdentity(record['processStart']);
492
+ return processStart === null
493
+ ? null
494
+ : { pid: record['pid'], token: record['token'], processStart };
495
+ }
496
+ function parseLockGuardian(raw) {
497
+ try {
498
+ const value = JSON.parse(raw);
499
+ if (!value || typeof value !== 'object' || Array.isArray(value))
500
+ return null;
501
+ const record = value;
502
+ const keys = Object.keys(record).sort();
503
+ if (keys.length !== 3 || keys[0] !== 'from' || keys[1] !== 'guardian'
504
+ || keys[2] !== 'v' || record['v'] !== 1)
505
+ return null;
506
+ const from = parseGuardianOwner(record['from']);
507
+ const guardian = parseGuardianOwner(record['guardian']);
508
+ return from && guardian ? { from, guardian } : null;
509
+ }
510
+ catch (error) {
511
+ if (error instanceof SyntaxError)
512
+ return null;
513
+ throw error;
514
+ }
515
+ }
516
+ function serializeLockGuardian(record) {
517
+ return `${JSON.stringify({
518
+ v: 1,
519
+ from: serializedOwnerRecord(record.from),
520
+ guardian: serializedOwnerRecord(record.guardian),
521
+ })}\n`;
522
+ }
523
+ function readLockGuardianSnapshot(lockPath) {
524
+ const read = readOwnerFileBounded(guardianPath(lockPath));
525
+ if (read === null)
526
+ return null;
527
+ return {
528
+ record: parseLockGuardian(read.raw),
529
+ snapshot: {
530
+ owner: null,
531
+ dev: read.stat.dev,
532
+ ino: read.stat.ino,
533
+ size: read.stat.size,
534
+ mtimeNs: read.stat.mtimeNs,
535
+ },
536
+ };
537
+ }
538
+ function guardianReclaimable(record) {
539
+ if (!pidAlive(record.guardian.pid)) {
540
+ observedProcessIdentities.delete(record.guardian.pid);
541
+ return true;
542
+ }
543
+ const current = processStartIdentity(record.guardian.pid, record.guardian.processStart);
544
+ return current !== null && !sameProcessStartIdentity(record.guardian.processStart, current);
545
+ }
546
+ function sameGuardianRecord(left, right) {
547
+ return sameOwner({
548
+ pid: left.from.pid,
549
+ token: left.from.token,
550
+ processStart: left.from.processStart,
551
+ }, right.from) && sameOwner({
552
+ pid: left.guardian.pid,
553
+ token: left.guardian.token,
554
+ processStart: left.guardian.processStart,
555
+ }, right.guardian);
86
556
  }
87
557
  function sidecarPath(lockPath, kind) {
88
558
  return join(dirname(lockPath), `.${basename(lockPath)}.${kind}.${process.pid}.${randomUUID()}`);
@@ -99,129 +569,614 @@ function removeFileIfExists(path) {
99
569
  throw error;
100
570
  }
101
571
  }
102
- function createLockFile(lockPath, owner, trackOwnership) {
572
+ function pathExists(path) {
573
+ return existsSync(path);
574
+ }
575
+ function restoreMovedFile(lockPath, movedPath, moved) {
103
576
  try {
104
- writeFileSync(lockPath, serializeOwner(owner), { flag: 'wx', mode: 0o600 });
105
- if (trackOwnership)
106
- ownedLocks.set(lockKey(lockPath), owner);
107
- return true;
577
+ linkSync(movedPath, lockPath);
578
+ removeFileIfExists(movedPath);
579
+ forgetMalformedObservation(lockPath);
580
+ return;
581
+ }
582
+ catch (error) {
583
+ if (!isErrno(error, 'EEXIST'))
584
+ throw error;
585
+ }
586
+ const current = lockSnapshot(lockPath);
587
+ if (sameInode(current, moved)) {
588
+ removeFileIfExists(movedPath);
589
+ forgetMalformedObservation(lockPath);
590
+ return;
591
+ }
592
+ throw new Error(`文件锁在安全回收期间被替换,已保留候选文件: ${movedPath}`);
593
+ }
594
+ function removeUnchangedLock(lockPath, expected, kind, onCleanupError) {
595
+ const movedPath = sidecarPath(lockPath, kind);
596
+ try {
597
+ renameSync(lockPath, movedPath);
108
598
  }
109
599
  catch (error) {
110
- if (isErrno(error, 'EEXIST'))
600
+ if (isErrno(error, 'ENOENT'))
111
601
  return false;
112
602
  throw error;
113
603
  }
604
+ const moved = lockSnapshot(movedPath);
605
+ if (!sameSnapshot(moved, expected)) {
606
+ if (moved !== null)
607
+ restoreMovedFile(lockPath, movedPath, moved);
608
+ return false;
609
+ }
610
+ try {
611
+ removeFileIfExists(movedPath);
612
+ }
613
+ catch (error) {
614
+ if (onCleanupError === undefined)
615
+ throw error;
616
+ onCleanupError(error);
617
+ }
618
+ forgetMalformedObservation(lockPath);
619
+ return true;
620
+ }
621
+ function removeFileIfOwned(lockPath, owner, onCleanupError) {
622
+ const snapshot = lockSnapshot(lockPath);
623
+ if (snapshot === null || !sameOwner(snapshot.owner, owner))
624
+ return false;
625
+ return removeUnchangedLock(lockPath, snapshot, 'released', onCleanupError);
626
+ }
627
+ function publishOwnerFile(lockPath, owner) {
628
+ const temporaryPath = sidecarPath(lockPath, 'publishing');
629
+ let descriptor;
630
+ let published = false;
631
+ let publicationResult = false;
632
+ let failure;
633
+ try {
634
+ descriptor = openSync(temporaryPath, 'wx', 0o600);
635
+ writeFileSync(descriptor, serializeOwner(owner), { encoding: 'utf8' });
636
+ fsyncSync(descriptor);
637
+ closeSync(descriptor);
638
+ descriptor = undefined;
639
+ fileLockTestHooks.beforeOwnerPublish?.(temporaryPath, lockPath);
640
+ try {
641
+ linkSync(temporaryPath, lockPath);
642
+ published = true;
643
+ publicationResult = true;
644
+ }
645
+ catch (error) {
646
+ if (!isErrno(error, 'EEXIST'))
647
+ failure = error;
648
+ }
649
+ }
650
+ catch (error) {
651
+ failure = error;
652
+ }
653
+ finally {
654
+ if (descriptor !== undefined) {
655
+ try {
656
+ closeSync(descriptor);
657
+ }
658
+ catch (error) {
659
+ failure ??= error;
660
+ }
661
+ }
662
+ try {
663
+ unlinkSync(temporaryPath);
664
+ }
665
+ catch (error) {
666
+ // Once linked, the final owner is valid and must be tracked even if the temporary name cannot be removed.
667
+ if (!published && !isErrno(error, 'ENOENT'))
668
+ failure ??= error;
669
+ }
670
+ }
671
+ if (failure !== undefined)
672
+ throw failure;
673
+ return publicationResult;
674
+ }
675
+ function publishGuardianFile(lockPath, record) {
676
+ const finalPath = guardianPath(lockPath);
677
+ const temporaryPath = sidecarPath(finalPath, 'publishing');
678
+ let descriptor;
679
+ let published = false;
680
+ let publicationResult = false;
681
+ let failure;
682
+ try {
683
+ descriptor = openSync(temporaryPath, 'wx', 0o600);
684
+ writeFileSync(descriptor, serializeLockGuardian(record), { encoding: 'utf8' });
685
+ fsyncSync(descriptor);
686
+ closeSync(descriptor);
687
+ descriptor = undefined;
688
+ fileLockTestHooks.beforeGuardianPublish?.(temporaryPath, finalPath);
689
+ try {
690
+ linkSync(temporaryPath, finalPath);
691
+ published = true;
692
+ publicationResult = true;
693
+ }
694
+ catch (error) {
695
+ if (!isErrno(error, 'EEXIST'))
696
+ failure = error;
697
+ }
698
+ }
699
+ catch (error) {
700
+ failure = error;
701
+ }
702
+ finally {
703
+ if (descriptor !== undefined) {
704
+ try {
705
+ closeSync(descriptor);
706
+ }
707
+ catch (error) {
708
+ failure ??= error;
709
+ }
710
+ }
711
+ try {
712
+ unlinkSync(temporaryPath);
713
+ }
714
+ catch (error) {
715
+ if (!published && !isErrno(error, 'ENOENT'))
716
+ failure ??= error;
717
+ }
718
+ }
719
+ if (failure !== undefined)
720
+ throw failure;
721
+ return publicationResult;
722
+ }
723
+ function createLockFile(lockPath, owner, trackOwnership) {
724
+ const guardPath = mutationGuardPath(lockPath);
725
+ if (trackOwnership && pathExists(guardPath))
726
+ return false;
727
+ if (trackOwnership && pathExists(guardianPath(lockPath)))
728
+ return false;
729
+ if (pathExists(lockPath))
730
+ return false;
731
+ for (let permissionAttempt = 0; permissionAttempt < 2; permissionAttempt++) {
732
+ try {
733
+ if (!publishOwnerFile(lockPath, owner))
734
+ return false;
735
+ if (trackOwnership && pathExists(guardPath)) {
736
+ removeFileIfOwned(lockPath, owner);
737
+ return false;
738
+ }
739
+ if (trackOwnership)
740
+ ownedLocks.set(lockKey(lockPath), { owner, releasePending: false });
741
+ forgetMalformedObservation(lockPath);
742
+ return true;
743
+ }
744
+ catch (error) {
745
+ if (isErrno(error, 'EEXIST'))
746
+ return false;
747
+ if (isErrno(error, 'EACCES') || isErrno(error, 'EPERM')) {
748
+ // Windows can report EPERM instead of EEXIST during contention. A regular owner is busy; if the owner
749
+ // disappeared before lstat, retry create once to distinguish that release race from a persistent ACL
750
+ // failure. Unsafe/inaccessible paths still throw from currentOwnerStat and remain fail closed.
751
+ if (currentOwnerStat(lockPath) !== null)
752
+ return false;
753
+ if (permissionAttempt === 0)
754
+ continue;
755
+ throw new UnsafeLockPathError('permission_denied');
756
+ }
757
+ throw error;
758
+ }
759
+ }
760
+ throw new UnsafeLockPathError('permission_denied');
114
761
  }
115
762
  function tryAcquireMutationGuard(lockPath) {
116
763
  const guardPath = mutationGuardPath(lockPath);
117
764
  const guard = newOwner();
118
765
  if (createLockFile(guardPath, guard, false))
119
766
  return guard;
120
- const owner = lockOwner(guardPath);
121
- if (owner === null || owner.pid === process.pid || pidAlive(owner.pid))
767
+ const snapshot = lockSnapshot(guardPath);
768
+ if (snapshot === null || !reclaimableSnapshot(guardPath, snapshot))
769
+ return null;
770
+ if (!removeUnchangedLock(guardPath, snapshot, 'stale'))
122
771
  return null;
123
- const stalePath = sidecarPath(guardPath, 'stale');
772
+ return createLockFile(guardPath, guard, false) ? guard : null;
773
+ }
774
+ function releaseMutationGuard(lockPath, guard, onCleanupError) {
775
+ fileLockTestHooks.beforeMutationGuardRelease?.(mutationGuardPath(lockPath), lockPath);
776
+ return removeFileIfOwned(mutationGuardPath(lockPath), guard, onCleanupError);
777
+ }
778
+ function withMutationGuardRelease(lockPath, guard, operation, onCleanupError) {
779
+ let operationCompleted = false;
780
+ let operationResult;
781
+ let operationError;
124
782
  try {
125
- renameSync(guardPath, stalePath);
783
+ operationResult = operation();
784
+ operationCompleted = true;
126
785
  }
127
786
  catch (error) {
128
- if (isErrno(error, 'ENOENT'))
129
- return null;
130
- throw error;
787
+ operationError = error;
131
788
  }
132
- removeFileIfExists(stalePath);
133
- return createLockFile(guardPath, guard, false) ? guard : null;
134
- }
135
- function releaseMutationGuard(lockPath, guard) {
136
- const guardPath = mutationGuardPath(lockPath);
137
- if (!sameOwner(lockOwner(guardPath), guard))
138
- return;
139
- const releasedPath = sidecarPath(guardPath, 'released');
789
+ let releaseError;
140
790
  try {
141
- renameSync(guardPath, releasedPath);
791
+ if (!releaseMutationGuard(lockPath, guard, onCleanupError)) {
792
+ releaseError = new UnsafeLockPathError('path_changed');
793
+ }
142
794
  }
143
795
  catch (error) {
144
- if (isErrno(error, 'ENOENT'))
145
- return;
146
- throw error;
796
+ releaseError = error;
797
+ }
798
+ if (!operationCompleted) {
799
+ if (releaseError !== undefined) {
800
+ throw new AggregateError([operationError, releaseError], 'file lock mutation failed and mutation-guard release is unconfirmed');
801
+ }
802
+ throw operationError;
147
803
  }
148
- removeFileIfExists(releasedPath);
804
+ if (releaseError !== undefined)
805
+ throw releaseError;
806
+ return operationResult;
149
807
  }
150
808
  function reclaimStaleLock(lockPath) {
151
809
  const guard = tryAcquireMutationGuard(lockPath);
152
810
  if (guard === null)
153
811
  return false;
154
812
  try {
155
- const owner = lockOwner(lockPath);
156
- if (owner === null || owner.pid === process.pid || pidAlive(owner.pid))
813
+ const snapshot = lockSnapshot(lockPath);
814
+ if (snapshot === null || !reclaimableSnapshot(lockPath, snapshot))
157
815
  return false;
158
- const stalePath = sidecarPath(lockPath, 'stale');
159
- try {
160
- renameSync(lockPath, stalePath);
161
- }
162
- catch (error) {
163
- if (isErrno(error, 'ENOENT'))
816
+ return removeUnchangedLock(lockPath, snapshot, 'stale');
817
+ }
818
+ finally {
819
+ releaseMutationGuard(lockPath, guard);
820
+ }
821
+ }
822
+ function reclaimStaleGuardian(lockPath) {
823
+ const guard = tryAcquireMutationGuard(lockPath);
824
+ if (guard === null)
825
+ return false;
826
+ try {
827
+ const observed = readLockGuardianSnapshot(lockPath);
828
+ if (observed === null)
829
+ return false;
830
+ const receiptPath = guardianPath(lockPath);
831
+ if (observed.record === null) {
832
+ if (!reclaimableSnapshot(receiptPath, observed.snapshot))
164
833
  return false;
165
- throw error;
834
+ fileLockTestHooks.beforeGuardianReclaim?.(receiptPath, lockPath);
835
+ return removeUnchangedLock(receiptPath, observed.snapshot, 'guardian-malformed');
166
836
  }
167
- removeFileIfExists(stalePath);
168
- return true;
837
+ if (!guardianReclaimable(observed.record))
838
+ return false;
839
+ fileLockTestHooks.beforeGuardianReclaim?.(guardianPath(lockPath), lockPath);
840
+ const current = lockSnapshot(lockPath);
841
+ const locallyRetained = observed.record.from.pid === process.pid
842
+ && ownedLocks.get(lockKey(lockPath)) === undefined;
843
+ if (current !== null
844
+ && sameOwner(current.owner, observed.record.from)
845
+ && (locallyRetained || reclaimableSnapshot(lockPath, current))) {
846
+ removeUnchangedLock(lockPath, current, 'guardian-owner-stale');
847
+ }
848
+ return removeUnchangedLock(receiptPath, observed.snapshot, 'guardian-stale');
169
849
  }
170
850
  finally {
171
851
  releaseMutationGuard(lockPath, guard);
172
852
  }
173
853
  }
854
+ function samePublicOwner(left, right) {
855
+ return left.pid === right.pid
856
+ && left.token === right.token
857
+ && sameProcessStartIdentity(left.processStartIdentity, right.processStart);
858
+ }
859
+ function observeTransferTarget(pid) {
860
+ if (!Number.isSafeInteger(pid) || pid <= 0 || !pidAlive(pid)) {
861
+ return { reason: 'target_process_dead' };
862
+ }
863
+ const first = readFileLockProcessStartIdentity(pid);
864
+ if (first === null)
865
+ return { reason: 'target_process_unverifiable' };
866
+ if (!pidAlive(pid))
867
+ return { reason: 'target_process_dead' };
868
+ const second = readFileLockProcessStartIdentity(pid);
869
+ if (second === null)
870
+ return { reason: 'target_process_unverifiable' };
871
+ if (!sameProcessStartIdentity(first, second)) {
872
+ return { reason: 'target_process_pid_reused' };
873
+ }
874
+ if (!pidAlive(pid))
875
+ return { reason: 'target_process_dead' };
876
+ return { owner: { pid, token: randomUUID(), processStart: second } };
877
+ }
878
+ /**
879
+ * Pre-arm an acquired lock with the exact child PID generation while retaining local ownership.
880
+ * A controller can therefore keep mutating its journal, while a hard controller crash cannot make
881
+ * the primary owner reclaimable until the child exits.
882
+ */
883
+ export function attachLockGuardianToProcess(lockPath, expectedOwner, targetPid) {
884
+ const key = lockKey(lockPath);
885
+ const owned = ownedLocks.get(key);
886
+ if (owned === undefined)
887
+ return { attached: false, reason: 'not_owned' };
888
+ if (owned.releasePending || !samePublicOwner(expectedOwner, owned.owner)) {
889
+ return { attached: false, reason: 'ownership_changed' };
890
+ }
891
+ const guard = tryAcquireMutationGuard(lockPath);
892
+ if (guard === null)
893
+ return { attached: false, reason: 'mutation_busy' };
894
+ const guarded = withMutationGuardRelease(lockPath, guard, () => {
895
+ const current = readLockSnapshot(lockPath);
896
+ if (current === null || !sameOwner(current.owner, owned.owner)) {
897
+ return { result: { attached: false, reason: 'ownership_changed' } };
898
+ }
899
+ if (owned.guardian !== undefined || readLockGuardianSnapshot(lockPath) !== null) {
900
+ return { result: { attached: false, reason: 'guardian_exists' } };
901
+ }
902
+ const target = observeTransferTarget(targetPid);
903
+ if ('reason' in target) {
904
+ return { result: { attached: false, reason: target.reason } };
905
+ }
906
+ const record = {
907
+ from: owned.owner,
908
+ guardian: target.owner,
909
+ };
910
+ if (!publishGuardianFile(lockPath, record)) {
911
+ return { result: { attached: false, reason: 'guardian_exists' } };
912
+ }
913
+ const publishedGuardian = readLockGuardianSnapshot(lockPath);
914
+ if (publishedGuardian?.record === null || publishedGuardian === null
915
+ || !sameGuardianRecord(publishedGuardian.record, record)) {
916
+ return { result: { attached: false, reason: 'path_changed' } };
917
+ }
918
+ fileLockTestHooks.afterGuardianPublish?.(guardianPath(lockPath), lockPath);
919
+ const settledGuardian = readLockGuardianSnapshot(lockPath);
920
+ const settled = lockSnapshot(lockPath);
921
+ const guardianUnchanged = settledGuardian !== null
922
+ && sameSnapshot(publishedGuardian.snapshot, settledGuardian.snapshot)
923
+ && settledGuardian.record !== null
924
+ && sameGuardianRecord(settledGuardian.record, record);
925
+ const ownerUnchanged = settled !== null
926
+ && sameSnapshot(current, settled)
927
+ && sameOwner(settled.owner, owned.owner);
928
+ if (!guardianUnchanged || !ownerUnchanged) {
929
+ if (guardianUnchanged) {
930
+ removeUnchangedLock(guardianPath(lockPath), settledGuardian.snapshot, 'guardian-transfer-aborted');
931
+ }
932
+ if (!ownerUnchanged)
933
+ ownedLocks.delete(key);
934
+ return { result: { attached: false, reason: 'path_changed' } };
935
+ }
936
+ forgetMalformedObservation(lockPath);
937
+ return {
938
+ result: {
939
+ attached: true,
940
+ reason: 'attached',
941
+ guardian: publicOwnerRecord(target.owner),
942
+ },
943
+ guardian: record,
944
+ };
945
+ });
946
+ if (guarded.guardian !== undefined) {
947
+ owned.guardian = guarded.guardian;
948
+ }
949
+ return guarded.result;
950
+ }
951
+ /**
952
+ * Drop local ownership only after an already-published guardian has been revalidated exactly.
953
+ * No file mutation is needed: the durable guardian receipt is the crash-safe owner binding.
954
+ */
955
+ export function retainLockGuardianForProcess(lockPath, expectedOwner, expectedGuardian) {
956
+ const key = lockKey(lockPath);
957
+ const owned = ownedLocks.get(key);
958
+ if (owned === undefined)
959
+ return { retained: false, reason: 'not_owned' };
960
+ if (owned.releasePending || !samePublicOwner(expectedOwner, owned.owner)) {
961
+ return { retained: false, reason: 'ownership_changed' };
962
+ }
963
+ if (owned.guardian === undefined)
964
+ return { retained: false, reason: 'guardian_missing' };
965
+ if (!samePublicOwner(expectedGuardian, owned.guardian.guardian)) {
966
+ return { retained: false, reason: 'ownership_changed' };
967
+ }
968
+ const guardian = owned.guardian;
969
+ const guard = tryAcquireMutationGuard(lockPath);
970
+ if (guard === null)
971
+ return { retained: false, reason: 'mutation_busy' };
972
+ const result = withMutationGuardRelease(lockPath, guard, () => {
973
+ const current = readLockSnapshot(lockPath);
974
+ const receipt = readLockGuardianSnapshot(lockPath);
975
+ if (current === null || !sameOwner(current.owner, owned.owner)) {
976
+ return { retained: false, reason: 'ownership_changed' };
977
+ }
978
+ if (receipt?.record === null || receipt === null
979
+ || !sameGuardianRecord(receipt.record, guardian)) {
980
+ return { retained: false, reason: 'path_changed' };
981
+ }
982
+ return { retained: true, reason: 'retained' };
983
+ });
984
+ if (result.retained) {
985
+ ownedLocks.delete(key);
986
+ }
987
+ return result;
988
+ }
174
989
  /**
175
- * Cross-process O_EXCL file lock with crashed-owner recovery.
990
+ * Remove an exact attached guardian after the child has definitely exited, retaining the primary
991
+ * owner so rollback or another launch can continue under the same lifecycle lease.
992
+ */
993
+ export function clearLockGuardianForProcess(lockPath, expectedOwner, expectedGuardian) {
994
+ const key = lockKey(lockPath);
995
+ const owned = ownedLocks.get(key);
996
+ if (owned === undefined)
997
+ return { cleared: false, reason: 'not_owned' };
998
+ if (owned.releasePending || !samePublicOwner(expectedOwner, owned.owner)) {
999
+ return { cleared: false, reason: 'ownership_changed' };
1000
+ }
1001
+ if (owned.guardian === undefined
1002
+ || !samePublicOwner(expectedGuardian, owned.guardian.guardian)) {
1003
+ return { cleared: false, reason: 'ownership_changed' };
1004
+ }
1005
+ const guardian = owned.guardian;
1006
+ const guard = tryAcquireMutationGuard(lockPath);
1007
+ if (guard === null)
1008
+ return { cleared: false, reason: 'mutation_busy' };
1009
+ const result = withMutationGuardRelease(lockPath, guard, () => {
1010
+ const current = readLockSnapshot(lockPath);
1011
+ if (current === null || !sameOwner(current.owner, owned.owner)) {
1012
+ return { cleared: false, reason: 'ownership_changed' };
1013
+ }
1014
+ const receipt = readLockGuardianSnapshot(lockPath);
1015
+ if (receipt === null) {
1016
+ return { cleared: true, reason: 'missing' };
1017
+ }
1018
+ if (receipt.record === null || !sameGuardianRecord(receipt.record, guardian)) {
1019
+ return { cleared: false, reason: 'path_changed' };
1020
+ }
1021
+ if (!removeUnchangedLock(guardianPath(lockPath), receipt.snapshot, 'guardian-cleared')) {
1022
+ return { cleared: false, reason: 'path_changed' };
1023
+ }
1024
+ return { cleared: true, reason: 'cleared' };
1025
+ });
1026
+ if (result.cleared) {
1027
+ delete owned.guardian;
1028
+ }
1029
+ return result;
1030
+ }
1031
+ /**
1032
+ * Atomically bind an acquired lock to a still-running child and relinquish local ownership.
1033
+ */
1034
+ export function transferLockOwnershipToProcess(lockPath, expectedOwner, targetPid) {
1035
+ const attached = attachLockGuardianToProcess(lockPath, expectedOwner, targetPid);
1036
+ if (!attached.attached)
1037
+ return { transferred: false, reason: attached.reason };
1038
+ const retained = retainLockGuardianForProcess(lockPath, expectedOwner, attached.guardian);
1039
+ if (!retained.retained)
1040
+ return { transferred: false, reason: retained.reason };
1041
+ return {
1042
+ transferred: true,
1043
+ reason: 'transferred',
1044
+ guardian: attached.guardian,
1045
+ };
1046
+ }
1047
+ /**
1048
+ * Cross-process file lock with crashed-owner recovery.
176
1049
  *
177
1050
  * The lock file records the owner pid and token. If a waiter finds the lock held by a pid that is no longer
178
1051
  * alive (the owner crashed without releaseLock), it reclaims the stale lock instead of spinning
179
1052
  * 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.
1053
+ * is removed by hand. Empty or truncated locks are reclaimed only after the same inode and contents
1054
+ * remain malformed for a grace period, so a live creator can finish publishing its owner payload.
1055
+ * Acquisition publishes a fully synced temporary owner with an atomic no-replace hardlink, and stale
1056
+ * reclaim moves the old lock aside under a mutation guard, then verifies the inode snapshot before
1057
+ * deletion. A live owner's lock (including this
1058
+ * process's own) is never stolen.
183
1059
  *
184
1060
  * NOTE: still synchronous (blocks the event loop while waiting) by design — it guards short
185
1061
  * synchronous critical sections (append-only writes).
186
1062
  */
187
1063
  export function acquireLock(lockPath, opts = {}) {
1064
+ const pending = ownedLocks.get(lockKey(lockPath));
1065
+ if (pending?.releasePending) {
1066
+ const released = releaseLock(lockPath);
1067
+ if (!released.released)
1068
+ throw new LockReleaseError(released.reason);
1069
+ }
188
1070
  const maxTries = opts.maxTries ?? 300;
189
1071
  const waitMs = opts.waitMs ?? 10;
190
1072
  for (let i = 0; i < maxTries; i++) {
1073
+ const guardian = readLockGuardianSnapshot(lockPath);
1074
+ if (guardian !== null) {
1075
+ const reclaimable = guardian.record === null
1076
+ ? reclaimableSnapshot(guardianPath(lockPath), guardian.snapshot)
1077
+ : guardianReclaimable(guardian.record);
1078
+ if (!reclaimable) {
1079
+ syncSleep(waitMs);
1080
+ continue;
1081
+ }
1082
+ if (!reclaimStaleGuardian(lockPath)) {
1083
+ syncSleep(waitMs);
1084
+ continue;
1085
+ }
1086
+ }
191
1087
  const owner = newOwner();
192
1088
  if (createLockFile(lockPath, owner, true)) {
193
- return;
1089
+ return publicOwnerRecord(owner);
194
1090
  }
195
- const current = lockOwner(lockPath);
196
- if (current !== null && current.pid !== process.pid && !pidAlive(current.pid)) {
1091
+ const current = lockSnapshot(lockPath);
1092
+ if (current !== null && reclaimableSnapshot(lockPath, current)) {
197
1093
  if (!reclaimStaleLock(lockPath))
198
1094
  syncSleep(waitMs);
199
1095
  continue;
200
1096
  }
1097
+ if (current === null && pathExists(mutationGuardPath(lockPath))) {
1098
+ const guard = tryAcquireMutationGuard(lockPath);
1099
+ if (guard !== null)
1100
+ releaseMutationGuard(lockPath, guard);
1101
+ }
201
1102
  syncSleep(waitMs);
202
1103
  }
203
- throw new LockTimeoutError(lockPath);
1104
+ throw new LockTimeoutError();
204
1105
  }
205
1106
  export function releaseLock(lockPath) {
206
1107
  const key = lockKey(lockPath);
207
- const owner = ownedLocks.get(key);
208
- if (owner === undefined)
209
- return;
1108
+ const owned = ownedLocks.get(key);
1109
+ if (owned === undefined)
1110
+ return { released: true, reason: 'not_owned' };
1111
+ const failed = (reason) => {
1112
+ owned.releasePending = true;
1113
+ return { released: false, reason };
1114
+ };
1115
+ let guard;
210
1116
  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;
221
- }
222
- removeFileIfExists(releasedPath);
1117
+ guard = tryAcquireMutationGuard(lockPath);
223
1118
  }
224
- finally {
1119
+ catch (error) {
1120
+ return failed(error instanceof UnsafeLockPathError ? error.reason : 'release_failed');
1121
+ }
1122
+ if (guard === null)
1123
+ return failed('release_failed');
1124
+ let cleanupFailed = false;
1125
+ let forgetOwnership = false;
1126
+ let result;
1127
+ try {
1128
+ result = withMutationGuardRelease(lockPath, guard, () => {
1129
+ // Release must use exact snapshots. Inconclusive reads leave a pending release instead of
1130
+ // silently unlocking a live guarded child.
1131
+ const current = readLockSnapshot(lockPath);
1132
+ if (current === null) {
1133
+ if (owned.guardian !== undefined)
1134
+ return failed('ownership_changed');
1135
+ forgetOwnership = true;
1136
+ return { released: true, reason: 'missing' };
1137
+ }
1138
+ if (current.owner === null)
1139
+ return failed('invalid_owner');
1140
+ if (!sameOwner(current.owner, owned.owner)) {
1141
+ forgetOwnership = true;
1142
+ return { released: true, reason: 'ownership_changed' };
1143
+ }
1144
+ const receipt = readLockGuardianSnapshot(lockPath);
1145
+ if (owned.guardian !== undefined) {
1146
+ if (receipt !== null) {
1147
+ if (receipt.record === null || !sameGuardianRecord(receipt.record, owned.guardian)) {
1148
+ return failed('path_changed');
1149
+ }
1150
+ if (!removeUnchangedLock(guardianPath(lockPath), receipt.snapshot, 'guardian-released', () => { cleanupFailed = true; })) {
1151
+ return failed('path_changed');
1152
+ }
1153
+ }
1154
+ }
1155
+ else if (receipt !== null) {
1156
+ return failed('path_changed');
1157
+ }
1158
+ if (!removeUnchangedLock(lockPath, current, 'released', () => { cleanupFailed = true; })) {
1159
+ return failed('path_changed');
1160
+ }
1161
+ forgetOwnership = true;
1162
+ return { released: true, reason: 'released' };
1163
+ }, () => { cleanupFailed = true; });
1164
+ }
1165
+ catch (error) {
1166
+ return failed(error instanceof UnsafeLockPathError ? error.reason : 'release_failed');
1167
+ }
1168
+ if (result.released && forgetOwnership)
225
1169
  ownedLocks.delete(key);
1170
+ return result.released && cleanupFailed
1171
+ ? { released: true, reason: 'released_with_cleanup_error' }
1172
+ : result;
1173
+ }
1174
+ export class LockReleaseError extends Error {
1175
+ reason;
1176
+ code = 'LOCK_RELEASE_FAILED';
1177
+ constructor(reason) {
1178
+ super(`文件锁释放未完成: ${reason}`);
1179
+ this.reason = reason;
1180
+ this.name = 'LockReleaseError';
226
1181
  }
227
1182
  }