@agentxm/workspace-operations 0.28.10 → 0.28.12

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.
@@ -27,5 +27,5 @@ export * from "./plan/job-step-message.js";
27
27
  export { scanPlanReadiness, type PlanReadinessReport } from "./operations/scan-plan-readiness.js";
28
28
  export { augmentPlanWithReconciliation, type AugmentedPlanResult, type DegradedLockfileState, } from "./operations/augment-plan.js";
29
29
  export { rollbackWorkspaceClosure, runWorkspaceTransaction, settleWorkspaceClosure, withWorkspaceClosure, type WorkspaceTransactionArgs, } from "./operations/transaction.js";
30
- export { TRANSITION_WAIT_BOUND_MILLIS, acquireWorkspaceTransitionLock, heldWorkspaceTransition, isWorkspaceTransitionHeldByThisInvocation, transitionLockPath, type HeldWorkspaceTransition, } from "./operations/transition-lock.js";
30
+ export { liveWorkspaceTransitionLock, type WorkspaceTransitionLock, TRANSITION_WAIT_BOUND_MILLIS, acquireWorkspaceTransitionLock, heldWorkspaceTransition, isWorkspaceTransitionHeldByThisInvocation, transitionLockPath, type HeldWorkspaceTransition, } from "./operations/transition-lock.js";
31
31
  //# sourceMappingURL=index.d.ts.map
package/dist/src/index.js CHANGED
@@ -39,5 +39,5 @@ export { augmentPlanWithReconciliation, } from "./operations/augment-plan.js";
39
39
  // Workspace transaction runner and closure lifecycle
40
40
  export { rollbackWorkspaceClosure, runWorkspaceTransaction, settleWorkspaceClosure, withWorkspaceClosure, } from "./operations/transaction.js";
41
41
  // Transition lock runtime
42
- export { TRANSITION_WAIT_BOUND_MILLIS, acquireWorkspaceTransitionLock, heldWorkspaceTransition, isWorkspaceTransitionHeldByThisInvocation, transitionLockPath, } from "./operations/transition-lock.js";
42
+ export { liveWorkspaceTransitionLock, TRANSITION_WAIT_BOUND_MILLIS, acquireWorkspaceTransitionLock, heldWorkspaceTransition, isWorkspaceTransitionHeldByThisInvocation, transitionLockPath, } from "./operations/transition-lock.js";
43
43
  //# sourceMappingURL=index.js.map
@@ -11,12 +11,13 @@ import * as FileSystem from "effect/FileSystem";
11
11
  import * as Layer from "effect/Layer";
12
12
  import * as Path from "effect/Path";
13
13
  import { WorkspaceMutations, type MakeWorkspaceTransactionCapabilities, type WorkspaceLayerOptions, type WorkspaceMutationsError, type WorkspaceMutationsService } from "@agentxm/workspace-state";
14
+ import { type WorkspaceTransitionLock } from "./transition-lock.js";
14
15
  /**
15
16
  * The live transaction capabilities: the runner claims the shared settings
16
17
  * and lockfile targets by default, and both members eliminate FileSystem and
17
18
  * Path so the facade's methods stay `R = never` for callers.
18
19
  */
19
- export declare const makeWorkspaceTransactionCapabilities: MakeWorkspaceTransactionCapabilities;
20
+ export declare const makeWorkspaceTransactionCapabilities: (lock: WorkspaceTransitionLock) => MakeWorkspaceTransactionCapabilities;
20
21
  /**
21
22
  * Create workspace mutations effect.
22
23
  *
@@ -13,13 +13,13 @@ import * as Path from "effect/Path";
13
13
  import * as Semaphore from "effect/Semaphore";
14
14
  import { WorkspaceMutations, makeWorkspaceMutations, } from "@agentxm/workspace-state";
15
15
  import { runWorkspaceTransaction } from "./transaction.js";
16
- import { acquireWorkspaceTransitionLock } from "./transition-lock.js";
16
+ import { liveWorkspaceTransitionLock } from "./transition-lock.js";
17
17
  /**
18
18
  * The live transaction capabilities: the runner claims the shared settings
19
19
  * and lockfile targets by default, and both members eliminate FileSystem and
20
20
  * Path so the facade's methods stay `R = never` for callers.
21
21
  */
