@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.
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/operations/load-workspace.d.ts +2 -1
- package/dist/src/operations/load-workspace.js +8 -5
- package/dist/src/operations/memory-transition-lock.d.ts +20 -0
- package/dist/src/operations/memory-transition-lock.js +57 -0
- package/dist/src/operations/transaction.d.ts +2 -0
- package/dist/src/operations/transaction.js +5 -6
- package/dist/src/operations/transition-lock.d.ts +6 -0
- package/dist/src/operations/transition-lock.js +4 -0
- package/dist/src/testing.d.ts +1 -0
- package/dist/src/testing.js +1 -0
- package/package.json +4 -4
package/dist/src/index.d.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
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) =>
|
|
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
|
-
})
|
|
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
|
|
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 (
|
|
271
|
-
const contention = yield*
|
|
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
|
package/dist/src/testing.d.ts
CHANGED
|
@@ -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
|
package/dist/src/testing.js
CHANGED
|
@@ -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.
|
|
8
|
-
"@agentxm/registry-protocol": "^0.28.
|
|
9
|
-
"@agentxm/workspace-state": "^0.28.
|
|
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.
|
|
63
|
+
"version": "0.28.12"
|
|
64
64
|
}
|