@atolis-hq/wake 0.4.49 → 0.4.50

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.
@@ -1,3 +1,4 @@
1
+ import { acquireFileLock } from '@atolis-hq/eventing-filesystem';
1
2
  import { createPullRequestService } from '../activities/index.js';
2
3
  import { FileArtifactMcpSessionStore, FileArtifactStore, createArtifactService, } from '../artifacts/index.js';
3
4
  import { DispatchPolicy, createActivationScheduler, createActivationSchedulerSubscriber, createControlPlaneService, createDurableRunnerIneligibility, createRunnerControlService, } from '../control-plane/index.js';
@@ -63,6 +64,11 @@ export async function createCompositionRoot(wakeRoot, options = {}) {
63
64
  throw new Error('Workspace resource ' + id + ' does not identify a GitHub repository');
64
65
  return 'https://github.com/' + match[1] + '.git';
65
66
  },
67
+ }, {
68
+ tryAcquire: (path) => acquireFileLock(path, {
69
+ staleAfterMs: 0,
70
+ staleRequiresDeadProcess: true,
71
+ }),
66
72
  }, undefined, undefined, config.execution.workspaceHooks?.prepare);
67
73
  const transcriptStore = config.transcripts.enabled
68
74
  ? (options.transcriptStore ?? new TranscriptStore(paths.transcriptsRoot))
@@ -124,6 +124,6 @@ export function resolveWakeVersion(options = {}) {
124
124
  return `g${headHash.slice(0, 7)}`;
125
125
  return '0.1.0-dev';
126
126
  }
127
- export const wakeVersion = "v0.4.49";
127
+ export const wakeVersion = "v0.4.50";
128
128
  /** The registry-installable version used by packaged sandbox images. */
129
129
  export const wakePackageVersion = packageVersion(defaultRepoRoot) ?? wakeVersion;
@@ -7,12 +7,26 @@ export async function acquireWorkspace(runId, mode, workItemId, resources, provi
7
7
  if (provider === undefined)
8
8
  throw new Error('Workspace provider is required');
9
9
  const repositoryResource = resources.find((resource) => resource.kind === BuiltInResourceKind.Repository) ??
10
- resources.find((resource) => resource.kind === BuiltInResourceKind.Issue ||
11
- resource.kind === BuiltInResourceKind.PullRequest);
10
+ resources.find((resource) => resource.kind === BuiltInResourceKind.PullRequest) ??
11
+ resources.find((resource) => resource.kind === BuiltInResourceKind.Issue);
12
12
  if (repositoryResource === undefined)
13
13
  throw new Error('Repository Resource is required');
14
14
  signal.throwIfAborted();
15
- return provider.acquire({ runId, signal, mode, workItemId, repositoryResource });
15
+ const revision = repositoryResource.revision;
16
+ const checkoutRevision = repositoryResource.kind === BuiltInResourceKind.Repository ||
17
+ (repositoryResource.kind === BuiltInResourceKind.PullRequest &&
18
+ revision !== undefined &&
19
+ /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/i.test(revision))
20
+ ? revision
21
+ : undefined;
22
+ return provider.acquire({
23
+ runId,
24
+ signal,
25
+ mode,
26
+ workItemId,
27
+ repositoryResource,
28
+ ...(checkoutRevision === undefined ? {} : { checkoutRevision }),
29
+ });
16
30
  }