22
- export const makeWorkspaceTransactionCapabilities = ({ workspaceDir, settingsPath, lockPath, }) => Effect.gen(function* () {
22
+ export const makeWorkspaceTransactionCapabilities = (lock) => ({ workspaceDir, settingsPath, lockPath }) => Effect.gen(function* () {
23
23
  const fs = yield* FileSystem.FileSystem;
24
24
  const path = yield* Path.Path;
25
25
  // Transaction admission must be distinct from the facade's mutation
@@ -28,6 +28,7 @@ export const makeWorkspaceTransactionCapabilities = ({ workspaceDir, settingsPat
28
28
  const transactionSemaphore = yield* Semaphore.make(1);
29
29
  const fsLayer = Layer.mergeAll(Layer.succeed(FileSystem.FileSystem, fs), Layer.succeed(Path.Path, path));
30
30
  const runTransaction = (args) => runWorkspaceTransaction({
31
+ lock,
31
32
  workspaceDir,
32
33
  semaphore: transactionSemaphore,
33
34
  targets: [
@@ -40,7 +41,8 @@ export const makeWorkspaceTransactionCapabilities = ({ workspaceDir, settingsPat
40
41
  ? {}
41
42
  : { onRestorationStarted: args.onRestorationStarted }),
42
43
  }).pipe(Effect.provide(fsLayer));
43
- const acquireTransition = (request) => acquireWorkspaceTransitionLock({
44
+ const acquireTransition = (request) => lock
45
+ .acquire({
44
46
  workspaceDir,
45
47
  holder: {
46
48
  command: request.command,
@@ -48,7 +50,8 @@ export const makeWorkspaceTransactionCapabilities = ({ workspaceDir, settingsPat
48
50
  ...(request.candidateId === undefined ? {} : { candidateId: request.candidateId }),
49
51
  },
50
52
  ...(request.onWaiting === undefined ? {} : { onWaiting: request.onWaiting }),
51
- }).pipe(Effect.provide(fsLayer));
53
+ })
54
+ .pipe(Effect.provide(fsLayer));
52
55
  return { runTransaction, acquireTransition };
53
56
  });
54
57
  /**
@@ -62,7 +65,7 @@ export const makeWorkspaceTransactionCapabilities = ({ workspaceDir, settingsPat
62
65
  * @param options - WorkspaceMutations layer options
63
66
  * @returns Effect yielding WorkspaceMutationsService
64
67
  */
65
- export const loadWorkspace = (options) => makeWorkspaceMutations(options, makeWorkspaceTransactionCapabilities);
68
+ export const loadWorkspace = (options) => makeWorkspaceMutations(options, makeWorkspaceTransactionCapabilities(liveWorkspaceTransitionLock));
66
69
  /**
67
70
  * Create a layer that loads workspace read model from disk.
68
71
  *
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Deterministic in-process workspace transition admission for tests.
3
+ *
4
+ * One world owns admission state while each invocation receives an isolated
5
+ * ownership view. Waiting uses Effect time so contention and interruption can
6
+ * be exercised without native timers or lock files.
7
+ *
8
+ * @experimental This API is unstable and may change without notice.
9
+ */
10
+ import type { WorkspaceTransitionLock } from "./transition-lock.js";
11
+ export interface MemoryTransitionLockWorld {
12
+ readonly counts: () => {
13
+ readonly acquisitions: number;
14
+ readonly releases: number;
15
+ };
16
+ readonly invocation: () => WorkspaceTransitionLock;
17
+ }
18
+ /** Make one isolated admission world for a deterministic test scenario. */
19
+ export declare const makeMemoryTransitionLockWorld: () => MemoryTransitionLockWorld;
20
+ //# sourceMappingURL=memory-transition-lock.d.ts.map
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Deterministic in-process workspace transition admission for tests.
3
+ *
4
+ * One world owns admission state while each invocation receives an isolated
5
+ * ownership view. Waiting uses Effect time so contention and interruption can
6
+ * be exercised without native timers or lock files.
7
+ *
8
+ * @experimental This API is unstable and may change without notice.
9
+ */
10
+ import * as Effect from "effect/Effect";
11
+ import * as Option from "effect/Option";
12
+ import * as Semaphore from "effect/Semaphore";
13
+ /** Make one isolated admission world for a deterministic test scenario. */
14
+ export const makeMemoryTransitionLockWorld = () => {
15
+ const admission = Semaphore.makeUnsafe(1);
16
+ let acquisitions = 0;
17
+ let releases = 0;
18
+ let holder = Option.none();
19
+ return {
20
+ counts: () => ({ acquisitions, releases }),
21
+ invocation: () => {
22
+ let held;
23
+ return {
24
+ held: (directory) => (held?.directory === directory ? held.lease : undefined),
25
+ acquire: (args) => Effect.uninterruptibleMask((restore) => Effect.gen(function* () {
26
+ let waitedMillis = 0;
27
+ let reportedWaiting = false;
28
+ while (!(yield* admission.takeIfAvailable(1))) {
29
+ if (!reportedWaiting && args.onWaiting !== undefined) {
30
+ yield* restore(args.onWaiting(holder));
31
+ reportedWaiting = true;
32
+ }
33
+ if (waitedMillis >= (args.waitBoundMillis ?? 60_000)) {
34
+ return Option.some({ holder, waitedMillis });
35
+ }
36
+ yield* restore(Effect.sleep("250 millis"));
37
+ waitedMillis += 250;
38
+ }
39
+ holder = Option.some(args.holder);
40
+ acquisitions += 1;
41
+ held = {
42
+ directory: args.workspaceDir,
43
+ lease: { compromised: Effect.never, isCompromised: () => false },
44
+ };
45
+ yield* Effect.addFinalizer(() => Effect.gen(function* () {
46
+ held = undefined;
47
+ holder = Option.none();
48
+ releases += 1;
49
+ yield* admission.release(1);
50
+ }));
51
+ return Option.none();
52
+ })),
53
+ };
54
+ },
55
+ };
56
+ };
57
+ //# sourceMappingURL=memory-transition-lock.js.map
@@ -10,9 +10,11 @@ import * as FileSystem from "effect/FileSystem";
10
10
  import * as Path from "effect/Path";
11
11
  import * as Semaphore from "effect/Semaphore";
12
12
  import { WorkspaceRestorationIncomplete, type WorkspaceTransactionFailure } from "@agentxm/workspace-state";
13
+ import type { WorkspaceTransitionLock } from "./transition-lock.js";
13
14
  /** Run one semantic closure's mutations under its closure identity. */
14
15
  export declare const withWorkspaceClosure: (closureId: string) => <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R>;
15
16
  export interface WorkspaceTransactionArgs<A, E, R> {
17
+ readonly lock: WorkspaceTransitionLock;
16
18
  readonly workspaceDir: string;
17
19
  /** In-process admission owned by the workspace service instance. */
18
20
  readonly semaphore: Semaphore.Semaphore;
@@ -14,7 +14,6 @@ import * as Path from "effect/Path";
14
14
  import * as Semaphore from "effect/Semaphore";
15
15
  import { recordFootprint } from "@agentxm/workspace-state";
16
16
  import { CurrentWorkspaceClosure, CurrentWorkspaceTransaction, protectInContext, TransitionLockUnavailable, WorkspaceDirectoryError, WorkspaceRestorationError, WorkspaceRestorationIncomplete, WorkspaceTransitionCompromised, } from "@agentxm/workspace-state";
17
- import { acquireWorkspaceTransitionLock, heldWorkspaceTransition, isWorkspaceTransitionHeldByThisInvocation, } from "./transition-lock.js";
18
17
  /** Run one semantic closure's mutations under its closure identity. */
19
18
  export const withWorkspaceClosure = (closureId) => (effect) => effect.pipe(Effect.provideService(CurrentWorkspaceClosure, closureId));
20
19
  const normalizedTargets = (path, targets) => {
@@ -94,8 +93,7 @@ export const rollbackWorkspaceClosure = (closureId) => CurrentWorkspaceTransacti
94
93
  dropClosureSnapshots(context, closureId);
95
94
  return;
96
95
  }
97
- const held = heldWorkspaceTransition(path.resolve(context.workspaceDir));
98
- const transitionCompromised = held === undefined ? () => false : held.isCompromised;
96
+ const transitionCompromised = context.isTransitionCompromised;
99
97
  yield* restoreAll(fs, path, owned, transitionCompromised).pipe(Effect.andThen(verifySnapshots(fs, path, owned)), Effect.matchEffect({
100
98
  onFailure: (restorationCause) => Effect.sync(() => {
101
99
  context.pendingRestorationFailures.push({
@@ -267,8 +265,8 @@ export const runWorkspaceTransaction = (args) => Effect.gen(function* () {
267
265
  // The invocation-level transition hold already provides
268
266
  // cross-process exclusion; acquiring here again would deadlock on
269
267
  // our own lock.
270
- if (!isWorkspaceTransitionHeldByThisInvocation(workspaceDir)) {
271
- const contention = yield* acquireWorkspaceTransitionLock({
268
+ if (args.lock.held(workspaceDir) === undefined) {
269
+ const contention = yield* args.lock.acquire({
272
270
  workspaceDir,
273
271
  holder: { command: "workspace-transaction", pid: process.pid },
274
272
  });
@@ -279,7 +277,9 @@ export const runWorkspaceTransaction = (args) => Effect.gen(function* () {
279
277
  });
280
278
  }
281
279
  }
280
+ const held = args.lock.held(workspaceDir);
282
281
  const context = {
282
+ isTransitionCompromised: held?.isCompromised ?? (() => false),
283
283
  fs,
284
284
  path,
285
285
  workspaceDir,
@@ -304,7 +304,6 @@ export const runWorkspaceTransaction = (args) => Effect.gen(function* () {
304
304
  // the invocation-level hold when one exists, else the one just
305
305
  // acquired above. Mutation races against it and stops when
306
306
  // ownership is lost.
307
- const held = heldWorkspaceTransition(workspaceDir);
308
307
  // Interruptible like the business side: the race runs inside the
309
308
  // uninterruptible rollback guard, and its loser must be
310
309
  // interruptible for the race to settle.
@@ -63,4 +63,10 @@ export declare const acquireWorkspaceTransitionLock: (args: {
63
63
  readonly update: number;
64
64
  };
65
65
  }) => Effect.Effect<Option.Option<TransitionContention>, WorkspaceTransitionAcquireFailure, FileSystem.FileSystem | Path.Path | Scope.Scope>;
66
+ /** One invocation's view of transition admission and ownership. */
67
+ export interface WorkspaceTransitionLock {
68
+ readonly acquire: typeof acquireWorkspaceTransitionLock;
69
+ readonly held: typeof heldWorkspaceTransition;
70
+ }
71
+ export declare const liveWorkspaceTransitionLock: WorkspaceTransitionLock;
66
72
  //# sourceMappingURL=transition-lock.d.ts.map
@@ -291,4 +291,8 @@ export const acquireWorkspaceTransitionLock = (args) => Effect.gen(function* ()
291
291
  waitedMillis += Duration.toMillis(WAIT_INTERVAL);
292
292
  }
293
293
  });
294
+ export const liveWorkspaceTransitionLock = {
295
+ acquire: acquireWorkspaceTransitionLock,
296
+ held: heldWorkspaceTransition,
297
+ };
294
298
  //# sourceMappingURL=transition-lock.js.map
@@ -9,4 +9,5 @@
9
9
  */
10
10
  export { ResolvePlanInteractionTest, type ResolvePlanInteractionTestState, } from "./plan/resolve-plan-interaction.js";
11
11
  export { interactiveOnlyPlanExecution, preapprovedPlanExecution, promptablePlanExecution, } from "./plan/plan-execution-fixtures.js";
12
+ export { makeMemoryTransitionLockWorld, type MemoryTransitionLockWorld, } from "./operations/memory-transition-lock.js";
12
13
  //# sourceMappingURL=testing.d.ts.map
@@ -9,4 +9,5 @@
9
9
  */
10
10
  export { ResolvePlanInteractionTest, } from "./plan/resolve-plan-interaction.js";
11
11
  export { interactiveOnlyPlanExecution, preapprovedPlanExecution, promptablePlanExecution, } from "./plan/plan-execution-fixtures.js";
12
+ export { makeMemoryTransitionLockWorld, } from "./operations/memory-transition-lock.js";
12
13
  //# sourceMappingURL=testing.js.map
package/package.json CHANGED
@@ -4,9 +4,9 @@
4
4
  "url": "https://github.com/agentxm/axm/issues"
5
5
  },
6
6
  "dependencies": {
7
- "@agentxm/extension-model": "^0.28.10",
8
- "@agentxm/registry-protocol": "^0.28.10",
9
- "@agentxm/workspace-state": "^0.28.10",
7
+ "@agentxm/extension-model": "^0.28.12",
8
+ "@agentxm/registry-protocol": "^0.28.12",
9
+ "@agentxm/workspace-state": "^0.28.12",
10
10
  "effect": "4.0.0-rc.112",
11
11
  "proper-lockfile": "^4.1.2"
12
12
  },
@@ -60,5 +60,5 @@
60
60
  },
61
61
  "sideEffects": false,
62
62
  "type": "module",
63
- "version": "0.28.10"
63
+ "version": "0.28.12"
64
64
  }