@agentxm/workspace-operations 0.28.4-bootstrap.0

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 (45) hide show
  1. package/LICENSE +110 -0
  2. package/README.md +12 -0
  3. package/dist/src/index.d.ts +31 -0
  4. package/dist/src/index.js +42 -0
  5. package/dist/src/live.d.ts +13 -0
  6. package/dist/src/live.js +12 -0
  7. package/dist/src/operations/augment-plan.d.ts +25 -0
  8. package/dist/src/operations/augment-plan.js +51 -0
  9. package/dist/src/operations/load-workspace.d.ts +43 -0
  10. package/dist/src/operations/load-workspace.js +77 -0
  11. package/dist/src/operations/scan-plan-readiness.d.ts +22 -0
  12. package/dist/src/operations/scan-plan-readiness.js +41 -0
  13. package/dist/src/operations/transaction.d.ts +58 -0
  14. package/dist/src/operations/transaction.js +359 -0
  15. package/dist/src/operations/transition-lock.d.ts +66 -0
  16. package/dist/src/operations/transition-lock.js +291 -0
  17. package/dist/src/plan/apply-plan.d.ts +45 -0
  18. package/dist/src/plan/apply-plan.js +238 -0
  19. package/dist/src/plan/errors.d.ts +85 -0
  20. package/dist/src/plan/errors.js +101 -0
  21. package/dist/src/plan/execution-candidate.d.ts +20 -0
  22. package/dist/src/plan/execution-candidate.js +95 -0
  23. package/dist/src/plan/interruption-signal.d.ts +18 -0
  24. package/dist/src/plan/interruption-signal.js +13 -0
  25. package/dist/src/plan/job-step-message.d.ts +7 -0
  26. package/dist/src/plan/job-step-message.js +7 -0
  27. package/dist/src/plan/operation-events.d.ts +62 -0
  28. package/dist/src/plan/operation-events.js +41 -0
  29. package/dist/src/plan/operation-journal.d.ts +69 -0
  30. package/dist/src/plan/operation-journal.js +52 -0
  31. package/dist/src/plan/operation-resolution.d.ts +219 -0
  32. package/dist/src/plan/operation-resolution.js +324 -0
  33. package/dist/src/plan/plan-execution.d.ts +75 -0
  34. package/dist/src/plan/plan-execution.js +117 -0
  35. package/dist/src/plan/plan.d.ts +248 -0
  36. package/dist/src/plan/plan.js +95 -0
  37. package/dist/src/plan/resolve-plan-interaction.d.ts +79 -0
  38. package/dist/src/plan/resolve-plan-interaction.js +52 -0
  39. package/dist/src/plan/resolve-plan.d.ts +42 -0
  40. package/dist/src/plan/resolve-plan.js +694 -0
  41. package/dist/src/plan/step-failure-conversions.d.ts +33 -0
  42. package/dist/src/plan/step-failure-conversions.js +177 -0
  43. package/dist/src/testing.d.ts +11 -0
  44. package/dist/src/testing.js +11 -0
  45. package/package.json +61 -0
@@ -0,0 +1,359 @@
1
+ /**
2
+ * Workspace transaction mechanics: the snapshot/restore/validate/rollback
3
+ * runner and the closure settlement operations, implemented against the
4
+ * ambient authority context declared in `../transaction.ts`.
5
+ *
6
+ * @experimental This API is unstable and may change without notice.
7
+ */
8
+ import { createHash, randomBytes } from "node:crypto";
9
+ import * as Cause from "effect/Cause";
10
+ import * as Effect from "effect/Effect";
11
+ import * as FileSystem from "effect/FileSystem";
12
+ import * as Option from "effect/Option";
13
+ import * as Path from "effect/Path";
14
+ import * as Semaphore from "effect/Semaphore";
15
+ import { recordFootprint } from "@agentxm/workspace-state";
16
+ import { CurrentWorkspaceClosure, CurrentWorkspaceTransaction, protectInContext, TransitionLockUnavailable, WorkspaceDirectoryError, WorkspaceRestorationError, WorkspaceRestorationIncomplete, WorkspaceTransitionCompromised, } from "@agentxm/workspace-state";
17
+ import { acquireWorkspaceTransitionLock, heldWorkspaceTransition, isWorkspaceTransitionHeldByThisInvocation, } from "./transition-lock.js";
18
+ /** Run one semantic closure's mutations under its closure identity. */
19
+ export const withWorkspaceClosure = (closureId) => (effect) => effect.pipe(Effect.provideService(CurrentWorkspaceClosure, closureId));
20
+ const normalizedTargets = (path, targets) => {
21
+ const sorted = Array.from(new Set(targets.map((target) => path.resolve(target)))).sort((left, right) => left.length - right.length || left.localeCompare(right));
22
+ const retained = [];
23
+ for (const target of sorted) {
24
+ if (retained.some((parent) => target === parent || target.startsWith(`${parent}${path.sep}`))) {
25
+ continue;
26
+ }
27
+ retained.push(target);
28
+ }
29
+ return retained;
30
+ };
31
+ const workspaceRelative = (path, workspaceDir, target) => {
32
+ const relative = path.relative(path.dirname(workspaceDir), target);
33
+ return relative.startsWith("..") ? target : relative;
34
+ };
35
+ const sha256 = (input) => createHash("sha256").update(input).digest("hex");
36
+ /**
37
+ * Deterministic content hash of a path's current state: file bytes, symlink
38
+ * target, recursive directory listing, or the literal `absent`.
39
+ */
40
+ const hashPathState = (fs, path, target) => Effect.gen(function* () {
41
+ const link = yield* fs.readLink(target).pipe(Effect.option);
42
+ if (Option.isSome(link))
43
+ return sha256(`symlink:${link.value}`);
44
+ const exists = yield* fs.exists(target);
45
+ if (!exists)
46
+ return "absent";
47
+ const info = yield* fs.stat(target);
48
+ if (info.type === "Directory") {
49
+ const entries = [...(yield* fs.readDirectory(target))].sort();
50
+ const parts = [];
51
+ for (const entry of entries) {
52
+ const child = yield* hashPathState(fs, path, path.join(target, entry));
53
+ parts.push(`${entry}:${child}`);
54
+ }
55
+ return sha256(`dir:${parts.join("\n")}`);
56
+ }
57
+ const bytes = yield* fs.readFile(target);
58
+ return sha256(bytes);
59
+ }).pipe(Effect.catch(() => Effect.succeed("unhashable")));
60
+ const dropClosureSnapshots = (context, closureId) => {
61
+ let index = context.snapshots.length;
62
+ while (index > 0) {
63
+ index -= 1;
64
+ if (context.snapshots[index]?.closure === closureId) {
65
+ context.snapshots.splice(index, 1);
66
+ }
67
+ }
68
+ context.protectedTargets.delete(closureId);
69
+ };
70
+ /**
71
+ * Settle one closure: its commits stand, so its snapshots leave the
72
+ * restoration set and a later closure touching the same target takes a fresh
73
+ * post-commit preimage. No-op outside a transaction.
74
+ */
75
+ export const settleWorkspaceClosure = (closureId) => CurrentWorkspaceTransaction.pipe(Effect.flatMap(Option.match({
76
+ onNone: () => Effect.void,
77
+ onSome: (context) => context.snapshotSemaphore.withPermits(1)(Effect.sync(() => {
78
+ dropClosureSnapshots(context, closureId);
79
+ })),
80
+ })));
81
+ /**
82
+ * Roll back one failed closure: restore and verify exactly its snapshots, in
83
+ * reverse order, leaving every other closure's work in place. A restoration
84
+ * that does not complete and verify records a pending typed failure the
85
+ * transaction surfaces at its end — the truth travels in memory, never
86
+ * through a later workspace write. No-op outside a transaction.
87
+ */
88
+ export const rollbackWorkspaceClosure = (closureId) => CurrentWorkspaceTransaction.pipe(Effect.flatMap(Option.match({
89
+ onNone: () => Effect.void,
90
+ onSome: (context) => context.snapshotSemaphore.withPermits(1)(Effect.gen(function* () {
91
+ const { fs, path } = context;
92
+ const owned = context.snapshots.filter((snapshot) => snapshot.closure === closureId);
93
+ if (owned.length === 0) {
94
+ dropClosureSnapshots(context, closureId);
95
+ return;
96
+ }
97
+ const held = heldWorkspaceTransition(path.resolve(context.workspaceDir));
98
+ const transitionCompromised = held === undefined ? () => false : held.isCompromised;
99
+ yield* restoreAll(fs, path, owned, transitionCompromised).pipe(Effect.andThen(verifySnapshots(fs, path, owned)), Effect.matchEffect({
100
+ onFailure: (restorationCause) => Effect.sync(() => {
101
+ context.pendingRestorationFailures.push({
102
+ closureId,
103
+ restorationCause,
104
+ retained: owned.map((snapshot) => workspaceRelative(path, context.workspaceDir, snapshot.target)),
105
+ });
106
+ }),
107
+ onSuccess: () => Effect.void,
108
+ }));
109
+ dropClosureSnapshots(context, closureId);
110
+ })),
111
+ })));
112
+ /** Whether anything occupies the path: a file, directory, or (broken) symlink. */
113
+ const pathPresent = (fs, target) => fs.readLink(target).pipe(Effect.map(() => true), Effect.catch(() => fs.exists(target)));
114
+ /**
115
+ * Restore one snapshot through validated staging and atomic publication.
116
+ * The restored content is fully staged and validated in an owned
117
+ * `<target>.tmp.<unique>` sibling before a rename publishes it, so abrupt
118
+ * termination — including a forced process exit — can never expose a
119
+ * partially restored target: the authoritative path holds the failure-time
120
+ * content, the restored content, or (for a directory swap only, between two
121
+ * renames) nothing, never a partial tree. The target path itself is never
122
+ * removed; only owned `.tmp.` siblings are.
123
+ */
124
+ const restoreSnapshot = (fs, path, snapshot) => Effect.gen(function* () {
125
+ if (snapshot.state === "absent") {
126
+ if (!(yield* pathPresent(fs, snapshot.target)))
127
+ return;
128
+ // Publishing absence is one rename: the mutated tree leaves the
129
+ // authoritative path atomically, then the owned trash is removed.
130
+ const trash = `${snapshot.target}.tmp.${randomBytes(6).toString("hex")}`;
131
+ yield* fs.rename(snapshot.target, trash);
132
+ yield* fs.remove(trash, { recursive: true, force: true }).pipe(Effect.ignore);
133
+ return;
134
+ }
135
+ yield* fs.makeDirectory(path.dirname(snapshot.target), { recursive: true });
136
+ const staging = `${snapshot.target}.tmp.${randomBytes(6).toString("hex")}`;
137
+ yield* Effect.gen(function* () {
138
+ if (snapshot.state === "symlink") {
139
+ yield* fs.symlink(snapshot.linkTarget, staging);
140
+ const staged = yield* fs.readLink(staging);
141
+ if (staged !== snapshot.linkTarget) {
142
+ return yield* new WorkspaceRestorationError({
143
+ target: snapshot.target,
144
+ step: "stage",
145
+ cause: { staged, expected: snapshot.linkTarget },
146
+ });
147
+ }
148
+ }
149
+ else {
150
+ yield* fs.copy(snapshot.backup, staging, { preserveTimestamps: true });
151
+ const stagedHash = yield* hashPathState(fs, path, staging);
152
+ const backupHash = yield* hashPathState(fs, path, snapshot.backup);
153
+ if (stagedHash !== backupHash || stagedHash === "unhashable") {
154
+ return yield* new WorkspaceRestorationError({
155
+ target: snapshot.target,
156
+ step: "stage",
157
+ cause: { stagedHash, backupHash },
158
+ });
159
+ }
160
+ }
161
+ const targetLink = yield* fs.readLink(snapshot.target).pipe(Effect.option);
162
+ const targetInfo = Option.isSome(targetLink)
163
+ ? Option.none()
164
+ : yield* fs.stat(snapshot.target).pipe(Effect.option);
165
+ const targetPresent = Option.isSome(targetLink) || Option.isSome(targetInfo);
166
+ const targetIsDirectory = Option.exists(targetInfo, (info) => info.type === "Directory");
167
+ const stagedIsDirectory = snapshot.state === "copied" && (yield* fs.stat(staging)).type === "Directory";
168
+ if (!targetPresent || (!targetIsDirectory && !stagedIsDirectory)) {
169
+ // rename atomically replaces a file or symlink target.
170
+ yield* fs.rename(staging, snapshot.target);
171
+ return;
172
+ }
173
+ // A directory is swapped through two renames of owned names; the
174
+ // moved-aside content is intact in the trash sibling until removal.
175
+ const trash = `${snapshot.target}.tmp.${randomBytes(6).toString("hex")}`;
176
+ yield* fs.rename(snapshot.target, trash);
177
+ yield* fs.rename(staging, snapshot.target);
178
+ yield* fs.remove(trash, { recursive: true, force: true }).pipe(Effect.ignore);
179
+ }).pipe(Effect.onError(() => fs.remove(staging, { recursive: true, force: true }).pipe(Effect.ignore)));
180
+ });
181
+ const restoreAll = (fs, path, snapshots, transitionCompromised) => Effect.forEach([...snapshots].reverse(), (snapshot) => Effect.suspend(() =>
182
+ // Restoration is a durable write like any other: once lock ownership
183
+ // is lost it must stop, or it could overwrite a successor's work.
184
+ transitionCompromised()
185
+ ? Effect.fail(new WorkspaceRestorationError({
186
+ target: snapshot.target,
187
+ step: "stopped",
188
+ cause: undefined,
189
+ }))
190
+ : restoreSnapshot(fs, path, snapshot).pipe(Effect.andThen(recordFootprint({ path: snapshot.target, change: "restored" })))), {
191
+ discard: true,
192
+ });
193
+ const verifySnapshots = (fs, path, snapshots) => Effect.forEach(snapshots, (snapshot) => Effect.gen(function* () {
194
+ const verified = yield* Effect.gen(function* () {
195
+ if (snapshot.state === "absent") {
196
+ return !(yield* fs.exists(snapshot.target));
197
+ }
198
+ if (snapshot.state === "symlink") {
199
+ const link = yield* fs.readLink(snapshot.target).pipe(Effect.option);
200
+ return Option.exists(link, (value) => value === snapshot.linkTarget);
201
+ }
202
+ const restored = yield* hashPathState(fs, path, snapshot.target);
203
+ const backup = yield* hashPathState(fs, path, snapshot.backup);
204
+ return restored === backup && restored !== "unhashable";
205
+ }).pipe(Effect.catch(() => Effect.succeed(false)));
206
+ if (!verified) {
207
+ return yield* new WorkspaceRestorationError({
208
+ target: snapshot.target,
209
+ step: "verify",
210
+ cause: { state: snapshot.state },
211
+ });
212
+ }
213
+ }), { discard: true });
214
+ /**
215
+ * Run one coupled workspace mutation under the workspace transition lock.
216
+ *
217
+ * Every authoritative target is snapshotted into a uniquely prefixed
218
+ * OS-temporary directory before the transition begins. A failed transition or
219
+ * postcondition check restores and verifies the exact pre-operation paths and
220
+ * removes the snapshots; a restoration that does not complete and verify
221
+ * fails with the typed {@link WorkspaceRestorationIncomplete}, preserving the
222
+ * snapshot directory for manual inspection. Nothing about a failure persists
223
+ * in the workspace: the next mutation plans from the current workspace state.
224
+ *
225
+ * The invocation-level transition hold is reused when a plan-family apply
226
+ * already acquired it; otherwise this transaction acquires its own for the
227
+ * duration of the mutation.
228
+ */
229
+ export const runWorkspaceTransaction = (args) => Effect.gen(function* () {
230
+ const current = yield* CurrentWorkspaceTransaction;
231
+ if (Option.isSome(current)) {
232
+ const activeClosure = yield* CurrentWorkspaceClosure;
233
+ yield* Effect.forEach(normalizedTargets(current.value.path, args.targets), (target) => protectInContext(current.value, target, activeClosure), { discard: true });
234
+ const value = yield* args.transition;
235
+ yield* args.validate(value);
236
+ return value;
237
+ }
238
+ const fs = yield* FileSystem.FileSystem;
239
+ const path = yield* Path.Path;
240
+ const workspaceDir = path.resolve(args.workspaceDir);
241
+ const missingWorkspaceAncestors = [];
242
+ let ancestor = workspaceDir;
243
+ while (true) {
244
+ const exists = yield* fs
245
+ .exists(ancestor)
246
+ .pipe(Effect.mapError((cause) => new WorkspaceDirectoryError({ path: ancestor, step: "inspect", cause })));
247
+ if (exists)
248
+ break;
249
+ missingWorkspaceAncestors.push(ancestor);
250
+ const parent = path.dirname(ancestor);
251
+ if (parent === ancestor)
252
+ break;
253
+ ancestor = parent;
254
+ }
255
+ return yield* args.semaphore.withPermits(1)(Effect.gen(function* () {
256
+ yield* fs
257
+ .makeDirectory(workspaceDir, { recursive: true })
258
+ .pipe(Effect.mapError((cause) => new WorkspaceDirectoryError({ path: workspaceDir, step: "create", cause })));
259
+ const scratchDir = path.join(workspaceDir, "tmp");
260
+ const removeEmptyScratch = fs.readDirectory(scratchDir).pipe(Effect.flatMap((entries) => entries.length === 0
261
+ ? fs.remove(scratchDir, { recursive: true, force: false })
262
+ : Effect.void), Effect.ignore);
263
+ const removeNewEmptyWorkspace = Effect.forEach(missingWorkspaceAncestors, (directory) => fs.readDirectory(directory).pipe(Effect.flatMap((entries) => entries.length === 0
264
+ ? fs.remove(directory, { recursive: true, force: false })
265
+ : Effect.void), Effect.ignore), { concurrency: 1, discard: true });
266
+ return yield* Effect.scoped(Effect.gen(function* () {
267
+ // The invocation-level transition hold already provides
268
+ // cross-process exclusion; acquiring here again would deadlock on
269
+ // our own lock.
270
+ if (!isWorkspaceTransitionHeldByThisInvocation(workspaceDir)) {
271
+ const contention = yield* acquireWorkspaceTransitionLock({
272
+ workspaceDir,
273
+ holder: { command: "workspace-transaction", pid: process.pid },
274
+ });
275
+ if (Option.isSome(contention)) {
276
+ return yield* new TransitionLockUnavailable({
277
+ holder: Option.getOrUndefined(contention.value.holder),
278
+ waitedMillis: contention.value.waitedMillis,
279
+ });
280
+ }
281
+ }
282
+ const context = {
283
+ fs,
284
+ path,
285
+ workspaceDir,
286
+ snapshotStore: { dir: undefined },
287
+ protectedTargets: new Map(),
288
+ snapshots: [],
289
+ snapshotSemaphore: Semaphore.makeUnsafe(1),
290
+ pendingRestorationFailures: [],
291
+ snapshotSequence: { value: 0 },
292
+ };
293
+ // The store is removed only when nothing in it is still needed:
294
+ // a closure whose rollback failed leaves its pre-change
295
+ // snapshots preserved for manual recovery, and the typed
296
+ // restoration fact names this directory.
297
+ const removeSnapshotStore = Effect.suspend(() => context.snapshotStore.dir === undefined ||
298
+ context.pendingRestorationFailures.length > 0
299
+ ? Effect.void
300
+ : fs
301
+ .remove(context.snapshotStore.dir, { recursive: true, force: true })
302
+ .pipe(Effect.ignore));
303
+ // The compromise signal of the hold serializing this mutation:
304
+ // the invocation-level hold when one exists, else the one just
305
+ // acquired above. Mutation races against it and stops when
306
+ // ownership is lost.
307
+ const held = heldWorkspaceTransition(workspaceDir);
308
+ // Interruptible like the business side: the race runs inside the
309
+ // uninterruptible rollback guard, and its loser must be
310
+ // interruptible for the race to settle.
311
+ const compromiseSignal = (held === undefined ? Effect.never : held.compromised).pipe(Effect.interruptible);
312
+ const transitionCompromised = held === undefined ? () => false : held.isCompromised;
313
+ const business = Effect.gen(function* () {
314
+ // The transaction's own declared targets belong to the
315
+ // operation closure: no semantic closure is active yet.
316
+ yield* Effect.forEach(normalizedTargets(path, args.targets), (target) => protectInContext(context, target, undefined), { discard: true });
317
+ const value = yield* args.transition;
318
+ yield* args.validate(value);
319
+ return value;
320
+ }).pipe(Effect.provideService(CurrentWorkspaceTransaction, Option.some(context)), Effect.interruptible);
321
+ const retainAll = (cause, restorationCause) => Effect.gen(function* () {
322
+ const interruption = Cause.hasInterruptsOnly(cause);
323
+ return yield* new WorkspaceRestorationIncomplete({
324
+ terminationCause: interruption ? "interruption" : "failure",
325
+ transitionCause: cause,
326
+ restorationCause,
327
+ snapshotDir: context.snapshotStore.dir,
328
+ retained: context.snapshots.map((snapshot) => workspaceRelative(path, workspaceDir, snapshot.target)),
329
+ });
330
+ });
331
+ // The mask/restore shape is load-bearing: the business runs in
332
+ // the restored (interruptible) region so an external termination
333
+ // request reaches it, while the settlement handlers — rollback,
334
+ // verification, and the typed retain path — run uninterruptibly
335
+ // and observe the interruption as a cause. A blanket mask would
336
+ // never deliver the interrupt to the parked business and the
337
+ // invocation could not stop.
338
+ return yield* Effect.uninterruptibleMask((restoreInterruptibility) => restoreInterruptibility(Effect.raceFirst(business, compromiseSignal)).pipe(Effect.matchCauseEffect({
339
+ onFailure: (cause) => {
340
+ const raceError = Option.getOrUndefined(Cause.findErrorOption(cause));
341
+ if (raceError instanceof WorkspaceTransitionCompromised) {
342
+ // Ownership is lost: restoring now could overwrite a
343
+ // successor's work. Retain everything the failure left,
344
+ // keep the snapshots, and fail typed.
345
+ return retainAll(cause, raceError);
346
+ }
347
+ return (args.onRestorationStarted ?? Effect.void)
348
+ .pipe(Effect.andThen(restoreAll(fs, path, context.snapshots, transitionCompromised)), Effect.andThen(verifySnapshots(fs, path, context.snapshots)))
349
+ .pipe(Effect.matchEffect({
350
+ onFailure: (restorationCause) => retainAll(cause, restorationCause),
351
+ onSuccess: () => removeSnapshotStore.pipe(Effect.andThen(Effect.failCause(cause))),
352
+ }));
353
+ },
354
+ onSuccess: (value) => removeSnapshotStore.pipe(Effect.as(value)),
355
+ })));
356
+ })).pipe(Effect.ensuring(removeEmptyScratch), Effect.ensuring(removeNewEmptyWorkspace));
357
+ }));
358
+ });
359
+ //# sourceMappingURL=transaction.js.map
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Workspace transition lock.
3
+ *
4
+ * One cross-process lock serializes mutation-class operations on a workspace.
5
+ * A plan-family apply acquires it after confirmation — planning, network
6
+ * acquisition, preview, and the confirmation decision stay lock-free — then
7
+ * revalidates its candidate and applies while holding it; the workspace
8
+ * transaction reuses the invocation's hold, or acquires its own when no
9
+ * plan-family hold exists. A contending invocation waits with a visible
10
+ * reason up to a bound, then terminates blocked with a machine-readable
11
+ * reference to the holder.
12
+ *
13
+ * Each acquisition records a distinct owner token in the holder metadata.
14
+ * Ownership is proven only by an exact token match — never inferred from a
15
+ * pid or from missing or unreadable metadata — and a holder write that fails
16
+ * releases the acquisition instead of holding anonymously.
17
+ *
18
+ * The lock file lives at `.axm/tmp/workspace-transition.lock`; its path is
19
+ * load-bearing for operational tooling and must not move casually.
20
+ *
21
+ * Acquisition is atomic with respect to interruption: the narrow region from
22
+ * the library granting the hold through finalizer registration is masked, so
23
+ * an interrupt can never strand a granted lock, while contention waits stay
24
+ * interruptible. Only the lock-is-held error class is absorbed by the bounded
25
+ * wait; any other acquisition error surfaces immediately as a typed failure.
26
+ *
27
+ * @experimental This API is unstable and may change without notice.
28
+ */
29
+ import * as Effect from "effect/Effect";
30
+ import * as FileSystem from "effect/FileSystem";
31
+ import * as Option from "effect/Option";
32
+ import * as Path from "effect/Path";
33
+ import type * as Scope from "effect/Scope";
34
+ import { WorkspaceTransitionCompromised, type TransitionContention, type TransitionLockHolder, type WorkspaceTransitionAcquireFailure } from "@agentxm/workspace-state";
35
+ export declare const TRANSITION_LOCK_FILENAME = "workspace-transition.lock";
36
+ /** How long a contending invocation serializes behind the holder. */
37
+ export declare const TRANSITION_WAIT_BOUND_MILLIS = 60000;
38
+ /** The live view of one acquisition this process currently holds. */
39
+ export interface HeldWorkspaceTransition {
40
+ /** Fails when the hold is compromised; never succeeds and never ends otherwise. */
41
+ readonly compromised: Effect.Effect<never, WorkspaceTransitionCompromised>;
42
+ /** Synchronous probe for boundaries that cannot race, such as restoration. */
43
+ readonly isCompromised: () => boolean;
44
+ }
45
+ export declare const isWorkspaceTransitionHeldByThisInvocation: (workspaceDir: string) => boolean;
46
+ /** The live hold for a workspace this process acquired, when one exists. */
47
+ export declare const heldWorkspaceTransition: (workspaceDir: string) => HeldWorkspaceTransition | undefined;
48
+ export declare const transitionLockPath: (path: Path.Path, workspaceDir: string) => string;
49
+ /**
50
+ * Acquire the workspace transition lock, waiting up to the bound while
51
+ * another invocation holds it. Resolves `None` when acquired (the release is
52
+ * a scope finalizer) and `Some(contention)` when the bound elapsed.
53
+ */
54
+ export declare const acquireWorkspaceTransitionLock: (args: {
55
+ readonly workspaceDir: string;
56
+ readonly holder: TransitionLockHolder;
57
+ readonly waitBoundMillis?: number;
58
+ /** Called once when the invocation starts waiting on another holder. */
59
+ readonly onWaiting?: (holder: Option.Option<TransitionLockHolder>) => Effect.Effect<void>;
60
+ /** Staleness/refresh override for deterministic compromise tests only. */
61
+ readonly timingMillis?: {
62
+ readonly stale: number;
63
+ readonly update: number;
64
+ };
65
+ }) => Effect.Effect<Option.Option<TransitionContention>, WorkspaceTransitionAcquireFailure, FileSystem.FileSystem | Path.Path | Scope.Scope>;
66
+ //# sourceMappingURL=transition-lock.d.ts.map