17
31
  export function validateResourceRequirements(requirements, resources) {
18
32
  for (const requirement of requirements) {
@@ -9,6 +9,8 @@ export interface WorkspaceRequest {
9
9
  readonly mode: typeof WorkspaceMode.ReadOnly | typeof WorkspaceMode.Branch;
10
10
  readonly workItemId: WorkItemId;
11
11
  readonly repositoryResource: ResourceView;
12
+ /** Git checkout target, independent of the resource observation revision. */
13
+ readonly checkoutRevision?: string;
12
14
  }
13
15
  export interface WorkspaceLease {
14
16
  readonly workspaceId: string;
@@ -16,7 +18,7 @@ export interface WorkspaceLease {
16
18
  /** Branch actually checked out for this lease, when the provider can attest it. */
17
19
  readonly branch?: string | undefined;
18
20
  readonly mode: WorkspaceRequest['mode'];
19
- /** Ends this Run's use of the workspace; branch-workspace disposal is WorkItem-scoped recovery. */
21
+ /** Ends this Run's use of the workspace; workspace disposal is WorkItem-scoped recovery. */
20
22
  release(): Promise<void>;
21
23
  }
22
24
  export interface WorkspaceProvider {
@@ -46,3 +48,11 @@ export interface WorkspaceRecoveryFailure {
46
48
  readonly path: string;
47
49
  readonly message: string;
48
50
  }
51
+ /** Process-owned exclusion shared by acquisition and recovery. */
52
+ export interface WorkspaceLockProvider {
53
+ tryAcquire(path: string): Promise<WorkspaceLock>;
54
+ }
55
+ export interface WorkspaceLock {
56
+ readonly acquired: boolean;
57
+ release(): Promise<void>;
58
+ }
@@ -1,11 +1,10 @@
1
- import type { WorkItemId } from '../../../work/index.js';
2
1
  import type { RunView } from '../../contracts/views.js';
3
- import type { WorkspaceProvider, WorkspaceRecovery, WorkspaceRecoveryOptions, WorkspaceRecoveryResult, WorkspaceRequest } from '../../contracts/workspace.js';
2
+ import type { WorkspaceLockProvider, WorkspaceProvider, WorkspaceRecovery, WorkspaceRecoveryOptions, WorkspaceRecoveryResult, WorkspaceRequest } from '../../contracts/workspace.js';
4
3
  import { type WorkspacePrepareHook } from './prepare-workspace.js';
5
4
  export interface RepositoryCloneResolver {
6
5
  cloneLocator(resourceId: string, signal: AbortSignal): Promise<string>;
7
6
  }
8
- export type GitRunner = (arguments_: readonly string[], signal: AbortSignal) => Promise<void>;
7
+ export type GitRunner = (arguments_: readonly string[], signal: AbortSignal) => Promise<string | void>;
9
8
  export interface WorkspaceRecoveryFileSystem {
10
9
  remove(path: string): Promise<void>;
11
10
  canonicalize(path: string): Promise<string>;
@@ -13,17 +12,23 @@ export interface WorkspaceRecoveryFileSystem {
13
12
  export declare class GitWorkspaceProvider implements WorkspaceProvider, WorkspaceRecovery {
14
13
  private readonly root;
15
14
  private readonly resolver;
15
+ private readonly locks;
16
16
  private readonly git;
17
17
  private readonly prepareHook?;
18
18
  private readonly markerRoot;
19
19
  private readonly recoveryFileSystem;
20
- constructor(root: string, resolver: RepositoryCloneResolver, git?: GitRunner, recoveryFileSystem?: WorkspaceRecoveryFileSystem, prepareHook?: WorkspacePrepareHook);
20
+ private readonly pendingReleases;
21
+ constructor(root: string, resolver: RepositoryCloneResolver, locks: WorkspaceLockProvider, git?: GitRunner, recoveryFileSystem?: WorkspaceRecoveryFileSystem, prepareHook?: WorkspacePrepareHook);
21
22
  acquire(request: WorkspaceRequest): Promise<{
22
23
  release: () => Promise<void>;
23
- branch?: WorkItemId;
24
+ branch?: string;
24
25
  workspaceId: string;
25
26
  path: string;
26
27
  mode: "read-only" | "branch";
27
28
  }>;
29
+ private releaseWorkspaceLock;
30
+ private prepareCheckout;
31
+ private reuseCheckout;
32
+ private validateLegacyCheckout;
28
33
  recover(runs: readonly RunView[], options?: WorkspaceRecoveryOptions): Promise<WorkspaceRecoveryResult>;
29
34
  }
@@ -1,6 +1,6 @@
1
1
  /* eslint-disable max-lines */
2
2
  import { execFile } from 'node:child_process';
3
- import { access, mkdir, readdir, readFile, realpath, rm, writeFile } from 'node:fs/promises';
3
+ import { access, mkdir, readdir, readFile, realpath, rename, rm, rmdir, unlink, writeFile, } from 'node:fs/promises';
4
4
  import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
5
5
  import { promisify } from 'node:util';
6
6
  import { isActiveRunStatus, RunStatus, WorkspaceMode } from '../../contracts/vocabulary.js';
@@ -9,18 +9,21 @@ const exec = promisify(execFile);
9
9
  export class GitWorkspaceProvider {
10
10
  root;
11
11
  resolver;
12
+ locks;
12
13
  git;
13
14
  prepareHook;
14
15
  markerRoot;
15
16
  recoveryFileSystem;
16
- constructor(root, resolver, git = async (arguments_, signal) => {
17
- await exec('git', arguments_, { signal });
17
+ pendingReleases = new Map();
18
+ constructor(root, resolver, locks, git = async (arguments_, signal) => {
19
+ return (await exec('git', arguments_, { signal })).stdout.trim();
18
20
  }, recoveryFileSystem = {
19
21
  remove: async (path) => rm(path, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 }),
20
22
  canonicalize: realpath,
21
23
  }, prepareHook) {
22
24
  this.root = root;
23
25
  this.resolver = resolver;
26
+ this.locks = locks;
24
27
  this.git = git;
25
28
  this.prepareHook = prepareHook;
26
29
  this.markerRoot = join(this.root, '.wake-workspace-ownership');
@@ -34,59 +37,161 @@ export class GitWorkspaceProvider {
34
37
  const name = `${request.workItemId}-${request.mode}-${slug(locator)}`;
35
38
  const path = resolve(this.root, name);
36
39
  const markerPath = join(this.markerRoot, `${name}.json`);
40
+ const lockPath = join(this.markerRoot, `${name}.lease`);
37
41
  await mkdir(dirname(markerPath), { recursive: true });
42
+ const scope = await recoveryScope(this.root, this.markerRoot);
43
+ if (scope === null)
44
+ throw new Error('Workspace ownership directory is outside the managed root');
45
+ await validateLeasePaths(scope, lockPath);
46
+ const lock = await acquireWorkspaceLock(this.locks, lockPath, signal);
47
+ try {
48
+ const branch = await this.prepareCheckout(request, locator, name, path, markerPath);
49
+ if (this.prepareHook !== undefined)
50
+ await prepareWorkspace(path, this.prepareHook, signal);
51
+ signal.throwIfAborted();
52
+ let released = false;
53
+ return {
54
+ workspaceId: name,
55
+ path,
56
+ mode: request.mode,
57
+ ...(branch === undefined ? {} : { branch }),
58
+ release: async () => {
59
+ if (released)
60
+ return;
61
+ await this.releaseWorkspaceLock(lockPath, lock);
62
+ released = true;
63
+ },
64
+ };
65
+ }
66
+ catch (error) {
67
+ await this.releaseWorkspaceLock(lockPath, lock);
68
+ throw error;
69
+ }
70
+ }
71
+ async releaseWorkspaceLock(path, lock) {
72
+ try {
73
+ await lock.release();
74
+ if (this.pendingReleases.get(path) === lock)
75
+ this.pendingReleases.delete(path);
76
+ }
77
+ catch (error) {
78
+ this.pendingReleases.set(path, lock);
79
+ throw error;
80
+ }
81
+ }
82
+ async prepareCheckout(request, locator, name, path, markerPath) {
83
+ const { signal } = request;
84
+ const previousMarker = await readMarker(markerPath);
85
+ if (await exists(join(this.markerRoot, `${name}.lock`), signal))
86
+ throw new Error('Legacy workspace lock requires recovery before acquisition');
87
+ let initialRevision = previousMarker?.initialRevision;
88
+ let checkoutReady;
38
89
  signal.throwIfAborted();
39
- await writeFile(markerPath, JSON.stringify({
90
+ const writeMarker = () => writeOwnershipMarker(markerPath, {
40
91
  runId: request.runId,
41
92
  workItemId: request.workItemId,
42
93
  repositoryResourceId: request.repositoryResource.resourceId,
43
94
  mode: request.mode,
44
95
  workspaceId: name,
45
96
  path,
46
- }), { encoding: 'utf8', signal });
47
- const existingWorkspace = await exists(join(path, '.git'), signal);
97
+ checkoutReady,
98
+ ...(initialRevision === undefined ? {} : { initialRevision }),
99
+ }, signal);
100
+ const existingWorkspace = await this.reuseCheckout(previousMarker, markerPath, path, request.mode, signal);
101
+ checkoutReady = previousMarker?.checkoutReady ?? existingWorkspace;
102
+ if (!existingWorkspace) {
103
+ initialRevision = undefined;
104
+ checkoutReady = false;
105
+ }
106
+ await writeMarker();
48
107
  if (!existingWorkspace) {
49
108
  await mkdir(dirname(path), { recursive: true });
50
109
  signal.throwIfAborted();
51
110
  await this.git(['clone', locator, path], signal);
52
111
  }
112
+ if (initialRevision === undefined)
113
+ initialRevision = await readCheckoutRevision(path, this.git, signal);
53
114
  const branch = request.mode === WorkspaceMode.Branch ? request.workItemId : undefined;
54
- if (branch !== undefined)
115
+ if (branch !== undefined) {
55
116
  await this.git(['-C', path, 'switch', ...(existingWorkspace ? [] : ['--create']), branch], signal);
56
- if (this.prepareHook !== undefined)
57
- await prepareWorkspace(path, this.prepareHook, signal);
58
- signal.throwIfAborted();
59
- return {
60
- workspaceId: name,
61
- path,
62
- mode: request.mode,
63
- ...(branch === undefined ? {} : { branch }),
64
- release: async () => {
65
- if (request.mode !== WorkspaceMode.ReadOnly)
66
- return;
67
- await rm(path, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 });
68
- await rm(markerPath, { force: true });
69
- },
70
- };
117
+ }
118
+ checkoutReady = true;
119
+ await writeMarker();
120
+ if (branch === undefined) {
121
+ await restoreReadOnlyWorkspace(path, request.checkoutRevision, initialRevision, this.git, signal);
122
+ }
123
+ return branch;
124
+ }
125
+ async reuseCheckout(marker, markerPath, path, mode, signal) {
126
+ if (!(await exists(path, signal)))
127
+ return false;
128
+ const scope = await recoveryScope(this.root, this.markerRoot);
129
+ if (scope === null ||
130
+ marker === null ||
131
+ !(await isOwnedWorkspace(scope, markerPath, marker, this.recoveryFileSystem)) ||
132
+ !(await canRemoveOwnedWorkspace(scope, path)))
133
+ throw new Error('Cannot acquire an unvalidated existing workspace');
134
+ if (marker.checkoutReady === false) {
135
+ await this.recoveryFileSystem.remove(path);
136
+ return false;
137
+ }
138
+ if (!(await exists(join(path, '.git'), signal))) {
139
+ if (mode === WorkspaceMode.Branch)
140
+ throw new Error('Cannot reuse a branch checkout without Git metadata');
141
+ await this.recoveryFileSystem.remove(path);
142
+ return false;
143
+ }
144
+ return this.validateLegacyCheckout(marker, path, mode, signal);
145
+ }
146
+ async validateLegacyCheckout(marker, path, mode, signal) {
147
+ if (marker.checkoutReady !== undefined || mode !== WorkspaceMode.ReadOnly)
148
+ return true;
149
+ try {
150
+ await readCheckoutRevision(path, this.git, signal);
151
+ return true;
152
+ }
153
+ catch {
154
+ signal.throwIfAborted();
155
+ // Legacy read-only clones may predate the durable completion flag.
156
+ await this.recoveryFileSystem.remove(path);
157
+ return false;
158
+ }
71
159
  }
72
160
  async recover(runs, options = {}) {
73
161
  const scope = await recoveryScope(this.root, this.markerRoot);
74
162
  if (scope === null)
75
163
  return emptyRecovery();
76
- const markers = await recoveryMarkerNames(this.markerRoot);
164
+ const releaseFailures = [];
165
+ for (const [path, lock] of this.pendingReleases) {
166
+ if (await options.isPaused?.())
167
+ break;
168
+ try {
169
+ await this.releaseWorkspaceLock(path, lock);
170
+ }
171
+ catch (error) {
172
+ releaseFailures.push({ markerPath: path, path, message: errorMessage(error) });
173
+ }
174
+ }
175
+ const names = await recoveryMarkerNames(this.markerRoot);
176
+ const workspaceIds = new Set(names.flatMap((name) => {
177
+ const match = /^(.*)\.(?:json(?:\.pending)?|lock|lease(?:\.owners)?)$/.exec(name);
178
+ return match !== null && isSafeWorkspaceId(match[1]) ? [match[1]] : [];
179
+ }));
77
180
  let reclaimed = 0;
78
181
  const reclaimedWorkItemIds = [];
79
- const failures = [];
80
- for (const filename of markers) {
182
+ const failures = [...releaseFailures];
183
+ for (const workspaceId of workspaceIds) {
81
184
  if (await options.isPaused?.())
82
185
  break;
83
- const result = await recoverMarker({
84
- filename,
186
+ const result = await recoverWorkspace({
187
+ workspaceId,
85
188
  markerRoot: this.markerRoot,
86
189
  scope,
87
190
  runs,
88
191
  fileSystem: this.recoveryFileSystem,
89
192
  options,
193
+ locks: this.locks,
194
+ releaseLock: (path, lock) => this.releaseWorkspaceLock(path, lock),
90
195
  });
91
196
  reclaimed += result.reclaimed;
92
197
  reclaimedWorkItemIds.push(...(result.reclaimedWorkItemIds ?? []));
@@ -113,13 +218,40 @@ async function recoveryMarkerNames(markerRoot) {
113
218
  return [];
114
219
  }
115
220
  }
116
- async function recoverMarker(input) {
117
- if (!input.filename.endsWith('.json'))
118
- return emptyRecovery();
119
- const markerPath = join(input.markerRoot, input.filename);
120
- const marker = await readMarker(markerPath);
221
+ async function validateLeasePaths(scope, path) {
222
+ for (const target of [
223
+ path,
224
+ `${path}.owners`,
225
+ `${path.slice(0, -'.lease'.length)}.lock`,
226
+ `${path.slice(0, -'.lease'.length)}.json.pending`,
227
+ ]) {
228
+ const canonical = await canonicalPath(target);
229
+ if (canonical !== null && canonical !== join(scope.markerRoot, basename(target)))
230
+ throw new Error('Workspace lease artifacts must not be symbolic links');
231
+ }
232
+ }
233
+ async function recoverWorkspace(input) {
234
+ const markerPath = join(input.markerRoot, `${input.workspaceId}.json`);
235
+ let marker = null;
121
236
  try {
122
- return await reclaimOwnedMarker(input, markerPath, marker);
237
+ const lockPath = join(input.markerRoot, `${input.workspaceId}.lease`);
238
+ await validateLeasePaths(input.scope, lockPath);
239
+ const lock = await input.locks.tryAcquire(lockPath);
240
+ if (!lock.acquired)
241
+ return emptyRecovery();
242
+ try {
243
+ if (!(await recoverLegacyLock(join(input.markerRoot, `${input.workspaceId}.lock`), input.runs)))
244
+ return emptyRecovery();
245
+ await rm(`${markerPath}.pending`, { force: true });
246
+ marker = await readMarker(markerPath);
247
+ if (marker === null ||
248
+ !(await isOwnedWorkspace(input.scope, markerPath, marker, input.fileSystem)))
249
+ return emptyRecovery();
250
+ return await reclaimUnlockedMarker(input, markerPath, marker);
251
+ }
252
+ finally {
253
+ await input.releaseLock(lockPath, lock);
254
+ }
123
255
  }
124
256
  catch (error) {
125
257
  return {
@@ -128,10 +260,35 @@ async function recoverMarker(input) {
128
260
  };
129
261
  }
130
262
  }
131
- async function reclaimOwnedMarker(input, markerPath, marker) {
132
- if (marker === null ||
133
- !(await isOwnedWorkspace(input.scope, markerPath, marker, input.fileSystem)))
134
- return emptyRecovery();
263
+ // Migration only: new acquisitions never create directory locks.
264
+ async function recoverLegacyLock(path, runs) {
265
+ if ((await canonicalPath(path)) === null)
266
+ return true;
267
+ const owner = await readLockOwner(path);
268
+ if (owner === undefined)
269
+ return false;
270
+ if (owner === null && runs.some(protectsWorkspace))
271
+ return false;
272
+ if (owner !== null && runs.some((run) => run.runId === owner && protectsWorkspace(run)))
273
+ return false;
274
+ if (owner !== null)
275
+ await unlink(join(path, owner));
276
+ try {
277
+ await rmdir(path);
278
+ return true;
279
+ }
280
+ catch (error) {
281
+ if (isNotFound(error))
282
+ return true;
283
+ if (isNotEmpty(error))
284
+ return false;
285
+ throw error;
286
+ }
287
+ }
288
+ function protectsWorkspace(run) {
289
+ return isActiveRunStatus(run.status) || run.status === RunStatus.Ambiguous;
290
+ }
291
+ async function reclaimUnlockedMarker(input, markerPath, marker) {
135
292
  if (input.runs.some((run) => (isActiveRunStatus(run.status) || run.status === RunStatus.Ambiguous) &&
136
293
  (run.runId === marker.runId || run.workspace?.path === marker.path)))
137
294
  return emptyRecovery();
@@ -143,6 +300,7 @@ async function reclaimOwnedMarker(input, markerPath, marker) {
143
300
  await input.fileSystem.remove(marker.path);
144
301
  if ((await input.options.onWorkspaceReclaimed?.(marker.workItemId)) === false)
145
302
  return emptyRecovery();
303
+ await rm(`${markerPath}.pending`, { force: true });
146
304
  await rm(markerPath, { force: true });
147
305
  return {
148
306
  reclaimed: 1,
@@ -182,7 +340,12 @@ function isOwnershipMarker(value) {
182
340
  typeof marker.repositoryResourceId === 'string' &&
183
341
  (marker.mode === WorkspaceMode.ReadOnly || marker.mode === WorkspaceMode.Branch) &&
184
342
  typeof marker.workspaceId === 'string' &&
185
- typeof marker.path === 'string');
343
+ typeof marker.path === 'string' &&
344
+ hasValidCheckoutState(marker));
345
+ }
346
+ function hasValidCheckoutState(marker) {
347
+ return ((marker.initialRevision === undefined || typeof marker.initialRevision === 'string') &&
348
+ (marker.checkoutReady === undefined || typeof marker.checkoutReady === 'boolean'));
186
349
  }
187
350
  async function isOwnedWorkspace(scope, markerPath, marker, fileSystem) {
188
351
  if (!isSafeWorkspaceId(marker.workspaceId))
@@ -248,6 +411,75 @@ async function exists(path, signal) {
248
411
  return false;
249
412
  }
250
413
  }
414
+ async function restoreReadOnlyWorkspace(path, revision, initialRevision, git, signal) {
415
+ if (revision !== undefined) {
416
+ await git(['-C', path, 'fetch', 'origin', revision], signal);
417
+ const fetched = await readGitRevision(path, 'FETCH_HEAD^{commit}', git, signal);
418
+ await git(['-C', path, 'checkout', '--detach', '--force', fetched], signal);
419
+ await git(['-C', path, 'reset', '--hard', fetched], signal);
420
+ return;
421
+ }
422
+ await git(['-C', path, 'reset', '--hard', initialRevision], signal);
423
+ }
424
+ async function readCheckoutRevision(path, git, signal) {
425
+ return readGitRevision(path, 'HEAD^{commit}', git, signal);
426
+ }
427
+ async function readGitRevision(path, reference, git, signal) {
428
+ const revision = await git(['-C', path, 'rev-parse', '--verify', reference], signal);
429
+ if (typeof revision !== 'string' || revision.trim() === '')
430
+ throw new Error('Git runner did not attest a valid workspace HEAD');
431
+ return revision.trim();
432
+ }
433
+ async function writeOwnershipMarker(path, marker, signal) {
434
+ const pending = `${path}.pending`;
435
+ try {
436
+ await writeFile(pending, JSON.stringify(marker), { encoding: 'utf8', signal });
437
+ signal.throwIfAborted();
438
+ await rename(pending, path);
439
+ }
440
+ finally {
441
+ await rm(pending, { force: true });
442
+ }
443
+ }
444
+ async function acquireWorkspaceLock(locks, path, signal) {
445
+ while (true) {
446
+ signal.throwIfAborted();
447
+ const lock = await locks.tryAcquire(path);
448
+ if (lock.acquired)
449
+ return lock;
450
+ await waitForLock(signal);
451
+ }
452
+ }
453
+ async function readLockOwner(path) {
454
+ try {
455
+ const owners = await readdir(path);
456
+ if (owners.length === 0)
457
+ return null;
458
+ return owners.length === 1 ? owners[0] : undefined;
459
+ }
460
+ catch (error) {
461
+ if (isNotFound(error))
462
+ return null;
463
+ throw error;
464
+ }
465
+ }
466
+ function isNotEmpty(error) {
467
+ return (typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOTEMPTY');
468
+ }
469
+ function waitForLock(signal) {
470
+ return new Promise((resolve, reject) => {
471
+ const timer = setTimeout(done, 25);
472
+ const abort = () => {
473
+ clearTimeout(timer);
474
+ reject(signal.reason);
475
+ };
476
+ function done() {
477
+ signal.removeEventListener('abort', abort);
478
+ resolve();
479
+ }
480
+ signal.addEventListener('abort', abort, { once: true });
481
+ });
482
+ }
251
483
  function slug(value) {
252
484
  return value
253
485
  .toLowerCase()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atolis-hq/wake",
3
- "version": "0.4.49",
3
+ "version": "0.4.50",
4
4
  "description": "Local autonomous agent control plane for software development",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {