agentera 3.0.0-dev.23 → 3.0.0-dev.25
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/README.md +2 -2
- package/bundle/CHANGELOG.md +13 -5
- package/bundle/README.md +9 -8
- package/bundle/UPGRADE.md +10 -24
- package/bundle/agents/openai.yaml +1 -1
- package/bundle/references/adapters/package-manifest-interface-model.yaml +2 -0
- package/bundle/references/adapters/package-registry.yaml +9 -0
- package/bundle/references/artifacts/state-storage-authority.yaml +733 -12
- package/bundle/references/cli/agent-ready-state-contract.yaml +3 -3
- package/bundle/references/cli/prime-consumer-compatibility.yaml +2 -2
- package/bundle/references/cli/update-channels.yaml +4 -2
- package/bundle/references/cli/vocabulary.md +4 -4
- package/bundle/skills/agentera/SKILL.md +28 -14
- package/bundle/skills/agentera/capabilities/audit/schemas/artifacts.yaml +12 -12
- package/bundle/skills/agentera/capabilities/audit/schemas/validation.yaml +2 -2
- package/bundle/skills/agentera/capabilities/build/schemas/artifacts.yaml +12 -12
- package/bundle/skills/agentera/capabilities/build/schemas/validation.yaml +8 -8
- package/bundle/skills/agentera/capabilities/design/schemas/artifacts.yaml +6 -6
- package/bundle/skills/agentera/capabilities/discuss/schemas/artifacts.yaml +7 -7
- package/bundle/skills/agentera/capabilities/document/schemas/artifacts.yaml +12 -12
- package/bundle/skills/agentera/capabilities/optimize/schemas/artifacts.yaml +9 -9
- package/bundle/skills/agentera/capabilities/orchestrate/schemas/artifacts.yaml +11 -11
- package/bundle/skills/agentera/capabilities/plan/schemas/artifacts.yaml +9 -9
- package/bundle/skills/agentera/capabilities/profile/schemas/artifacts.yaml +2 -2
- package/bundle/skills/agentera/capabilities/research/schemas/artifacts.yaml +3 -3
- package/bundle/skills/agentera/capabilities/status/schemas/artifacts.yaml +11 -11
- package/bundle/skills/agentera/capabilities/vision/schemas/artifacts.yaml +8 -8
- package/bundle/skills/agentera/schemas/artifacts/decisions.yaml +15 -2
- package/bundle/skills/agentera/schemas/artifacts/docs.yaml +4 -1
- package/bundle/skills/agentera/schemas/artifacts/experiments.yaml +21 -21
- package/bundle/skills/agentera/schemas/artifacts/health.yaml +15 -2
- package/bundle/skills/agentera/schemas/artifacts/plan.yaml +36 -15
- package/bundle/skills/agentera/schemas/artifacts/progress.yaml +15 -2
- package/bundle/skills/agentera/schemas/artifacts/todo.yaml +4 -4
- package/dist/capabilities/audit/instructions.js +13 -1
- package/dist/capabilities/audit/instructions.js.map +1 -1
- package/dist/capabilities/build/instructions.js +14 -1
- package/dist/capabilities/build/instructions.js.map +1 -1
- package/dist/capabilities/discuss/instructions.js +11 -1
- package/dist/capabilities/discuss/instructions.js.map +1 -1
- package/dist/capabilities/document/instructions.js +9 -1
- package/dist/capabilities/document/instructions.js.map +1 -1
- package/dist/capabilities/index.js +7 -7
- package/dist/capabilities/index.js.map +1 -1
- package/dist/capabilities/optimize/instructions.js +10 -1
- package/dist/capabilities/optimize/instructions.js.map +1 -1
- package/dist/capabilities/orchestrate/instructions.js +13 -2
- package/dist/capabilities/orchestrate/instructions.js.map +1 -1
- package/dist/capabilities/plan/instructions.js +10 -1
- package/dist/capabilities/plan/instructions.js.map +1 -1
- package/dist/cli/capabilityContext/contract.js +10 -15
- package/dist/cli/capabilityContext/contract.js.map +1 -1
- package/dist/cli/capabilityContext/evidence.js +3 -3
- package/dist/cli/capabilityContext/evidence.js.map +1 -1
- package/dist/cli/capabilityContext/planState.js +5 -5
- package/dist/cli/capabilityContext/planState.js.map +1 -1
- package/dist/cli/capabilityContext/progress.js +1 -1
- package/dist/cli/capabilityContext/progress.js.map +1 -1
- package/dist/cli/capabilityContext/shared.js +8 -2
- package/dist/cli/capabilityContext/shared.js.map +1 -1
- package/dist/cli/capabilityContext/slim.js +2 -1
- package/dist/cli/capabilityContext/slim.js.map +1 -1
- package/dist/cli/capabilityContext/startup.js +18 -7
- package/dist/cli/capabilityContext/startup.js.map +1 -1
- package/dist/cli/capabilityContext/types.js +4 -4
- package/dist/cli/capabilityContext/types.js.map +1 -1
- package/dist/cli/commands/backfill.js +39 -2
- package/dist/cli/commands/backfill.js.map +1 -1
- package/dist/cli/commands/durability.js +34 -1
- package/dist/cli/commands/durability.js.map +1 -1
- package/dist/cli/commands/entityMigrate.js +128 -0
- package/dist/cli/commands/entityMigrate.js.map +1 -0
- package/dist/cli/commands/migrate.js +4 -0
- package/dist/cli/commands/migrate.js.map +1 -1
- package/dist/cli/commands/prime/briefOrientation.js +5 -3
- package/dist/cli/commands/prime/briefOrientation.js.map +1 -1
- package/dist/cli/commands/prime/collectEntityOrientation.js +148 -0
- package/dist/cli/commands/prime/collectEntityOrientation.js.map +1 -0
- package/dist/cli/commands/prime/collectOrientationState.js +21 -27
- package/dist/cli/commands/prime/collectOrientationState.js.map +1 -1
- package/dist/cli/commands/prime/orientationOutput.js +1 -1
- package/dist/cli/commands/prime/orientationOutput.js.map +1 -1
- package/dist/cli/commands/prime.js +2 -0
- package/dist/cli/commands/prime.js.map +1 -1
- package/dist/cli/commands/query.js +41 -15
- package/dist/cli/commands/query.js.map +1 -1
- package/dist/cli/commands/schema.js +7 -5
- package/dist/cli/commands/schema.js.map +1 -1
- package/dist/cli/commands/state/decisions.js +13 -0
- package/dist/cli/commands/state/decisions.js.map +1 -1
- package/dist/cli/commands/state/docs.js +38 -0
- package/dist/cli/commands/state/docs.js.map +1 -1
- package/dist/cli/commands/state/experimentRecords.js +82 -0
- package/dist/cli/commands/state/experimentRecords.js.map +1 -1
- package/dist/cli/commands/state/experiments.js +13 -0
- package/dist/cli/commands/state/experiments.js.map +1 -1
- package/dist/cli/commands/state/get.js +69 -2
- package/dist/cli/commands/state/get.js.map +1 -1
- package/dist/cli/commands/state/health.js +13 -0
- package/dist/cli/commands/state/health.js.map +1 -1
- package/dist/cli/commands/state/index.js +9 -0
- package/dist/cli/commands/state/index.js.map +1 -1
- package/dist/cli/commands/state/list.js +40 -7
- package/dist/cli/commands/state/list.js.map +1 -1
- package/dist/cli/commands/state/objective.js +24 -0
- package/dist/cli/commands/state/objective.js.map +1 -1
- package/dist/cli/commands/state/plan.js +14 -0
- package/dist/cli/commands/state/plan.js.map +1 -1
- package/dist/cli/commands/state/planTasks.js +38 -25
- package/dist/cli/commands/state/planTasks.js.map +1 -1
- package/dist/cli/commands/state/plans.js +32 -16
- package/dist/cli/commands/state/plans.js.map +1 -1
- package/dist/cli/commands/state/progress.js +12 -0
- package/dist/cli/commands/state/progress.js.map +1 -1
- package/dist/cli/commands/state/shared.js.map +1 -1
- package/dist/cli/commands/state/todo.js +30 -0
- package/dist/cli/commands/state/todo.js.map +1 -1
- package/dist/cli/commands/state/write.js +79 -4
- package/dist/cli/commands/state/write.js.map +1 -1
- package/dist/cli/commands/upgrade.js +65 -31
- package/dist/cli/commands/upgrade.js.map +1 -1
- package/dist/cli/commands/upgradeVerify.js +46 -14
- package/dist/cli/commands/upgradeVerify.js.map +1 -1
- package/dist/cli/commands/validate.js +65 -0
- package/dist/cli/commands/validate.js.map +1 -1
- package/dist/cli/dispatch/check.js +8 -1
- package/dist/cli/dispatch/check.js.map +1 -1
- package/dist/cli/dispatch/index.js +6 -0
- package/dist/cli/dispatch/index.js.map +1 -1
- package/dist/cli/dispatch/lifecycle.js +2 -5
- package/dist/cli/dispatch/lifecycle.js.map +1 -1
- package/dist/cli/dispatch/prime.js +15 -0
- package/dist/cli/dispatch/prime.js.map +1 -1
- package/dist/cli/dispatch/state.js +7 -1
- package/dist/cli/dispatch/state.js.map +1 -1
- package/dist/cli/help.js +78 -20
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/migrationRequired.js +84 -0
- package/dist/cli/migrationRequired.js.map +1 -0
- package/dist/cli/orientation/attention.js +2 -2
- package/dist/cli/orientation/attention.js.map +1 -1
- package/dist/cli/orientation.js +10 -7
- package/dist/cli/orientation.js.map +1 -1
- package/dist/cli/planArtifacts.js +192 -42
- package/dist/cli/planArtifacts.js.map +1 -1
- package/dist/cli/prime-blob.js +5 -5
- package/dist/cli/prime-blob.js.map +1 -1
- package/dist/cli/stateQuery.js +5 -5
- package/dist/cli/stateQuery.js.map +1 -1
- package/dist/hooks/compaction/status.js +25 -1
- package/dist/hooks/compaction/status.js.map +1 -1
- package/dist/hooks/cursorSessionStart.js +6 -0
- package/dist/hooks/cursorSessionStart.js.map +1 -1
- package/dist/hooks/sessionStart.js +62 -0
- package/dist/hooks/sessionStart.js.map +1 -1
- package/dist/registries/artifactRegistry.js +23 -14
- package/dist/registries/artifactRegistry.js.map +1 -1
- package/dist/registries/packageRegistry.js +1 -1
- package/dist/registries/packageRegistry.js.map +1 -1
- package/dist/state/archivePublication.js +3 -2
- package/dist/state/archivePublication.js.map +1 -1
- package/dist/state/decisionEntities.js +279 -0
- package/dist/state/decisionEntities.js.map +1 -0
- package/dist/state/decisionOverlay.js +2 -2
- package/dist/state/decisionOverlay.js.map +1 -1
- package/dist/state/decisionRevision.js +44 -34
- package/dist/state/decisionRevision.js.map +1 -1
- package/dist/state/directRetrieval.js.map +1 -1
- package/dist/state/durability.js +92 -1
- package/dist/state/durability.js.map +1 -1
- package/dist/state/entityCutover.js +390 -0
- package/dist/state/entityCutover.js.map +1 -0
- package/dist/state/entityCutoverGit.js +128 -0
- package/dist/state/entityCutoverGit.js.map +1 -0
- package/dist/state/entityMigrationPreview.js +803 -0
- package/dist/state/entityMigrationPreview.js.map +1 -0
- package/dist/state/entityPublicationContext.js +612 -0
- package/dist/state/entityPublicationContext.js.map +1 -0
- package/dist/state/entityStorage.js +807 -0
- package/dist/state/entityStorage.js.map +1 -0
- package/dist/state/experimentIdentity.js +23 -11
- package/dist/state/experimentIdentity.js.map +1 -1
- package/dist/state/gitBackfill.js.map +1 -1
- package/dist/state/healthEntities.js +227 -0
- package/dist/state/healthEntities.js.map +1 -0
- package/dist/state/healthEntityValidation.js +6 -0
- package/dist/state/healthEntityValidation.js.map +1 -0
- package/dist/state/legacyEntityCutoverEvidence.js +66 -0
- package/dist/state/legacyEntityCutoverEvidence.js.map +1 -0
- package/dist/state/listRetrieval.js.map +1 -1
- package/dist/state/objectiveExperimentEntities.js +321 -0
- package/dist/state/objectiveExperimentEntities.js.map +1 -0
- package/dist/state/planEntities.js +358 -0
- package/dist/state/planEntities.js.map +1 -0
- package/dist/state/progressEntities.js +382 -0
- package/dist/state/progressEntities.js.map +1 -0
- package/dist/state/projectRoot.js +63 -0
- package/dist/state/projectRoot.js.map +1 -0
- package/dist/state/projectionRecovery.js +458 -0
- package/dist/state/projectionRecovery.js.map +1 -0
- package/dist/state/retrievalAuthority.js +13 -6
- package/dist/state/retrievalAuthority.js.map +1 -1
- package/dist/state/safeProjectFile.js +153 -0
- package/dist/state/safeProjectFile.js.map +1 -0
- package/dist/state/stateMode.js +76 -0
- package/dist/state/stateMode.js.map +1 -0
- package/dist/state/todoDocsEntities.js +223 -0
- package/dist/state/todoDocsEntities.js.map +1 -0
- package/dist/state/todoDocsEntityValidation.js +26 -0
- package/dist/state/todoDocsEntityValidation.js.map +1 -0
- package/dist/state/write/candidateMutation.js +5 -55
- package/dist/state/write/candidateMutation.js.map +1 -1
- package/dist/state/write/explain.js +127 -28
- package/dist/state/write/explain.js.map +1 -1
- package/dist/state/write/lock.js +794 -87
- package/dist/state/write/lock.js.map +1 -1
- package/dist/state/write/mutation.js +5 -2
- package/dist/state/write/mutation.js.map +1 -1
- package/dist/state/write/operations.js +52 -7
- package/dist/state/write/operations.js.map +1 -1
- package/dist/state/write/planEvaluation.js +107 -0
- package/dist/state/write/planEvaluation.js.map +1 -0
- package/dist/state/write/planPublication.js +11 -55
- package/dist/state/write/planPublication.js.map +1 -1
- package/dist/state/write/transaction.js +88 -1
- package/dist/state/write/transaction.js.map +1 -1
- package/dist/upgrade/compatibility.js +4 -4
- package/dist/upgrade/compatibility.js.map +1 -1
- package/dist/upgrade/migrateArtifactsV2ToV3.js +13 -14
- package/dist/upgrade/migrateArtifactsV2ToV3.js.map +1 -1
- package/dist/upgrade/upgradeOrchestrator.js +126 -23
- package/dist/upgrade/upgradeOrchestrator.js.map +1 -1
- package/dist/validate/crossCapability.js +2 -2
- package/dist/validate/crossCapability.js.map +1 -1
- package/package.json +3 -3
- package/dist/upgrade/upgradeSnapshot.js +0 -93
- package/dist/upgrade/upgradeSnapshot.js.map +0 -1
package/dist/state/write/lock.js
CHANGED
|
@@ -1,131 +1,838 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import fs from "node:fs";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { assertRealpathBoundary } from "../../registries/artifactRegistry.js";
|
|
5
|
+
import { assertValidatedProjectRoot } from "../projectRoot.js";
|
|
4
6
|
import { reject } from "./errors.js";
|
|
5
7
|
const sleepArray = new Int32Array(new SharedArrayBuffer(4));
|
|
6
8
|
const INITIALIZATION_GRACE_MS = 250;
|
|
7
|
-
|
|
9
|
+
const OWNER_NAME = "owner.json";
|
|
10
|
+
const OWNER_PREPARATION_NAME = ".owner.json.tmp";
|
|
11
|
+
const CLAIM_NAME = ".reclaim.json";
|
|
12
|
+
const MAX_OWNER_BYTES = 8 * 1024;
|
|
13
|
+
const MAX_RECOVERY_ENTRIES = 128;
|
|
14
|
+
const DIRECTORY_FLAGS = fs.constants.O_RDONLY
|
|
15
|
+
| (fs.constants.O_DIRECTORY ?? 0)
|
|
16
|
+
| (fs.constants.O_NOFOLLOW ?? 0);
|
|
17
|
+
const FILE_FLAGS = fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0);
|
|
18
|
+
const PRIVATE_NAME = /^\.writer\.(?:(\d+)\.)?([0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})\.tmp$/;
|
|
19
|
+
const LEGACY_CLAIM_NAME = /^\.writer\.[0-9a-f-]{36}\.claim$/;
|
|
20
|
+
function lockRecord(value) {
|
|
21
|
+
if (value === null
|
|
22
|
+
|| typeof value !== "object"
|
|
23
|
+
|| !Number.isSafeInteger(value.pid)
|
|
24
|
+
|| value.pid <= 0
|
|
25
|
+
|| typeof value.token !== "string"
|
|
26
|
+
|| value.token.length === 0
|
|
27
|
+
|| typeof value.created_at !== "string")
|
|
28
|
+
return null;
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
function identity(fd) {
|
|
32
|
+
const stat = fs.fstatSync(fd, { bigint: true });
|
|
33
|
+
return { dev: stat.dev, ino: stat.ino };
|
|
34
|
+
}
|
|
35
|
+
function sameIdentity(left, right) {
|
|
36
|
+
return left.dev === right.dev && left.ino === right.ino;
|
|
37
|
+
}
|
|
38
|
+
function sameRecord(left, right) {
|
|
39
|
+
return left.pid === right.pid
|
|
40
|
+
&& left.token === right.token
|
|
41
|
+
&& left.created_at === right.created_at;
|
|
42
|
+
}
|
|
43
|
+
function fdPath(fd, name) {
|
|
44
|
+
return name ? `/proc/self/fd/${fd}/${name}` : `/proc/self/fd/${fd}`;
|
|
45
|
+
}
|
|
46
|
+
function pathMatches(pathname, expectedFd, flags) {
|
|
47
|
+
let observedFd;
|
|
8
48
|
try {
|
|
9
|
-
|
|
49
|
+
observedFd = fs.openSync(pathname, flags);
|
|
50
|
+
return sameIdentity(identity(observedFd), identity(expectedFd));
|
|
10
51
|
}
|
|
11
52
|
catch {
|
|
12
|
-
return
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
if (observedFd !== undefined)
|
|
57
|
+
fs.closeSync(observedFd);
|
|
13
58
|
}
|
|
14
59
|
}
|
|
15
|
-
function
|
|
60
|
+
function entryAbsent(directoryFd, name) {
|
|
16
61
|
try {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
62
|
+
fs.lstatSync(fdPath(directoryFd, name));
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
return error.code === "ENOENT";
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function readRecord(fd) {
|
|
70
|
+
try {
|
|
71
|
+
const stat = fs.fstatSync(fd, { bigint: true });
|
|
72
|
+
if (!stat.isFile() || stat.size > BigInt(MAX_OWNER_BYTES))
|
|
73
|
+
return null;
|
|
74
|
+
const size = Number(stat.size);
|
|
75
|
+
const bytes = Buffer.alloc(size);
|
|
76
|
+
let offset = 0;
|
|
77
|
+
while (offset < size) {
|
|
78
|
+
const read = fs.readSync(fd, bytes, offset, size - offset, offset);
|
|
79
|
+
if (read === 0)
|
|
80
|
+
break;
|
|
81
|
+
offset += read;
|
|
82
|
+
}
|
|
83
|
+
if (offset !== size)
|
|
84
|
+
return null;
|
|
85
|
+
return lockRecord(JSON.parse(bytes.toString("utf8")));
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function inspectRecord(directoryFd, name) {
|
|
92
|
+
let fd;
|
|
93
|
+
try {
|
|
94
|
+
fd = fs.openSync(fdPath(directoryFd, name), FILE_FLAGS);
|
|
95
|
+
const stat = fs.fstatSync(fd, { bigint: true });
|
|
96
|
+
const file = stat.isFile();
|
|
97
|
+
const record = file ? readRecord(fd) : null;
|
|
98
|
+
return record
|
|
99
|
+
? { state: "valid", stale: false, fd, file, record }
|
|
100
|
+
: {
|
|
101
|
+
state: "malformed",
|
|
102
|
+
stale: Date.now() - Number(stat.mtimeMs) >= INITIALIZATION_GRACE_MS,
|
|
103
|
+
fd,
|
|
104
|
+
file,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
if (fd !== undefined)
|
|
109
|
+
fs.closeSync(fd);
|
|
110
|
+
if (error.code !== "ENOENT") {
|
|
111
|
+
return { state: "malformed", stale: false, file: false };
|
|
112
|
+
}
|
|
113
|
+
const stat = fs.fstatSync(directoryFd, { bigint: true });
|
|
114
|
+
return {
|
|
115
|
+
state: "malformed",
|
|
116
|
+
stale: Date.now() - Number(stat.mtimeMs) >= INITIALIZATION_GRACE_MS,
|
|
117
|
+
file: false,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function inspectLock(lockPath) {
|
|
122
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
22
123
|
try {
|
|
23
|
-
|
|
24
|
-
return
|
|
124
|
+
const fd = fs.openSync(lockPath, DIRECTORY_FLAGS);
|
|
125
|
+
return { state: "directory", fd, owner: inspectRecord(fd, OWNER_NAME) };
|
|
25
126
|
}
|
|
26
127
|
catch (error) {
|
|
27
|
-
|
|
128
|
+
const code = error.code;
|
|
129
|
+
if (code === "ENOENT")
|
|
130
|
+
return { state: "absent" };
|
|
131
|
+
if (code !== "ENOTDIR" && code !== "ELOOP")
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
let stat;
|
|
135
|
+
try {
|
|
136
|
+
stat = fs.lstatSync(lockPath);
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
if (error.code === "ENOENT")
|
|
140
|
+
return { state: "absent" };
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
if (stat.isDirectory() && !stat.isSymbolicLink())
|
|
144
|
+
continue;
|
|
145
|
+
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
146
|
+
return { state: "unsupported", kind: stat.isSymbolicLink() ? "symbolic link" : "non-file entry" };
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
const fd = fs.openSync(lockPath, FILE_FLAGS);
|
|
150
|
+
const opened = fs.fstatSync(fd);
|
|
151
|
+
if (opened.isFile())
|
|
152
|
+
return { state: "legacy_file", fd };
|
|
153
|
+
fs.closeSync(fd);
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
const code = error.code;
|
|
157
|
+
if (code === "ENOENT" || code === "EISDIR" || code === "ENOTDIR")
|
|
158
|
+
continue;
|
|
159
|
+
throw error;
|
|
28
160
|
}
|
|
29
161
|
}
|
|
30
|
-
|
|
31
|
-
|
|
162
|
+
return { state: "unsupported", kind: "entry changing during inspection" };
|
|
163
|
+
}
|
|
164
|
+
function closeRecord(inspection) {
|
|
165
|
+
if (inspection.fd !== undefined)
|
|
166
|
+
fs.closeSync(inspection.fd);
|
|
167
|
+
}
|
|
168
|
+
function closeInspection(inspection) {
|
|
169
|
+
if (inspection.state === "directory") {
|
|
170
|
+
closeRecord(inspection.owner);
|
|
171
|
+
fs.closeSync(inspection.fd);
|
|
172
|
+
}
|
|
173
|
+
else if (inspection.state === "legacy_file") {
|
|
174
|
+
fs.closeSync(inspection.fd);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function pidState(pid) {
|
|
178
|
+
if (pid === process.pid)
|
|
179
|
+
return "live";
|
|
180
|
+
try {
|
|
181
|
+
process.kill(pid, 0);
|
|
182
|
+
return "live";
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
return error.code === "ESRCH" ? "dead" : "indeterminate";
|
|
32
186
|
}
|
|
33
187
|
}
|
|
188
|
+
function ownerIsDead(record) {
|
|
189
|
+
return pidState(record.pid) === "dead";
|
|
190
|
+
}
|
|
34
191
|
function syncDirectory(directory) {
|
|
35
|
-
const fd = fs.openSync(directory,
|
|
192
|
+
const fd = typeof directory === "number" ? directory : fs.openSync(directory, DIRECTORY_FLAGS);
|
|
36
193
|
try {
|
|
37
194
|
fs.fsyncSync(fd);
|
|
38
195
|
}
|
|
39
196
|
finally {
|
|
40
|
-
|
|
197
|
+
if (typeof directory !== "number")
|
|
198
|
+
fs.closeSync(fd);
|
|
41
199
|
}
|
|
42
200
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if (
|
|
201
|
+
function unlinkPinnedFile(directoryFd, name, expectedFd) {
|
|
202
|
+
if (!fs.fstatSync(expectedFd).isFile())
|
|
203
|
+
return false;
|
|
204
|
+
const target = fdPath(directoryFd, name);
|
|
205
|
+
if (!pathMatches(target, expectedFd, FILE_FLAGS))
|
|
206
|
+
return false;
|
|
207
|
+
try {
|
|
208
|
+
fs.unlinkSync(target);
|
|
209
|
+
syncDirectory(directoryFd);
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
if (error.code === "ENOENT")
|
|
214
|
+
return false;
|
|
215
|
+
throw error;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function removePinnedDirectory(parentFd, name, directoryFd) {
|
|
219
|
+
const target = fdPath(parentFd, name);
|
|
220
|
+
if (!pathMatches(target, directoryFd, DIRECTORY_FLAGS))
|
|
221
|
+
return false;
|
|
222
|
+
try {
|
|
223
|
+
fs.rmdirSync(target);
|
|
224
|
+
syncDirectory(parentFd);
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
if (["ENOENT", "ENOTEMPTY", "EEXIST"].includes(error.code ?? ""))
|
|
229
|
+
return false;
|
|
230
|
+
throw error;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
function recordCanBeRecovered(inspection, token) {
|
|
234
|
+
if (!inspection.file || inspection.fd === undefined)
|
|
235
|
+
return false;
|
|
236
|
+
if (inspection.state === "malformed")
|
|
237
|
+
return inspection.stale;
|
|
238
|
+
return (token === undefined || inspection.record.token === token) && ownerIsDead(inspection.record);
|
|
239
|
+
}
|
|
240
|
+
function recoverPrivateDirectory(parentFd, name, token, pid, transitionedTokens) {
|
|
241
|
+
// Preparation may pause indefinitely; only positive owner-death evidence permits mutation.
|
|
242
|
+
const privatePath = fdPath(parentFd, name);
|
|
243
|
+
let directoryFd;
|
|
244
|
+
try {
|
|
245
|
+
directoryFd = fs.openSync(privatePath, DIRECTORY_FLAGS);
|
|
246
|
+
const transitioned = transitionedTokens.has(token);
|
|
247
|
+
const namedPidState = pid === undefined ? undefined : pidState(pid);
|
|
248
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
249
|
+
const names = fs.readdirSync(fdPath(directoryFd));
|
|
250
|
+
if (!pathMatches(privatePath, directoryFd, DIRECTORY_FLAGS)) {
|
|
251
|
+
return entryAbsent(parentFd, name) ? "recovered" : "unknown";
|
|
252
|
+
}
|
|
253
|
+
if (names.length === 0) {
|
|
254
|
+
if (namedPidState === "live")
|
|
255
|
+
return "live";
|
|
256
|
+
if (!transitioned && namedPidState !== "dead")
|
|
257
|
+
return "unknown";
|
|
258
|
+
return removePinnedDirectory(parentFd, name, directoryFd) || entryAbsent(parentFd, name)
|
|
259
|
+
? "recovered"
|
|
260
|
+
: "unknown";
|
|
261
|
+
}
|
|
262
|
+
if (names.includes(OWNER_PREPARATION_NAME)) {
|
|
263
|
+
if (names.length !== 1)
|
|
264
|
+
return "unknown";
|
|
265
|
+
const preparation = inspectRecord(directoryFd, OWNER_PREPARATION_NAME);
|
|
93
266
|
try {
|
|
94
|
-
|
|
267
|
+
if (!preparation.file || preparation.fd === undefined) {
|
|
268
|
+
if (entryAbsent(directoryFd, OWNER_PREPARATION_NAME))
|
|
269
|
+
continue;
|
|
270
|
+
return "unknown";
|
|
271
|
+
}
|
|
272
|
+
if (preparation.state === "valid"
|
|
273
|
+
&& (preparation.record.token !== token || preparation.record.pid !== pid))
|
|
274
|
+
return "unknown";
|
|
275
|
+
if (namedPidState === "live")
|
|
276
|
+
return "live";
|
|
277
|
+
if (namedPidState !== "dead")
|
|
278
|
+
return "unknown";
|
|
279
|
+
unlinkPinnedFile(directoryFd, OWNER_PREPARATION_NAME, preparation.fd);
|
|
280
|
+
return removePinnedDirectory(parentFd, name, directoryFd) || entryAbsent(parentFd, name)
|
|
281
|
+
? "recovered"
|
|
282
|
+
: "unknown";
|
|
95
283
|
}
|
|
96
|
-
|
|
97
|
-
|
|
284
|
+
finally {
|
|
285
|
+
closeRecord(preparation);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
if (names.length > MAX_RECOVERY_ENTRIES
|
|
289
|
+
|| names.some((entry) => entry !== OWNER_NAME && !LEGACY_CLAIM_NAME.test(entry)))
|
|
290
|
+
return "unknown";
|
|
291
|
+
const records = names.map((entry) => ({ entry, inspection: inspectRecord(directoryFd, entry) }));
|
|
292
|
+
try {
|
|
293
|
+
const missing = records.filter(({ inspection }) => !inspection.file || inspection.fd === undefined);
|
|
294
|
+
if (missing.length > 0) {
|
|
295
|
+
if (missing.every(({ entry }) => entryAbsent(directoryFd, entry)))
|
|
296
|
+
continue;
|
|
297
|
+
return "unknown";
|
|
298
|
+
}
|
|
299
|
+
if (records.some(({ inspection }) => inspection.state === "valid" && inspection.record.token !== token)) {
|
|
300
|
+
return "unknown";
|
|
301
|
+
}
|
|
302
|
+
if (pid !== undefined
|
|
303
|
+
&& records.some(({ inspection }) => inspection.state === "valid" && inspection.record.pid !== pid))
|
|
304
|
+
return "unknown";
|
|
305
|
+
const validRecords = records.filter(({ inspection }) => inspection.state === "valid");
|
|
306
|
+
const recordPidStates = validRecords.map(({ inspection }) => pidState(inspection.record.pid));
|
|
307
|
+
if (namedPidState === "indeterminate" || recordPidStates.includes("indeterminate"))
|
|
308
|
+
return "unknown";
|
|
309
|
+
const fullyInitializedLivePreparation = pid !== undefined
|
|
310
|
+
&& namedPidState === "live"
|
|
311
|
+
&& records.every(({ inspection }) => inspection.state === "valid")
|
|
312
|
+
&& records.some(({ entry }) => entry === OWNER_NAME)
|
|
313
|
+
&& recordPidStates.every((state) => state === "live");
|
|
314
|
+
if (fullyInitializedLivePreparation)
|
|
315
|
+
return "live";
|
|
316
|
+
if (namedPidState === "live" || recordPidStates.includes("live"))
|
|
317
|
+
return "unknown";
|
|
318
|
+
if (!transitioned && (pid === undefined || namedPidState !== "dead"))
|
|
319
|
+
return "unknown";
|
|
320
|
+
for (const { entry, inspection } of records) {
|
|
321
|
+
unlinkPinnedFile(directoryFd, entry, inspection.fd);
|
|
322
|
+
}
|
|
323
|
+
return removePinnedDirectory(parentFd, name, directoryFd) || entryAbsent(parentFd, name)
|
|
324
|
+
? "recovered"
|
|
325
|
+
: "unknown";
|
|
326
|
+
}
|
|
327
|
+
finally {
|
|
328
|
+
for (const { inspection } of records)
|
|
329
|
+
closeRecord(inspection);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return "unknown";
|
|
333
|
+
}
|
|
334
|
+
catch (error) {
|
|
335
|
+
const code = error.code;
|
|
336
|
+
if (code === "ENOENT")
|
|
337
|
+
return "recovered";
|
|
338
|
+
if (code === "ENOTDIR" || code === "ELOOP")
|
|
339
|
+
return "unknown";
|
|
340
|
+
throw error;
|
|
341
|
+
}
|
|
342
|
+
finally {
|
|
343
|
+
if (directoryFd !== undefined)
|
|
344
|
+
fs.closeSync(directoryFd);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
function staleTransitionTokens(parentFd) {
|
|
348
|
+
const tokens = new Set();
|
|
349
|
+
let lockFd;
|
|
350
|
+
try {
|
|
351
|
+
lockFd = fs.openSync(fdPath(parentFd, ".writer.lock"), DIRECTORY_FLAGS);
|
|
352
|
+
for (const name of [OWNER_NAME, CLAIM_NAME]) {
|
|
353
|
+
const record = inspectRecord(lockFd, name);
|
|
354
|
+
try {
|
|
355
|
+
if (record.state === "valid" && ownerIsDead(record.record))
|
|
356
|
+
tokens.add(record.record.token);
|
|
357
|
+
}
|
|
358
|
+
finally {
|
|
359
|
+
closeRecord(record);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
catch (error) {
|
|
364
|
+
if (!["ENOENT", "ENOTDIR", "ELOOP"].includes(error.code ?? ""))
|
|
365
|
+
throw error;
|
|
366
|
+
}
|
|
367
|
+
finally {
|
|
368
|
+
if (lockFd !== undefined)
|
|
369
|
+
fs.closeSync(lockFd);
|
|
370
|
+
}
|
|
371
|
+
return tokens;
|
|
372
|
+
}
|
|
373
|
+
function recoverPreparedLocks(parentFd, lockPath) {
|
|
374
|
+
const candidates = fs.readdirSync(fdPath(parentFd), { withFileTypes: true })
|
|
375
|
+
.map((entry) => ({ entry, match: PRIVATE_NAME.exec(entry.name) }))
|
|
376
|
+
.filter(({ match }) => match !== null);
|
|
377
|
+
if (candidates.length > MAX_RECOVERY_ENTRIES) {
|
|
378
|
+
privateResidue(lockPath, candidates.map(({ entry }) => entry.name));
|
|
379
|
+
}
|
|
380
|
+
const transitionedTokens = staleTransitionTokens(parentFd);
|
|
381
|
+
const live = [];
|
|
382
|
+
const unresolved = [];
|
|
383
|
+
for (const { entry, match } of candidates) {
|
|
384
|
+
if (!entry.isDirectory() || entry.isSymbolicLink()) {
|
|
385
|
+
unresolved.push(entry.name);
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
const recovery = recoverPrivateDirectory(parentFd, entry.name, match[2], match[1] === undefined ? undefined : Number(match[1]), transitionedTokens);
|
|
389
|
+
if (recovery === "live")
|
|
390
|
+
live.push(entry.name);
|
|
391
|
+
if (recovery === "unknown")
|
|
392
|
+
unresolved.push(entry.name);
|
|
393
|
+
}
|
|
394
|
+
if (unresolved.length > 0)
|
|
395
|
+
privateResidue(lockPath, unresolved);
|
|
396
|
+
return live.length > 0;
|
|
397
|
+
}
|
|
398
|
+
function prepareLock(parentFd) {
|
|
399
|
+
const record = {
|
|
400
|
+
pid: process.pid,
|
|
401
|
+
token: randomUUID(),
|
|
402
|
+
created_at: new Date().toISOString(),
|
|
403
|
+
};
|
|
404
|
+
const name = `.writer.${record.pid}.${record.token}.tmp`;
|
|
405
|
+
const preparedPath = fdPath(parentFd, name);
|
|
406
|
+
let dirFd;
|
|
407
|
+
let ownerFd;
|
|
408
|
+
let ownerName = OWNER_PREPARATION_NAME;
|
|
409
|
+
try {
|
|
410
|
+
fs.mkdirSync(preparedPath, { mode: 0o700 });
|
|
411
|
+
dirFd = fs.openSync(preparedPath, DIRECTORY_FLAGS);
|
|
412
|
+
ownerFd = fs.openSync(fdPath(dirFd, ownerName), fs.constants.O_RDWR | fs.constants.O_CREAT | fs.constants.O_EXCL | (fs.constants.O_NOFOLLOW ?? 0), 0o600);
|
|
413
|
+
fs.writeFileSync(ownerFd, `${JSON.stringify(record)}\n`);
|
|
414
|
+
fs.fsyncSync(ownerFd);
|
|
415
|
+
const validated = readRecord(ownerFd);
|
|
416
|
+
if (validated === null || !sameRecord(validated, record)) {
|
|
417
|
+
throw new Error("writer owner metadata validation failed before publication");
|
|
418
|
+
}
|
|
419
|
+
fs.renameSync(fdPath(dirFd, ownerName), fdPath(dirFd, OWNER_NAME));
|
|
420
|
+
ownerName = OWNER_NAME;
|
|
421
|
+
syncDirectory(dirFd);
|
|
422
|
+
return { path: preparedPath, dirFd, ownerFd, record };
|
|
423
|
+
}
|
|
424
|
+
catch (error) {
|
|
425
|
+
if (dirFd !== undefined && ownerFd !== undefined) {
|
|
426
|
+
try {
|
|
427
|
+
unlinkPinnedFile(dirFd, ownerName, ownerFd);
|
|
428
|
+
}
|
|
429
|
+
catch {
|
|
430
|
+
// Preserve the acquisition error.
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
if (ownerFd !== undefined)
|
|
434
|
+
fs.closeSync(ownerFd);
|
|
435
|
+
if (dirFd !== undefined)
|
|
436
|
+
fs.closeSync(dirFd);
|
|
437
|
+
try {
|
|
438
|
+
fs.rmdirSync(preparedPath);
|
|
439
|
+
syncDirectory(parentFd);
|
|
440
|
+
}
|
|
441
|
+
catch {
|
|
442
|
+
// Preserve the acquisition error.
|
|
443
|
+
}
|
|
444
|
+
throw error;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
function disposePrepared(prepared, parentFd) {
|
|
448
|
+
if (prepared.ownerFd >= 0 && prepared.dirFd >= 0) {
|
|
449
|
+
try {
|
|
450
|
+
unlinkPinnedFile(prepared.dirFd, OWNER_NAME, prepared.ownerFd);
|
|
451
|
+
}
|
|
452
|
+
catch {
|
|
453
|
+
// The owner was moved or the private directory changed identity.
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
if (prepared.ownerFd >= 0) {
|
|
457
|
+
fs.closeSync(prepared.ownerFd);
|
|
458
|
+
prepared.ownerFd = -1;
|
|
459
|
+
}
|
|
460
|
+
const exactDirectory = prepared.dirFd >= 0
|
|
461
|
+
&& pathMatches(prepared.path, prepared.dirFd, DIRECTORY_FLAGS);
|
|
462
|
+
if (prepared.dirFd >= 0) {
|
|
463
|
+
fs.closeSync(prepared.dirFd);
|
|
464
|
+
prepared.dirFd = -1;
|
|
465
|
+
}
|
|
466
|
+
if (exactDirectory) {
|
|
467
|
+
try {
|
|
468
|
+
fs.rmdirSync(prepared.path);
|
|
469
|
+
syncDirectory(parentFd);
|
|
470
|
+
}
|
|
471
|
+
catch {
|
|
472
|
+
// A non-empty replacement is not this attempt's directory.
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
function verifyCanonicalLock(projectDirectory, parentFd, lockPath, lockFd, ownerFd, expected) {
|
|
477
|
+
if (!pathMatches(projectDirectory, parentFd, DIRECTORY_FLAGS))
|
|
478
|
+
return false;
|
|
479
|
+
let canonicalFd;
|
|
480
|
+
let canonicalOwnerFd;
|
|
481
|
+
try {
|
|
482
|
+
canonicalFd = fs.openSync(lockPath, DIRECTORY_FLAGS);
|
|
483
|
+
if (!sameIdentity(identity(canonicalFd), identity(lockFd)))
|
|
484
|
+
return false;
|
|
485
|
+
canonicalOwnerFd = fs.openSync(fdPath(canonicalFd, OWNER_NAME), FILE_FLAGS);
|
|
486
|
+
if (!sameIdentity(identity(canonicalOwnerFd), identity(ownerFd)))
|
|
487
|
+
return false;
|
|
488
|
+
const observed = readRecord(canonicalOwnerFd);
|
|
489
|
+
return observed !== null && sameRecord(observed, expected);
|
|
490
|
+
}
|
|
491
|
+
catch {
|
|
492
|
+
return false;
|
|
493
|
+
}
|
|
494
|
+
finally {
|
|
495
|
+
if (canonicalOwnerFd !== undefined)
|
|
496
|
+
fs.closeSync(canonicalOwnerFd);
|
|
497
|
+
if (canonicalFd !== undefined)
|
|
498
|
+
fs.closeSync(canonicalFd);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
function removeOwnedDirectory(parentFd, lockName, lockFd, ownerFd) {
|
|
502
|
+
if (!unlinkPinnedFile(lockFd, OWNER_NAME, ownerFd))
|
|
503
|
+
return false;
|
|
504
|
+
return removePinnedDirectory(parentFd, lockName, lockFd);
|
|
505
|
+
}
|
|
506
|
+
function writerLock(projectDirectory, lockPath, lockName, parentFd, lockFd, ownerFd, record, createdDirectory) {
|
|
507
|
+
let released = false;
|
|
508
|
+
return {
|
|
509
|
+
path: lockPath,
|
|
510
|
+
release: () => {
|
|
511
|
+
if (released)
|
|
512
|
+
return;
|
|
513
|
+
released = true;
|
|
514
|
+
try {
|
|
515
|
+
if (verifyCanonicalLock(projectDirectory, parentFd, lockPath, lockFd, ownerFd, record)) {
|
|
516
|
+
removeOwnedDirectory(parentFd, lockName, lockFd, ownerFd);
|
|
98
517
|
}
|
|
99
518
|
}
|
|
100
|
-
|
|
519
|
+
catch {
|
|
520
|
+
// A missing or replaced lock is not this owner's to remove.
|
|
521
|
+
}
|
|
522
|
+
fs.closeSync(ownerFd);
|
|
523
|
+
const exactDirectory = pathMatches(projectDirectory, parentFd, DIRECTORY_FLAGS);
|
|
524
|
+
fs.closeSync(lockFd);
|
|
525
|
+
fs.closeSync(parentFd);
|
|
526
|
+
if (createdDirectory && exactDirectory) {
|
|
101
527
|
try {
|
|
102
|
-
fs.
|
|
528
|
+
fs.rmdirSync(projectDirectory);
|
|
103
529
|
}
|
|
104
530
|
catch {
|
|
105
|
-
|
|
531
|
+
// A published artifact or peer now owns the directory.
|
|
106
532
|
}
|
|
107
533
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
534
|
+
},
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
function publishPrepared(prepared, projectDirectory, parentFd, lockPath, lockName, createdDirectory) {
|
|
538
|
+
try {
|
|
539
|
+
fs.renameSync(prepared.path, fdPath(parentFd, lockName));
|
|
540
|
+
}
|
|
541
|
+
catch (error) {
|
|
542
|
+
if (renameConflict(error))
|
|
543
|
+
return null;
|
|
544
|
+
throw error;
|
|
545
|
+
}
|
|
546
|
+
syncDirectory(parentFd);
|
|
547
|
+
if (verifyCanonicalLock(projectDirectory, parentFd, lockPath, prepared.dirFd, prepared.ownerFd, prepared.record)) {
|
|
548
|
+
return writerLock(projectDirectory, lockPath, lockName, parentFd, prepared.dirFd, prepared.ownerFd, prepared.record, createdDirectory);
|
|
549
|
+
}
|
|
550
|
+
try {
|
|
551
|
+
removeOwnedDirectory(parentFd, lockName, prepared.dirFd, prepared.ownerFd);
|
|
552
|
+
}
|
|
553
|
+
catch {
|
|
554
|
+
// Only the exact prepared instance may be cleaned.
|
|
555
|
+
}
|
|
556
|
+
fs.closeSync(prepared.ownerFd);
|
|
557
|
+
prepared.ownerFd = -1;
|
|
558
|
+
fs.closeSync(prepared.dirFd);
|
|
559
|
+
prepared.dirFd = -1;
|
|
560
|
+
return null;
|
|
561
|
+
}
|
|
562
|
+
function inspectedOwnerMatches(inspection) {
|
|
563
|
+
return inspection.owner.fd === undefined
|
|
564
|
+
? entryAbsent(inspection.fd, OWNER_NAME)
|
|
565
|
+
: pathMatches(fdPath(inspection.fd, OWNER_NAME), inspection.owner.fd, FILE_FLAGS);
|
|
566
|
+
}
|
|
567
|
+
function recoverLegacyClaims(inspection) {
|
|
568
|
+
const names = fs.readdirSync(fdPath(inspection.fd));
|
|
569
|
+
if (names.length > MAX_RECOVERY_ENTRIES)
|
|
570
|
+
return false;
|
|
571
|
+
for (const name of names.filter((entry) => LEGACY_CLAIM_NAME.test(entry))) {
|
|
572
|
+
const claim = inspectRecord(inspection.fd, name);
|
|
573
|
+
try {
|
|
574
|
+
if (!recordCanBeRecovered(claim))
|
|
575
|
+
return false;
|
|
576
|
+
unlinkPinnedFile(inspection.fd, name, claim.fd);
|
|
577
|
+
}
|
|
578
|
+
finally {
|
|
579
|
+
closeRecord(claim);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
return fs.readdirSync(fdPath(inspection.fd)).every((name) => name === OWNER_NAME || name === CLAIM_NAME);
|
|
583
|
+
}
|
|
584
|
+
function removeClaim(inspection, claim) {
|
|
585
|
+
const removed = unlinkPinnedFile(inspection.fd, CLAIM_NAME, claim.fd);
|
|
586
|
+
fs.closeSync(claim.fd);
|
|
587
|
+
claim.fd = -1;
|
|
588
|
+
return removed;
|
|
589
|
+
}
|
|
590
|
+
function claimInspectedDirectory(inspection, prepared, parentFd, lockName) {
|
|
591
|
+
const canonicalPath = fdPath(parentFd, lockName);
|
|
592
|
+
if (!pathMatches(canonicalPath, inspection.fd, DIRECTORY_FLAGS)
|
|
593
|
+
|| !inspectedOwnerMatches(inspection)
|
|
594
|
+
|| !recoverLegacyClaims(inspection))
|
|
595
|
+
return null;
|
|
596
|
+
const claimPath = fdPath(inspection.fd, CLAIM_NAME);
|
|
597
|
+
try {
|
|
598
|
+
// One fixed O_EXCL entry elects one reclaimer inside the exact directory inspected above.
|
|
599
|
+
fs.linkSync(fdPath(prepared.dirFd, OWNER_NAME), claimPath);
|
|
600
|
+
}
|
|
601
|
+
catch (error) {
|
|
602
|
+
if (error.code !== "EEXIST")
|
|
603
|
+
throw error;
|
|
604
|
+
const existing = inspectRecord(inspection.fd, CLAIM_NAME);
|
|
605
|
+
try {
|
|
606
|
+
if (recordCanBeRecovered(existing))
|
|
607
|
+
unlinkPinnedFile(inspection.fd, CLAIM_NAME, existing.fd);
|
|
608
|
+
}
|
|
609
|
+
finally {
|
|
610
|
+
closeRecord(existing);
|
|
611
|
+
}
|
|
612
|
+
return null;
|
|
613
|
+
}
|
|
614
|
+
let claimFd;
|
|
615
|
+
try {
|
|
616
|
+
claimFd = fs.openSync(claimPath, FILE_FLAGS);
|
|
617
|
+
if (!sameIdentity(identity(claimFd), identity(prepared.ownerFd))
|
|
618
|
+
|| fs.fstatSync(claimFd, { bigint: true }).nlink !== 2n) {
|
|
619
|
+
fs.closeSync(claimFd);
|
|
620
|
+
unlinkPinnedFile(inspection.fd, CLAIM_NAME, prepared.ownerFd);
|
|
621
|
+
return null;
|
|
622
|
+
}
|
|
623
|
+
const claim = { fd: claimFd };
|
|
624
|
+
syncDirectory(inspection.fd);
|
|
625
|
+
if (pathMatches(canonicalPath, inspection.fd, DIRECTORY_FLAGS)
|
|
626
|
+
&& pathMatches(claimPath, claimFd, FILE_FLAGS)
|
|
627
|
+
&& inspectedOwnerMatches(inspection))
|
|
628
|
+
return claim;
|
|
629
|
+
removeClaim(inspection, claim);
|
|
630
|
+
return null;
|
|
631
|
+
}
|
|
632
|
+
catch (error) {
|
|
633
|
+
if (claimFd !== undefined)
|
|
634
|
+
fs.closeSync(claimFd);
|
|
635
|
+
try {
|
|
636
|
+
unlinkPinnedFile(inspection.fd, CLAIM_NAME, prepared.ownerFd);
|
|
637
|
+
}
|
|
638
|
+
catch {
|
|
639
|
+
// Preserve the claim-publication error.
|
|
640
|
+
}
|
|
641
|
+
throw error;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
function adoptInspectedDirectory(inspection, prepared, projectDirectory, parentFd, lockPath, lockName, createdDirectory) {
|
|
645
|
+
const claim = claimInspectedDirectory(inspection, prepared, parentFd, lockName);
|
|
646
|
+
if (!claim)
|
|
647
|
+
return null;
|
|
648
|
+
let transitioned = false;
|
|
649
|
+
try {
|
|
650
|
+
if (!inspectedOwnerMatches(inspection)) {
|
|
651
|
+
removeClaim(inspection, claim);
|
|
652
|
+
return null;
|
|
653
|
+
}
|
|
654
|
+
fs.renameSync(fdPath(prepared.dirFd, OWNER_NAME), fdPath(inspection.fd, OWNER_NAME));
|
|
655
|
+
transitioned = true;
|
|
656
|
+
syncDirectory(inspection.fd);
|
|
657
|
+
syncDirectory(parentFd);
|
|
658
|
+
if (!verifyCanonicalLock(projectDirectory, parentFd, lockPath, inspection.fd, prepared.ownerFd, prepared.record)) {
|
|
659
|
+
unlinkPinnedFile(inspection.fd, OWNER_NAME, prepared.ownerFd);
|
|
660
|
+
removeClaim(inspection, claim);
|
|
661
|
+
return null;
|
|
662
|
+
}
|
|
663
|
+
closeRecord(inspection.owner);
|
|
664
|
+
inspection.owner.fd = undefined;
|
|
665
|
+
const exactPrivateDirectory = pathMatches(prepared.path, prepared.dirFd, DIRECTORY_FLAGS);
|
|
666
|
+
if (!exactPrivateDirectory || !removePinnedDirectory(parentFd, path.basename(prepared.path), prepared.dirFd)) {
|
|
667
|
+
unlinkPinnedFile(inspection.fd, OWNER_NAME, prepared.ownerFd);
|
|
668
|
+
removeClaim(inspection, claim);
|
|
669
|
+
return null;
|
|
670
|
+
}
|
|
671
|
+
fs.closeSync(prepared.dirFd);
|
|
672
|
+
prepared.dirFd = -1;
|
|
673
|
+
if (!removeClaim(inspection, claim)) {
|
|
674
|
+
unlinkPinnedFile(inspection.fd, OWNER_NAME, prepared.ownerFd);
|
|
675
|
+
return null;
|
|
676
|
+
}
|
|
677
|
+
if (!verifyCanonicalLock(projectDirectory, parentFd, lockPath, inspection.fd, prepared.ownerFd, prepared.record)) {
|
|
678
|
+
unlinkPinnedFile(inspection.fd, OWNER_NAME, prepared.ownerFd);
|
|
679
|
+
return null;
|
|
680
|
+
}
|
|
681
|
+
return writerLock(projectDirectory, lockPath, lockName, parentFd, inspection.fd, prepared.ownerFd, prepared.record, createdDirectory);
|
|
682
|
+
}
|
|
683
|
+
catch (error) {
|
|
684
|
+
if (transitioned) {
|
|
685
|
+
try {
|
|
686
|
+
unlinkPinnedFile(inspection.fd, OWNER_NAME, prepared.ownerFd);
|
|
687
|
+
}
|
|
688
|
+
catch {
|
|
689
|
+
// The exact transitioned owner is already absent.
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
if (claim.fd >= 0) {
|
|
693
|
+
try {
|
|
694
|
+
removeClaim(inspection, claim);
|
|
695
|
+
}
|
|
696
|
+
catch {
|
|
697
|
+
if (claim.fd >= 0)
|
|
698
|
+
fs.closeSync(claim.fd);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
throw error;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
function renameConflict(error) {
|
|
705
|
+
return ["EEXIST", "ENOTEMPTY", "ENOTDIR", "EISDIR"].includes(error.code ?? "");
|
|
706
|
+
}
|
|
707
|
+
function timeout(lockPath) {
|
|
708
|
+
reject({
|
|
709
|
+
class: "conflict",
|
|
710
|
+
message: `writer lock timeout at '${lockPath}'; retry the command after the active writer finishes`,
|
|
711
|
+
syntax: "agentera state <artifact-id> <verb> ... --format json",
|
|
712
|
+
example: "retry the same command after the active writer finishes",
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
function waitForContention(lockPath, deadline) {
|
|
716
|
+
const remaining = deadline - Date.now();
|
|
717
|
+
if (remaining <= 0)
|
|
718
|
+
timeout(lockPath);
|
|
719
|
+
Atomics.wait(sleepArray, 0, 0, Math.min(25, remaining));
|
|
720
|
+
}
|
|
721
|
+
function privateResidue(lockPath, names) {
|
|
722
|
+
const directory = path.dirname(lockPath);
|
|
723
|
+
const listed = names.slice(0, 3).map((name) => `'${path.join(directory, name)}'`).join(", ");
|
|
724
|
+
const omitted = names.length > 3 ? `, and ${names.length - 3} more` : "";
|
|
725
|
+
reject({
|
|
726
|
+
class: "conflict",
|
|
727
|
+
message: `private writer preparation residue at ${listed}${omitted} has unknown or unverifiable ownership and cannot be reclaimed safely; verify no Agentera writer is running, remove the listed residue, and retry`,
|
|
728
|
+
syntax: "agentera state <artifact-id> <verb> ... --format json",
|
|
729
|
+
example: `after verifying no Agentera writer is running, remove '${path.join(directory, names[0])}' and retry the same command`,
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
function unsupportedLock(lockPath, kind) {
|
|
733
|
+
const legacy = kind === "legacy file" ? "legacy writer lock file" : `writer lock ${kind}`;
|
|
734
|
+
reject({
|
|
735
|
+
class: "conflict",
|
|
736
|
+
message: `${legacy} at '${lockPath}' cannot be reclaimed safely; verify no Agentera writer is running, remove the entry, and retry`,
|
|
737
|
+
syntax: "agentera state <artifact-id> <verb> ... --format json",
|
|
738
|
+
example: `after verifying no Agentera writer is running, remove '${lockPath}' and retry the same command`,
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
function cleanupCreatedDirectory(projectDirectory, createdDirectory) {
|
|
742
|
+
if (!createdDirectory)
|
|
743
|
+
return;
|
|
744
|
+
try {
|
|
745
|
+
fs.rmdirSync(projectDirectory);
|
|
746
|
+
}
|
|
747
|
+
catch {
|
|
748
|
+
// A peer or project artifact now owns the directory.
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
function openProjectDirectory(projectRoot, expectedRoot) {
|
|
752
|
+
if (expectedRoot)
|
|
753
|
+
assertValidatedProjectRoot(expectedRoot);
|
|
754
|
+
const projectDirectory = path.join(projectRoot, ".agentera");
|
|
755
|
+
assertRealpathBoundary(projectRoot, projectDirectory, "writer lock");
|
|
756
|
+
const createdDirectory = !fs.existsSync(projectDirectory);
|
|
757
|
+
let parentFd;
|
|
758
|
+
try {
|
|
759
|
+
fs.mkdirSync(projectDirectory, { recursive: true });
|
|
760
|
+
if (expectedRoot)
|
|
761
|
+
assertValidatedProjectRoot(expectedRoot);
|
|
762
|
+
parentFd = fs.openSync(projectDirectory, DIRECTORY_FLAGS);
|
|
763
|
+
if (expectedRoot)
|
|
764
|
+
assertValidatedProjectRoot(expectedRoot);
|
|
765
|
+
return { projectDirectory, createdDirectory, parentFd };
|
|
766
|
+
}
|
|
767
|
+
catch (error) {
|
|
768
|
+
if (parentFd !== undefined)
|
|
769
|
+
fs.closeSync(parentFd);
|
|
770
|
+
cleanupCreatedDirectory(projectDirectory, createdDirectory);
|
|
771
|
+
throw error;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
export function acquireWriterLock(projectRoot, timeoutMs = 2000, expectedRoot) {
|
|
775
|
+
const { projectDirectory, createdDirectory, parentFd } = openProjectDirectory(projectRoot, expectedRoot);
|
|
776
|
+
const lockName = ".writer.lock";
|
|
777
|
+
const lockPath = path.join(projectDirectory, lockName);
|
|
778
|
+
const anchoredLockPath = fdPath(parentFd, lockName);
|
|
779
|
+
const deadline = Date.now() + timeoutMs;
|
|
780
|
+
let prepared;
|
|
781
|
+
try {
|
|
782
|
+
let waitedForPreparation = false;
|
|
783
|
+
while (recoverPreparedLocks(parentFd, lockPath)) {
|
|
784
|
+
waitedForPreparation = true;
|
|
785
|
+
waitForContention(lockPath, deadline);
|
|
786
|
+
}
|
|
787
|
+
if (waitedForPreparation && Date.now() >= deadline)
|
|
788
|
+
timeout(lockPath);
|
|
789
|
+
prepared = prepareLock(parentFd);
|
|
790
|
+
while (true) {
|
|
791
|
+
if (expectedRoot)
|
|
792
|
+
assertValidatedProjectRoot(expectedRoot);
|
|
793
|
+
const published = publishPrepared(prepared, projectDirectory, parentFd, lockPath, lockName, createdDirectory);
|
|
794
|
+
if (published) {
|
|
111
795
|
try {
|
|
112
|
-
|
|
796
|
+
if (expectedRoot)
|
|
797
|
+
assertValidatedProjectRoot(expectedRoot);
|
|
113
798
|
}
|
|
114
|
-
catch {
|
|
115
|
-
|
|
799
|
+
catch (error) {
|
|
800
|
+
published.release();
|
|
801
|
+
prepared = undefined;
|
|
802
|
+
throw error;
|
|
116
803
|
}
|
|
117
|
-
|
|
804
|
+
return published;
|
|
805
|
+
}
|
|
806
|
+
if (prepared.dirFd < 0 || prepared.ownerFd < 0) {
|
|
807
|
+
prepared = prepareLock(parentFd);
|
|
118
808
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
syntax: "agentera state <artifact-id> <verb> ... --format json",
|
|
124
|
-
example: "retry the same command after the active writer finishes",
|
|
125
|
-
});
|
|
809
|
+
const existing = inspectLock(anchoredLockPath);
|
|
810
|
+
if (existing.state === "legacy_file") {
|
|
811
|
+
closeInspection(existing);
|
|
812
|
+
unsupportedLock(lockPath, "legacy file");
|
|
126
813
|
}
|
|
127
|
-
|
|
814
|
+
if (existing.state === "unsupported")
|
|
815
|
+
unsupportedLock(lockPath, existing.kind);
|
|
816
|
+
if (existing.state === "directory") {
|
|
817
|
+
const reclaimable = existing.owner.state === "valid"
|
|
818
|
+
? ownerIsDead(existing.owner.record)
|
|
819
|
+
: existing.owner.stale;
|
|
820
|
+
if (reclaimable) {
|
|
821
|
+
const lock = adoptInspectedDirectory(existing, prepared, projectDirectory, parentFd, lockPath, lockName, createdDirectory);
|
|
822
|
+
if (lock)
|
|
823
|
+
return lock;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
closeInspection(existing);
|
|
827
|
+
waitForContention(lockPath, deadline);
|
|
128
828
|
}
|
|
129
829
|
}
|
|
830
|
+
catch (error) {
|
|
831
|
+
if (prepared !== undefined)
|
|
832
|
+
disposePrepared(prepared, parentFd);
|
|
833
|
+
fs.closeSync(parentFd);
|
|
834
|
+
cleanupCreatedDirectory(projectDirectory, createdDirectory);
|
|
835
|
+
throw error;
|
|
836
|
+
}
|
|
130
837
|
}
|
|
131
838
|
//# sourceMappingURL=lock.js.map
|