@agentxm/workspace-transactions 0.29.0 → 0.29.2

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.
@@ -60,17 +60,47 @@ const errorCode = (cause) => typeof cause === "object" && cause !== null && "cod
60
60
  export const transitionLockPath = (path, workspaceDir) => path.join(workspaceDir, "tmp", TRANSITION_LOCK_FILENAME);
61
61
  /**
62
62
  * The filesystem handed to `proper-lockfile`. Identical to the platform fs
63
- * except that removing a transition-lock directory first clears the tool's
64
- * own holder metadata inside it. The library reclaims a stale lock and
65
- * releases a held one with a non-recursive `rmdir`; without this, a lock
66
- * directory left by a crashed process — which always still contains
67
- * `holder.json` could never be reclaimed, and every later mutation would
68
- * wait out its bound and report a false contention. Removal stays
69
- * non-recursive past that one owned name, so foreign content keeps blocking
70
- * removal the way it always did.
63
+ * except for the transition-lock directory itself:
64
+ *
65
+ * - acquisition creates an empty holder file before `proper-lockfile` probes
66
+ * and records the directory mtime;
67
+ * - removal clears that owned holder file before the library's non-recursive
68
+ * `rmdir`.
69
+ *
70
+ * Recording holder metadata therefore overwrites an existing file instead of
71
+ * adding a directory entry, so it cannot change the lock directory mtime and
72
+ * compromise the first heartbeat. The empty placeholder is also safe during
73
+ * the narrow acquisition window: it is unreadable as holder metadata, so an
74
+ * old finalizer can never mistake a successor's acquisition for its own.
75
+ * Stale reclamation can remove crashed holders, while foreign content still
76
+ * blocks removal as it always did.
71
77
  */
72
78
  const lockDirectoryFs = {
73
79
  ...nodeFs,
80
+ mkdir: (target, callback) => {
81
+ if (typeof target !== "string" || !target.endsWith(TRANSITION_LOCK_FILENAME)) {
82
+ nodeFs.mkdir(target, callback);
83
+ return;
84
+ }
85
+ nodeFs.mkdir(target, (directoryError) => {
86
+ if (directoryError !== null) {
87
+ callback(directoryError);
88
+ return;
89
+ }
90
+ const holderPath = `${target}/holder.json`;
91
+ nodeFs.writeFile(holderPath, "", { flag: "wx" }, (holderError) => {
92
+ if (holderError === null) {
93
+ callback(null);
94
+ return;
95
+ }
96
+ // Surface the original placeholder failure after best-effort cleanup;
97
+ // leaving a half-created directory would turn it into false contention.
98
+ nodeFs.unlink(holderPath, () => {
99
+ nodeFs.rmdir(target, () => callback(holderError));
100
+ });
101
+ });
102
+ });
103
+ },
74
104
  rmdir: (target, callback) => {
75
105
  if (typeof target === "string" && target.endsWith(TRANSITION_LOCK_FILENAME)) {
76
106
  nodeFs.unlink(`${target}/holder.json`, () => {
@@ -202,25 +232,9 @@ const acquireWorkspaceTransitionLock = (registry, args) => Effect.gen(function*
202
232
  }
203
233
  const release = granted.success;
204
234
  const token = randomBytes(16).toString("hex");
205
- const holderStamped = yield* Effect.gen(function* () {
206
- const lockInfo = yield* fs
207
- .stat(lockPath)
208
- .pipe(Effect.mapError((cause) => new TransitionLockError({ path: lockPath, step: "inspect-timestamp", cause })));
209
- const lockMtime = yield* Option.match(lockInfo.mtime, {
210
- onNone: () => Effect.fail(new TransitionLockError({ path: lockPath, step: "missing-timestamp" })),
211
- onSome: Effect.succeed,
212
- });
213
- yield* writeHolder(fs, path, lockPath, {
214
- ...args.holder,
215
- token,
216
- });
217
- // proper-lockfile proves ownership by comparing the directory's
218
- // mtime with the timestamp it recorded at acquisition. Writing
219
- // holder.json changes that directory mtime, so restore the exact
220
- // acquired value before the first refresh observes it.
221
- yield* fs
222
- .utimes(lockPath, lockMtime, lockMtime)
223
- .pipe(Effect.mapError((cause) => new TransitionLockError({ path: lockPath, step: "preserve-timestamp", cause })));
235
+ const holderStamped = yield* writeHolder(fs, path, lockPath, {
236
+ ...args.holder,
237
+ token,
224
238
  }).pipe(Effect.result);
225
239
  if (holderStamped._tag === "Failure") {
226
240
  // The holder metadata is the only ownership evidence this
package/package.json CHANGED
@@ -4,17 +4,17 @@
4
4
  "url": "https://github.com/agentxm/axm/issues"
5
5
  },
6
6
  "dependencies": {
7
- "effect": "4.0.0-rc.112",
7
+ "effect": "4.0.0-rc.115",
8
8
  "proper-lockfile": "^4.1.2"
9
9
  },
10
10
  "description": "AXM workspace transactions: the process transition lock, per-closure snapshot ledger, restoration, atomic single-file publication, and footprint observation for the axm CLI. Unstable and unsupported — use the axm.sh CLI.",
11
11
  "devDependencies": {
12
- "@effect/platform-node": "4.0.0-rc.112",
13
- "@effect/vitest": "4.0.0-rc.112",
12
+ "@effect/platform-node": "4.0.0-rc.115",
13
+ "@effect/vitest": "4.0.0-rc.115",
14
14
  "@types/proper-lockfile": "^4.1.4",
15
15
  "@typescript/native": "npm:typescript@^7.0.2",
16
16
  "typescript": "npm:@typescript/typescript6@^6.0.2",
17
- "vitest": "^4.1.10"
17
+ "vitest": "^5.0.0"
18
18
  },
19
19
  "engines": {
20
20
  "node": ">=22.19.0"
@@ -51,5 +51,5 @@
51
51
  },
52
52
  "sideEffects": false,
53
53
  "type": "module",
54
- "version": "0.29.0"
54
+ "version": "0.29.2"
55
55
  }