@agentxm/workspace-transactions 0.28.14-preview.1789139351.5cd4595aa
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/LICENSE +110 -0
- package/README.md +15 -0
- package/dist/src/atomic-write.d.ts +67 -0
- package/dist/src/atomic-write.js +71 -0
- package/dist/src/context.d.ts +56 -0
- package/dist/src/context.js +115 -0
- package/dist/src/errors.d.ts +129 -0
- package/dist/src/errors.js +58 -0
- package/dist/src/footprint-recorder.d.ts +32 -0
- package/dist/src/footprint-recorder.js +29 -0
- package/dist/src/index.d.ts +25 -0
- package/dist/src/index.js +24 -0
- package/dist/src/ledger.d.ts +61 -0
- package/dist/src/ledger.js +56 -0
- package/dist/src/memory-transition-lock.d.ts +31 -0
- package/dist/src/memory-transition-lock.js +69 -0
- package/dist/src/restoration.d.ts +21 -0
- package/dist/src/restoration.js +159 -0
- package/dist/src/scope.d.ts +61 -0
- package/dist/src/scope.js +48 -0
- package/dist/src/testing.d.ts +52 -0
- package/dist/src/testing.js +49 -0
- package/dist/src/transaction.d.ts +82 -0
- package/dist/src/transaction.js +223 -0
- package/dist/src/transition-lock.d.ts +78 -0
- package/dist/src/transition-lock.js +302 -0
- package/package.json +55 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Functional Source License, Version 1.1, MIT Future License
|
|
2
|
+
|
|
3
|
+
## Abbreviation
|
|
4
|
+
|
|
5
|
+
FSL-1.1-MIT
|
|
6
|
+
|
|
7
|
+
## Notice
|
|
8
|
+
|
|
9
|
+
Copyright 2025-2026 AgentXM, Inc.
|
|
10
|
+
|
|
11
|
+
## Terms and Conditions
|
|
12
|
+
|
|
13
|
+
### Licensor ("We")
|
|
14
|
+
|
|
15
|
+
The party offering the Software under these Terms and Conditions.
|
|
16
|
+
|
|
17
|
+
### The Software
|
|
18
|
+
|
|
19
|
+
The "Software" is each version of the software that we make available under
|
|
20
|
+
these Terms and Conditions, as indicated by our inclusion of these Terms and
|
|
21
|
+
Conditions with the Software.
|
|
22
|
+
|
|
23
|
+
### License Grant
|
|
24
|
+
|
|
25
|
+
Subject to your compliance with this License Grant and the Patents,
|
|
26
|
+
Redistribution and Trademark clauses below, we hereby grant you the right to
|
|
27
|
+
use, copy, modify, create derivative works, publicly perform, publicly display
|
|
28
|
+
and redistribute the Software for any Permitted Purpose identified below.
|
|
29
|
+
|
|
30
|
+
### Permitted Purpose
|
|
31
|
+
|
|
32
|
+
A Permitted Purpose is any purpose other than a Competing Use. A Competing Use
|
|
33
|
+
means making the Software available to others in a commercial product or
|
|
34
|
+
service that:
|
|
35
|
+
|
|
36
|
+
1. substitutes for the Software;
|
|
37
|
+
|
|
38
|
+
2. substitutes for any other product or service we offer using the Software
|
|
39
|
+
that exists as of the date we make the Software available; or
|
|
40
|
+
|
|
41
|
+
3. offers the same or substantially similar functionality as the Software.
|
|
42
|
+
|
|
43
|
+
Permitted Purposes specifically include using the Software:
|
|
44
|
+
|
|
45
|
+
1. for your internal use and access;
|
|
46
|
+
|
|
47
|
+
2. for non-commercial education;
|
|
48
|
+
|
|
49
|
+
3. for non-commercial research; and
|
|
50
|
+
|
|
51
|
+
4. in connection with professional services that you provide to a licensee
|
|
52
|
+
using the Software in accordance with these Terms and Conditions.
|
|
53
|
+
|
|
54
|
+
### Patents
|
|
55
|
+
|
|
56
|
+
To the extent your use for a Permitted Purpose would necessarily infringe our
|
|
57
|
+
patents, the license grant above includes a license under our patents. If you
|
|
58
|
+
make a claim against any party that the Software infringes or contributes to
|
|
59
|
+
the infringement of any patent, then your patent license to the Software ends
|
|
60
|
+
immediately.
|
|
61
|
+
|
|
62
|
+
### Redistribution
|
|
63
|
+
|
|
64
|
+
The Terms and Conditions apply to all copies, modifications and derivatives of
|
|
65
|
+
the Software.
|
|
66
|
+
|
|
67
|
+
If you redistribute any copies, modifications or derivatives of the Software,
|
|
68
|
+
you must include a copy of or a link to these Terms and Conditions and not
|
|
69
|
+
remove any copyright notices provided in or with the Software.
|
|
70
|
+
|
|
71
|
+
### Disclaimer
|
|
72
|
+
|
|
73
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR
|
|
74
|
+
IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR
|
|
75
|
+
PURPOSE, MERCHANTABILITY, TITLE OR NON-INFRINGEMENT.
|
|
76
|
+
|
|
77
|
+
IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE
|
|
78
|
+
SOFTWARE, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES,
|
|
79
|
+
EVEN IF WE HAVE BEEN INFORMED OF THEIR POSSIBILITY IN ADVANCE.
|
|
80
|
+
|
|
81
|
+
### Trademarks
|
|
82
|
+
|
|
83
|
+
Except for displaying the License Details and identifying us as the origin of
|
|
84
|
+
the Software, you have no right under these Terms and Conditions to use our
|
|
85
|
+
trademarks, trade names, service marks or product names.
|
|
86
|
+
|
|
87
|
+
## Grant of Future License
|
|
88
|
+
|
|
89
|
+
We hereby irrevocably grant you an additional license to use the Software under
|
|
90
|
+
the MIT license that is effective on the second anniversary of the date we make
|
|
91
|
+
the Software available. On or after that date, you may use the Software under
|
|
92
|
+
the MIT license, in which case the following will apply:
|
|
93
|
+
|
|
94
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
95
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
96
|
+
the Software without restriction, including without limitation the rights to
|
|
97
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
98
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
|
99
|
+
so, subject to the following conditions:
|
|
100
|
+
|
|
101
|
+
The above copyright notice and this permission notice shall be included in all
|
|
102
|
+
copies or substantial portions of the Software.
|
|
103
|
+
|
|
104
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
105
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
106
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
107
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
108
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
109
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
110
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# @agentxm/workspace-transactions
|
|
2
|
+
|
|
3
|
+
The AXM workspace transaction capability: the cross-process workspace
|
|
4
|
+
transition lock, the per-closure snapshot ledger with restoration and
|
|
5
|
+
verification, atomic single-file publication, and footprint observation. Every
|
|
6
|
+
workspace writer registers its target here before the first mutation, and
|
|
7
|
+
`workspace-operations` settles or rolls back each semantic closure through the
|
|
8
|
+
closure API this package exports for it alone.
|
|
9
|
+
|
|
10
|
+
The package root is the public API; deterministic in-memory admission and the
|
|
11
|
+
fault-injection hooks live behind `./testing`. The surface is unsupported and
|
|
12
|
+
may change in any release. Ordinary users should use the
|
|
13
|
+
[`axm` CLI](https://axm.sh) instead.
|
|
14
|
+
|
|
15
|
+
Part of the [AgentXM](https://agentxm.ai) toolchain.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared atomic single-file replacement: write a uniquely named temp file in
|
|
3
|
+
* the target's directory, then rename it over the target.
|
|
4
|
+
*
|
|
5
|
+
* The temp file lives next to the target so the rename stays on one
|
|
6
|
+
* filesystem (and therefore atomic), and is removed on any failure or
|
|
7
|
+
* interruption. Temp names follow `<target>.tmp.<unique>`; sweepers that
|
|
8
|
+
* clean stale temps (e.g. the lockfile writer) rely on that prefix.
|
|
9
|
+
*
|
|
10
|
+
* @experimental This API is unstable and may change without notice.
|
|
11
|
+
*/
|
|
12
|
+
import * as Effect from "effect/Effect";
|
|
13
|
+
import type * as FileSystem from "effect/FileSystem";
|
|
14
|
+
import * as Path from "effect/Path";
|
|
15
|
+
import type { PlatformError } from "effect/PlatformError";
|
|
16
|
+
/**
|
|
17
|
+
* Step at which an atomic write failed. `check-target` and `read-target` can
|
|
18
|
+
* only occur with `skipIfUnchanged: "fail-on-read-error"`.
|
|
19
|
+
*
|
|
20
|
+
* @experimental This API is unstable and may change without notice.
|
|
21
|
+
*/
|
|
22
|
+
export type AtomicWriteStep = "check-target" | "read-target" | "write-temp" | "rename";
|
|
23
|
+
/**
|
|
24
|
+
* Failure passed to `mapError` so each call site keeps its own error shape.
|
|
25
|
+
*
|
|
26
|
+
* @experimental This API is unstable and may change without notice.
|
|
27
|
+
*/
|
|
28
|
+
export interface AtomicWriteFailure {
|
|
29
|
+
readonly step: AtomicWriteStep;
|
|
30
|
+
readonly targetPath: string;
|
|
31
|
+
readonly tempPath: string;
|
|
32
|
+
readonly cause: PlatformError;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Content-equality short-circuit mode: `fail-on-read-error` surfaces
|
|
36
|
+
* check/read failures via `mapError`; `ignore-read-errors` treats an
|
|
37
|
+
* unreadable target as changed and proceeds to write.
|
|
38
|
+
*
|
|
39
|
+
* @experimental This API is unstable and may change without notice.
|
|
40
|
+
*/
|
|
41
|
+
export type SkipIfUnchanged = "fail-on-read-error" | "ignore-read-errors";
|
|
42
|
+
/**
|
|
43
|
+
* Options for `writeFileAtomic`.
|
|
44
|
+
*
|
|
45
|
+
* @experimental This API is unstable and may change without notice.
|
|
46
|
+
*/
|
|
47
|
+
export interface WriteFileAtomicOptions<E> {
|
|
48
|
+
readonly targetPath: string;
|
|
49
|
+
readonly content: string | Uint8Array;
|
|
50
|
+
/** When set, leave the target untouched if it already has this content. */
|
|
51
|
+
readonly skipIfUnchanged?: SkipIfUnchanged;
|
|
52
|
+
/** Best-effort remove of the target before rename (Windows cannot always rename over an existing file). */
|
|
53
|
+
readonly removeTargetBeforeRename?: boolean;
|
|
54
|
+
readonly mapError: (failure: AtomicWriteFailure) => E;
|
|
55
|
+
}
|
|
56
|
+
export declare const atomicWriteTempPrefix: (targetPath: string) => string;
|
|
57
|
+
/** Remove only stale temp siblings belonging to one atomic-write target. */
|
|
58
|
+
export declare const sweepStaleAtomicWriteTemps: (fs: FileSystem.FileSystem, targetPath: string) => Effect.Effect<void, never, Path.Path>;
|
|
59
|
+
/**
|
|
60
|
+
* Atomically replace `targetPath` with `content` via a same-directory temp
|
|
61
|
+
* file and rename. Failures at each step are mapped by the caller, so error
|
|
62
|
+
* codes, details, and suggestions stay call-site specific.
|
|
63
|
+
*
|
|
64
|
+
* @experimental This API is unstable and may change without notice.
|
|
65
|
+
*/
|
|
66
|
+
export declare const writeFileAtomic: <E>(fs: FileSystem.FileSystem, options: WriteFileAtomicOptions<E>) => Effect.Effect<"written" | "skipped", E>;
|
|
67
|
+
//# sourceMappingURL=atomic-write.d.ts.map
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared atomic single-file replacement: write a uniquely named temp file in
|
|
3
|
+
* the target's directory, then rename it over the target.
|
|
4
|
+
*
|
|
5
|
+
* The temp file lives next to the target so the rename stays on one
|
|
6
|
+
* filesystem (and therefore atomic), and is removed on any failure or
|
|
7
|
+
* interruption. Temp names follow `<target>.tmp.<unique>`; sweepers that
|
|
8
|
+
* clean stale temps (e.g. the lockfile writer) rely on that prefix.
|
|
9
|
+
*
|
|
10
|
+
* @experimental This API is unstable and may change without notice.
|
|
11
|
+
*/
|
|
12
|
+
import * as Effect from "effect/Effect";
|
|
13
|
+
import * as Option from "effect/Option";
|
|
14
|
+
import * as Path from "effect/Path";
|
|
15
|
+
/** In-process sequence so concurrent fibers never share a temp path. */
|
|
16
|
+
let tempSequence = 0;
|
|
17
|
+
export const atomicWriteTempPrefix = (targetPath) => `${targetPath}.tmp.`;
|
|
18
|
+
/** Remove only stale temp siblings belonging to one atomic-write target. */
|
|
19
|
+
export const sweepStaleAtomicWriteTemps = (fs, targetPath) => Effect.gen(function* () {
|
|
20
|
+
const path = yield* Path.Path;
|
|
21
|
+
const parent = path.dirname(targetPath);
|
|
22
|
+
const filenamePrefix = `${path.basename(targetPath)}.tmp.`;
|
|
23
|
+
const entries = yield* fs.readDirectory(parent).pipe(Effect.orElseSucceed(() => []));
|
|
24
|
+
yield* Effect.forEach(entries.filter((entry) => entry.startsWith(filenamePrefix)), (entry) => fs.remove(path.join(parent, entry), { force: true }).pipe(Effect.ignore), { discard: true });
|
|
25
|
+
});
|
|
26
|
+
const makeTempPath = (targetPath) => {
|
|
27
|
+
tempSequence += 1;
|
|
28
|
+
const pid = typeof process === "object" ? process.pid.toString(36) : "x";
|
|
29
|
+
const random = Math.random().toString(36).slice(2, 8);
|
|
30
|
+
return `${atomicWriteTempPrefix(targetPath)}${pid}.${tempSequence.toString(36)}.${random}`;
|
|
31
|
+
};
|
|
32
|
+
const bytesEqual = (a, b) => a.length === b.length && a.every((value, index) => value === b[index]);
|
|
33
|
+
const targetHasContent = (fs, targetPath, content) => typeof content === "string"
|
|
34
|
+
? Effect.map(fs.readFileString(targetPath), (current) => current === content)
|
|
35
|
+
: Effect.map(fs.readFile(targetPath), (current) => bytesEqual(current, content));
|
|
36
|
+
/**
|
|
37
|
+
* Atomically replace `targetPath` with `content` via a same-directory temp
|
|
38
|
+
* file and rename. Failures at each step are mapped by the caller, so error
|
|
39
|
+
* codes, details, and suggestions stay call-site specific.
|
|
40
|
+
*
|
|
41
|
+
* @experimental This API is unstable and may change without notice.
|
|
42
|
+
*/
|
|
43
|
+
export const writeFileAtomic = (fs, options) => Effect.gen(function* () {
|
|
44
|
+
const { content, mapError, targetPath } = options;
|
|
45
|
+
const tempPath = makeTempPath(targetPath);
|
|
46
|
+
const fail = (step) => (cause) => mapError({ step, targetPath, tempPath, cause });
|
|
47
|
+
if (options.skipIfUnchanged === "fail-on-read-error") {
|
|
48
|
+
const exists = yield* fs.exists(targetPath).pipe(Effect.mapError(fail("check-target")));
|
|
49
|
+
if (exists) {
|
|
50
|
+
const unchanged = yield* targetHasContent(fs, targetPath, content).pipe(Effect.mapError(fail("read-target")));
|
|
51
|
+
if (unchanged)
|
|
52
|
+
return "skipped";
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
else if (options.skipIfUnchanged === "ignore-read-errors") {
|
|
56
|
+
const unchanged = yield* targetHasContent(fs, targetPath, content).pipe(Effect.option);
|
|
57
|
+
if (Option.isSome(unchanged) && unchanged.value)
|
|
58
|
+
return "skipped";
|
|
59
|
+
}
|
|
60
|
+
yield* Effect.gen(function* () {
|
|
61
|
+
yield* (typeof content === "string"
|
|
62
|
+
? fs.writeFileString(tempPath, content)
|
|
63
|
+
: fs.writeFile(tempPath, content)).pipe(Effect.mapError(fail("write-temp")));
|
|
64
|
+
if (options.removeTargetBeforeRename === true) {
|
|
65
|
+
yield* fs.remove(targetPath).pipe(Effect.ignore);
|
|
66
|
+
}
|
|
67
|
+
yield* fs.rename(tempPath, targetPath).pipe(Effect.mapError(fail("rename")));
|
|
68
|
+
}).pipe(Effect.ensuring(fs.remove(tempPath, { force: true }).pipe(Effect.ignore)));
|
|
69
|
+
return "written";
|
|
70
|
+
});
|
|
71
|
+
//# sourceMappingURL=atomic-write.js.map
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ambient transaction context and the write registration primitives
|
|
3
|
+
* every workspace writer calls.
|
|
4
|
+
*
|
|
5
|
+
* `protectWorkspacePath` and `protectCreatedAncestors` snapshot a path before
|
|
6
|
+
* its first mutation within the active closure. The context itself — the
|
|
7
|
+
* ledger reference and the two ambient references that carry it — is
|
|
8
|
+
* package-private: the transaction runner in `./transaction.ts` constructs
|
|
9
|
+
* and provides it, and nothing outside this package can reach the ledger.
|
|
10
|
+
*
|
|
11
|
+
* @experimental This API is unstable and may change without notice.
|
|
12
|
+
*/
|
|
13
|
+
import * as ServiceMap from "effect/Context";
|
|
14
|
+
import * as Effect from "effect/Effect";
|
|
15
|
+
import * as FileSystem from "effect/FileSystem";
|
|
16
|
+
import * as Option from "effect/Option";
|
|
17
|
+
import * as Path from "effect/Path";
|
|
18
|
+
import * as SynchronizedRef from "effect/SynchronizedRef";
|
|
19
|
+
import { WorkspaceSnapshotError } from "./errors.js";
|
|
20
|
+
import { type TransactionLedger } from "./ledger.js";
|
|
21
|
+
export interface WorkspaceTransactionContext {
|
|
22
|
+
readonly isTransitionCompromised: () => boolean;
|
|
23
|
+
readonly fs: FileSystem.FileSystem;
|
|
24
|
+
readonly path: Path.Path;
|
|
25
|
+
readonly workspaceDir: string;
|
|
26
|
+
/** The one serialized record of preimages, closures, and pending restorations. */
|
|
27
|
+
readonly ledger: SynchronizedRef.SynchronizedRef<TransactionLedger>;
|
|
28
|
+
}
|
|
29
|
+
/** The active transaction, when one is running on this fiber's context. */
|
|
30
|
+
export declare const CurrentWorkspaceTransaction: ServiceMap.Reference<Option.Option<WorkspaceTransactionContext>>;
|
|
31
|
+
/**
|
|
32
|
+
* The semantic closure whose mutations are currently executing. Snapshots
|
|
33
|
+
* taken while a closure is active belong to it: they are dropped when the
|
|
34
|
+
* closure settles and restored when it — and only it — rolls back. Snapshots
|
|
35
|
+
* taken outside any closure belong to the operation closure and are restored
|
|
36
|
+
* by the transaction's own failure handling.
|
|
37
|
+
*/
|
|
38
|
+
export declare const CurrentWorkspaceClosure: ServiceMap.Reference<string | undefined>;
|
|
39
|
+
/**
|
|
40
|
+
* Take one target's preimage into the ledger, deduplicating on first touch
|
|
41
|
+
* per closure. One serialized ledger transition: the snapshot store is
|
|
42
|
+
* created, the backup name allocated, and the bytes copied while the ledger
|
|
43
|
+
* is held, so no concurrent registration or rollback observes a half-taken
|
|
44
|
+
* snapshot.
|
|
45
|
+
*/
|
|
46
|
+
export declare const protectInContext: (context: WorkspaceTransactionContext, target: string, closure: string | undefined) => Effect.Effect<void, WorkspaceSnapshotError>;
|
|
47
|
+
/** Snapshot a path before its first mutation when a workspace transaction is active. */
|
|
48
|
+
export declare const protectWorkspacePath: (target: string) => Effect.Effect<void, WorkspaceSnapshotError>;
|
|
49
|
+
/**
|
|
50
|
+
* Protect the first ancestor a recursive directory creation is about to
|
|
51
|
+
* create, so restoration removes the created directory chain instead of
|
|
52
|
+
* leaving empty parents behind. No-op outside a transaction or when the
|
|
53
|
+
* directory already exists.
|
|
54
|
+
*/
|
|
55
|
+
export declare const protectCreatedAncestors: (fs: FileSystem.FileSystem, path: Path.Path, directory: string) => Effect.Effect<void, WorkspaceSnapshotError>;
|
|
56
|
+
//# sourceMappingURL=context.d.ts.map
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ambient transaction context and the write registration primitives
|
|
3
|
+
* every workspace writer calls.
|
|
4
|
+
*
|
|
5
|
+
* `protectWorkspacePath` and `protectCreatedAncestors` snapshot a path before
|
|
6
|
+
* its first mutation within the active closure. The context itself — the
|
|
7
|
+
* ledger reference and the two ambient references that carry it — is
|
|
8
|
+
* package-private: the transaction runner in `./transaction.ts` constructs
|
|
9
|
+
* and provides it, and nothing outside this package can reach the ledger.
|
|
10
|
+
*
|
|
11
|
+
* @experimental This API is unstable and may change without notice.
|
|
12
|
+
*/
|
|
13
|
+
import * as ServiceMap from "effect/Context";
|
|
14
|
+
import * as Effect from "effect/Effect";
|
|
15
|
+
import * as FileSystem from "effect/FileSystem";
|
|
16
|
+
import * as Option from "effect/Option";
|
|
17
|
+
import * as Path from "effect/Path";
|
|
18
|
+
import * as SynchronizedRef from "effect/SynchronizedRef";
|
|
19
|
+
import { WorkspaceSnapshotError } from "./errors.js";
|
|
20
|
+
import { isProtected, withSnapshot } from "./ledger.js";
|
|
21
|
+
/** The active transaction, when one is running on this fiber's context. */
|
|
22
|
+
export const CurrentWorkspaceTransaction = ServiceMap.Reference("@agentxm/workspace-transactions/CurrentWorkspaceTransaction", {
|
|
23
|
+
defaultValue: () => Option.none(),
|
|
24
|
+
});
|
|
25
|
+
/**
|
|
26
|
+
* The semantic closure whose mutations are currently executing. Snapshots
|
|
27
|
+
* taken while a closure is active belong to it: they are dropped when the
|
|
28
|
+
* closure settles and restored when it — and only it — rolls back. Snapshots
|
|
29
|
+
* taken outside any closure belong to the operation closure and are restored
|
|
30
|
+
* by the transaction's own failure handling.
|
|
31
|
+
*/
|
|
32
|
+
export const CurrentWorkspaceClosure = ServiceMap.Reference("@agentxm/workspace-transactions/CurrentWorkspaceClosure", { defaultValue: () => undefined });
|
|
33
|
+
/**
|
|
34
|
+
* Take one target's preimage into the ledger, deduplicating on first touch
|
|
35
|
+
* per closure. One serialized ledger transition: the snapshot store is
|
|
36
|
+
* created, the backup name allocated, and the bytes copied while the ledger
|
|
37
|
+
* is held, so no concurrent registration or rollback observes a half-taken
|
|
38
|
+
* snapshot.
|
|
39
|
+
*/
|
|
40
|
+
export const protectInContext = (context, target, closure) => SynchronizedRef.updateEffect(context.ledger, (ledger) => Effect.gen(function* () {
|
|
41
|
+
const { fs, path } = context;
|
|
42
|
+
const normalized = path.resolve(target);
|
|
43
|
+
// First-touch dedupe is per closure: a later closure touching a target
|
|
44
|
+
// an earlier closure already committed needs its own — post-commit —
|
|
45
|
+
// preimage, so restoring it undoes only that closure's work.
|
|
46
|
+
if (isProtected(ledger, closure, normalized))
|
|
47
|
+
return ledger;
|
|
48
|
+
const link = yield* fs.readLink(normalized).pipe(Effect.option);
|
|
49
|
+
if (Option.isSome(link)) {
|
|
50
|
+
const snapshot = {
|
|
51
|
+
closure,
|
|
52
|
+
target: normalized,
|
|
53
|
+
state: "symlink",
|
|
54
|
+
linkTarget: link.value,
|
|
55
|
+
};
|
|
56
|
+
return withSnapshot(ledger, snapshot, ledger);
|
|
57
|
+
}
|
|
58
|
+
const exists = yield* fs
|
|
59
|
+
.exists(normalized)
|
|
60
|
+
.pipe(Effect.mapError((cause) => new WorkspaceSnapshotError({ target: normalized, step: "inspect-target", cause })));
|
|
61
|
+
if (!exists) {
|
|
62
|
+
return withSnapshot(ledger, { closure, target: normalized, state: "absent" }, ledger);
|
|
63
|
+
}
|
|
64
|
+
const snapshotDir = ledger.snapshotDir ??
|
|
65
|
+
(yield* fs
|
|
66
|
+
.makeTempDirectory({ prefix: "axm-rollback-" })
|
|
67
|
+
.pipe(Effect.mapError((cause) => new WorkspaceSnapshotError({ target: normalized, step: "create-store", cause }))));
|
|
68
|
+
const backup = path.join(snapshotDir, `${ledger.snapshotSequence}.snap`);
|
|
69
|
+
// The pre-change bytes are preserved before the path is first mutated;
|
|
70
|
+
// a path that cannot be snapshotted is never mutated.
|
|
71
|
+
yield* fs
|
|
72
|
+
.copy(normalized, backup, { preserveTimestamps: true })
|
|
73
|
+
.pipe(Effect.mapError((cause) => new WorkspaceSnapshotError({ target: normalized, step: "copy", cause })));
|
|
74
|
+
return withSnapshot(ledger, { closure, target: normalized, state: "copied", backup }, { snapshotDir, snapshotSequence: ledger.snapshotSequence + 1 });
|
|
75
|
+
}));
|
|
76
|
+
/** Snapshot a path before its first mutation when a workspace transaction is active. */
|
|
77
|
+
export const protectWorkspacePath = (target) => Effect.gen(function* () {
|
|
78
|
+
const current = yield* CurrentWorkspaceTransaction;
|
|
79
|
+
if (Option.isNone(current))
|
|
80
|
+
return;
|
|
81
|
+
const closure = yield* CurrentWorkspaceClosure;
|
|
82
|
+
yield* protectInContext(current.value, target, closure);
|
|
83
|
+
});
|
|
84
|
+
/**
|
|
85
|
+
* Protect the first ancestor a recursive directory creation is about to
|
|
86
|
+
* create, so restoration removes the created directory chain instead of
|
|
87
|
+
* leaving empty parents behind. No-op outside a transaction or when the
|
|
88
|
+
* directory already exists.
|
|
89
|
+
*/
|
|
90
|
+
export const protectCreatedAncestors = (fs, path, directory) => CurrentWorkspaceTransaction.pipe(Effect.flatMap(Option.match({
|
|
91
|
+
onNone: () => Effect.void,
|
|
92
|
+
onSome: (context) => Effect.gen(function* () {
|
|
93
|
+
let firstMissing;
|
|
94
|
+
let current = path.resolve(directory);
|
|
95
|
+
while (true) {
|
|
96
|
+
const exists = yield* fs.exists(current).pipe(Effect.mapError((cause) => new WorkspaceSnapshotError({
|
|
97
|
+
target: current,
|
|
98
|
+
step: "inspect-ancestor",
|
|
99
|
+
cause,
|
|
100
|
+
})));
|
|
101
|
+
if (exists)
|
|
102
|
+
break;
|
|
103
|
+
firstMissing = current;
|
|
104
|
+
const parent = path.dirname(current);
|
|
105
|
+
if (parent === current)
|
|
106
|
+
break;
|
|
107
|
+
current = parent;
|
|
108
|
+
}
|
|
109
|
+
if (firstMissing !== undefined) {
|
|
110
|
+
const closure = yield* CurrentWorkspaceClosure;
|
|
111
|
+
yield* protectInContext(context, firstMissing, closure);
|
|
112
|
+
}
|
|
113
|
+
}),
|
|
114
|
+
})));
|
|
115
|
+
//# sourceMappingURL=context.js.map
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed failure vocabulary of the workspace transaction and transition-lock
|
|
3
|
+
* machinery, plus the holder and contention facts the lock records. The
|
|
4
|
+
* application error boundary owns rendering, codes, and suggestions for every
|
|
5
|
+
* failure declared here.
|
|
6
|
+
*
|
|
7
|
+
* @experimental This API is unstable and may change without notice.
|
|
8
|
+
*/
|
|
9
|
+
import type * as Cause from "effect/Cause";
|
|
10
|
+
import type * as Option from "effect/Option";
|
|
11
|
+
/** Identity an invocation records while it holds the workspace transition. */
|
|
12
|
+
export interface TransitionLockHolder {
|
|
13
|
+
readonly command: string;
|
|
14
|
+
readonly pid: number;
|
|
15
|
+
readonly candidateId?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface TransitionContention {
|
|
18
|
+
/** The holder recorded by the invocation that owns the lock, when readable. */
|
|
19
|
+
readonly holder: Option.Option<TransitionLockHolder>;
|
|
20
|
+
readonly waitedMillis: number;
|
|
21
|
+
}
|
|
22
|
+
declare const WorkspaceRestorationIncomplete_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => Cause.YieldableError & {
|
|
23
|
+
readonly _tag: "WorkspaceRestorationIncomplete";
|
|
24
|
+
} & Readonly<A>;
|
|
25
|
+
/**
|
|
26
|
+
* Restoration did not complete: the typed fact the terminal resolution
|
|
27
|
+
* derives outcome, disposition, and exit status from. The pre-change
|
|
28
|
+
* snapshots survive in the OS-temporary snapshot directory; nothing about
|
|
29
|
+
* this failure persists in the workspace, and the next mutation converges
|
|
30
|
+
* from the current workspace state.
|
|
31
|
+
*/
|
|
32
|
+
export declare class WorkspaceRestorationIncomplete extends WorkspaceRestorationIncomplete_base<{
|
|
33
|
+
readonly terminationCause: "failure" | "interruption";
|
|
34
|
+
readonly transitionCause: Cause.Cause<unknown>;
|
|
35
|
+
readonly restorationCause: unknown;
|
|
36
|
+
/** OS-temporary directory preserving the pre-change snapshots, when any were taken. */
|
|
37
|
+
readonly snapshotDir: string | undefined;
|
|
38
|
+
/** Protected paths, workspace-root-relative where possible, left as the failure left them. */
|
|
39
|
+
readonly retained: ReadonlyArray<string>;
|
|
40
|
+
/** Closures whose rollback did not complete, when closure-scoped. */
|
|
41
|
+
readonly closureIds?: ReadonlyArray<string>;
|
|
42
|
+
}> {
|
|
43
|
+
}
|
|
44
|
+
declare const WorkspaceSnapshotError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => Cause.YieldableError & {
|
|
45
|
+
readonly _tag: "WorkspaceSnapshotError";
|
|
46
|
+
} & Readonly<A>;
|
|
47
|
+
/**
|
|
48
|
+
* Registering a path with the active transaction failed: the pre-mutation
|
|
49
|
+
* preimage could not be taken, so the path was never mutated. `target` is the
|
|
50
|
+
* path being protected; the `create-store` step's message interpolates
|
|
51
|
+
* nothing.
|
|
52
|
+
*/
|
|
53
|
+
export declare class WorkspaceSnapshotError extends WorkspaceSnapshotError_base<{
|
|
54
|
+
readonly target: string;
|
|
55
|
+
readonly step: "inspect-target" | "create-store" | "copy" | "inspect-ancestor";
|
|
56
|
+
readonly cause: unknown;
|
|
57
|
+
}> {
|
|
58
|
+
}
|
|
59
|
+
declare const WorkspaceDirectoryError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => Cause.YieldableError & {
|
|
60
|
+
readonly _tag: "WorkspaceDirectoryError";
|
|
61
|
+
} & Readonly<A>;
|
|
62
|
+
/** Preparing the workspace state directory for a transition failed. */
|
|
63
|
+
export declare class WorkspaceDirectoryError extends WorkspaceDirectoryError_base<{
|
|
64
|
+
readonly path: string;
|
|
65
|
+
readonly step: "inspect" | "create";
|
|
66
|
+
readonly cause: unknown;
|
|
67
|
+
}> {
|
|
68
|
+
}
|
|
69
|
+
declare const TransitionLockError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => Cause.YieldableError & {
|
|
70
|
+
readonly _tag: "TransitionLockError";
|
|
71
|
+
} & Readonly<A>;
|
|
72
|
+
/**
|
|
73
|
+
* Workspace transition-lock mechanics failed. `path` carries the fact each
|
|
74
|
+
* step's message interpolates: the scratch directory for `create-scratch`,
|
|
75
|
+
* the lock path otherwise. `missing-timestamp` has no underlying cause.
|
|
76
|
+
*/
|
|
77
|
+
export declare class TransitionLockError extends TransitionLockError_base<{
|
|
78
|
+
readonly path: string;
|
|
79
|
+
readonly step: "create-scratch" | "acquire" | "record-holder" | "inspect-timestamp" | "missing-timestamp" | "preserve-timestamp" | "release";
|
|
80
|
+
readonly cause?: unknown;
|
|
81
|
+
}> {
|
|
82
|
+
}
|
|
83
|
+
declare const TransitionLockUnavailable_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => Cause.YieldableError & {
|
|
84
|
+
readonly _tag: "TransitionLockUnavailable";
|
|
85
|
+
} & Readonly<A>;
|
|
86
|
+
/**
|
|
87
|
+
* The bounded contention wait elapsed while another invocation held the
|
|
88
|
+
* workspace transition.
|
|
89
|
+
*/
|
|
90
|
+
export declare class TransitionLockUnavailable extends TransitionLockUnavailable_base<{
|
|
91
|
+
readonly holder: TransitionLockHolder | undefined;
|
|
92
|
+
readonly waitedMillis: number;
|
|
93
|
+
}> {
|
|
94
|
+
}
|
|
95
|
+
declare const WorkspaceTransitionCompromised_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => Cause.YieldableError & {
|
|
96
|
+
readonly _tag: "WorkspaceTransitionCompromised";
|
|
97
|
+
} & Readonly<A>;
|
|
98
|
+
/**
|
|
99
|
+
* The hold is no longer provably owned: ownership could not be confirmed
|
|
100
|
+
* within the staleness window, so a contender may already have reclaimed the
|
|
101
|
+
* lock. Any further durable write by the original owner — mutation and
|
|
102
|
+
* restoration alike — could overwrite a successor's work.
|
|
103
|
+
*/
|
|
104
|
+
export declare class WorkspaceTransitionCompromised extends WorkspaceTransitionCompromised_base<{
|
|
105
|
+
readonly workspaceDir: string;
|
|
106
|
+
readonly lockPath: string;
|
|
107
|
+
readonly cause: unknown;
|
|
108
|
+
}> {
|
|
109
|
+
}
|
|
110
|
+
declare const WorkspaceRestorationError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => Cause.YieldableError & {
|
|
111
|
+
readonly _tag: "WorkspaceRestorationError";
|
|
112
|
+
} & Readonly<A>;
|
|
113
|
+
/**
|
|
114
|
+
* One restoration step did not complete or verify. Never a channel failure:
|
|
115
|
+
* it travels as the `restorationCause` inside
|
|
116
|
+
* {@link WorkspaceRestorationIncomplete} and the pending closure records.
|
|
117
|
+
*/
|
|
118
|
+
export declare class WorkspaceRestorationError extends WorkspaceRestorationError_base<{
|
|
119
|
+
readonly target: string;
|
|
120
|
+
readonly step: "stage" | "stopped" | "verify";
|
|
121
|
+
readonly cause: unknown;
|
|
122
|
+
}> {
|
|
123
|
+
}
|
|
124
|
+
/** Failures acquiring the workspace transition lock. */
|
|
125
|
+
export type WorkspaceTransitionAcquireFailure = WorkspaceDirectoryError | TransitionLockError;
|
|
126
|
+
/** Failures the transaction machinery itself produces, beside the transition's own. */
|
|
127
|
+
export type WorkspaceTransactionFailure = WorkspaceSnapshotError | WorkspaceDirectoryError | TransitionLockError | TransitionLockUnavailable | WorkspaceTransitionCompromised;
|
|
128
|
+
export {};
|
|
129
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed failure vocabulary of the workspace transaction and transition-lock
|
|
3
|
+
* machinery, plus the holder and contention facts the lock records. The
|
|
4
|
+
* application error boundary owns rendering, codes, and suggestions for every
|
|
5
|
+
* failure declared here.
|
|
6
|
+
*
|
|
7
|
+
* @experimental This API is unstable and may change without notice.
|
|
8
|
+
*/
|
|
9
|
+
import * as Data from "effect/Data";
|
|
10
|
+
/**
|
|
11
|
+
* Restoration did not complete: the typed fact the terminal resolution
|
|
12
|
+
* derives outcome, disposition, and exit status from. The pre-change
|
|
13
|
+
* snapshots survive in the OS-temporary snapshot directory; nothing about
|
|
14
|
+
* this failure persists in the workspace, and the next mutation converges
|
|
15
|
+
* from the current workspace state.
|
|
16
|
+
*/
|
|
17
|
+
export class WorkspaceRestorationIncomplete extends Data.TaggedError("WorkspaceRestorationIncomplete") {
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Registering a path with the active transaction failed: the pre-mutation
|
|
21
|
+
* preimage could not be taken, so the path was never mutated. `target` is the
|
|
22
|
+
* path being protected; the `create-store` step's message interpolates
|
|
23
|
+
* nothing.
|
|
24
|
+
*/
|
|
25
|
+
export class WorkspaceSnapshotError extends Data.TaggedError("WorkspaceSnapshotError") {
|
|
26
|
+
}
|
|
27
|
+
/** Preparing the workspace state directory for a transition failed. */
|
|
28
|
+
export class WorkspaceDirectoryError extends Data.TaggedError("WorkspaceDirectoryError") {
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Workspace transition-lock mechanics failed. `path` carries the fact each
|
|
32
|
+
* step's message interpolates: the scratch directory for `create-scratch`,
|
|
33
|
+
* the lock path otherwise. `missing-timestamp` has no underlying cause.
|
|
34
|
+
*/
|
|
35
|
+
export class TransitionLockError extends Data.TaggedError("TransitionLockError") {
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The bounded contention wait elapsed while another invocation held the
|
|
39
|
+
* workspace transition.
|
|
40
|
+
*/
|
|
41
|
+
export class TransitionLockUnavailable extends Data.TaggedError("TransitionLockUnavailable") {
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The hold is no longer provably owned: ownership could not be confirmed
|
|
45
|
+
* within the staleness window, so a contender may already have reclaimed the
|
|
46
|
+
* lock. Any further durable write by the original owner — mutation and
|
|
47
|
+
* restoration alike — could overwrite a successor's work.
|
|
48
|
+
*/
|
|
49
|
+
export class WorkspaceTransitionCompromised extends Data.TaggedError("WorkspaceTransitionCompromised") {
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* One restoration step did not complete or verify. Never a channel failure:
|
|
53
|
+
* it travels as the `restorationCause` inside
|
|
54
|
+
* {@link WorkspaceRestorationIncomplete} and the pending closure records.
|
|
55
|
+
*/
|
|
56
|
+
export class WorkspaceRestorationError extends Data.TaggedError("WorkspaceRestorationError") {
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Footprint recorder — observed durable changes.
|
|
3
|
+
*
|
|
4
|
+
* The layers that touch durable state record what they actually committed,
|
|
5
|
+
* removed, or restored here; the operation resolution's footprint is these
|
|
6
|
+
* observations, not planner-claimed artifact paths. Recording is a no-op when
|
|
7
|
+
* no recorder is provided, so the writers stay usable outside an operation
|
|
8
|
+
* boundary; reading requires the recorder, because only an operation
|
|
9
|
+
* boundary has a footprint to report.
|
|
10
|
+
*
|
|
11
|
+
* @experimental This API is unstable and may change without notice.
|
|
12
|
+
*/
|
|
13
|
+
import * as Effect from "effect/Effect";
|
|
14
|
+
import * as Ref from "effect/Ref";
|
|
15
|
+
import * as ServiceMap from "effect/Context";
|
|
16
|
+
export interface FootprintObservation {
|
|
17
|
+
/** Absolute path of the durable change. */
|
|
18
|
+
readonly path: string;
|
|
19
|
+
readonly change: "created" | "modified" | "removed" | "restored";
|
|
20
|
+
}
|
|
21
|
+
declare const FootprintRecorder_base: ServiceMap.ServiceClass<FootprintRecorder, "@agentxm/workspace-transactions/FootprintRecorder", {
|
|
22
|
+
readonly ref: Ref.Ref<ReadonlyArray<FootprintObservation>>;
|
|
23
|
+
}>;
|
|
24
|
+
export declare class FootprintRecorder extends FootprintRecorder_base {
|
|
25
|
+
}
|
|
26
|
+
/** Record one observed durable change. No-op without a recorder. */
|
|
27
|
+
export declare const recordFootprint: (observation: FootprintObservation) => Effect.Effect<void>;
|
|
28
|
+
/** Every durable change observed so far by the operation's recorder. */
|
|
29
|
+
export declare const readFootprint: Effect.Effect<ReadonlyArray<FootprintObservation>, never, FootprintRecorder>;
|
|
30
|
+
export declare const makeFootprintRecorder: Effect.Effect<ServiceMap.Service.Shape<typeof FootprintRecorder>>;
|
|
31
|
+
export {};
|
|
32
|
+
//# sourceMappingURL=footprint-recorder.d.ts.map
|