@nowcrew/daemon 0.6.77-beta.9 → 0.6.78
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/control-plane-url.js +3 -9
- package/dist/execution-runner.js +4 -5
- package/dist/local-executor.js +18 -17
- package/dist/local-memory/runtime.js +5 -3
- package/dist/machine-info.js +3 -0
- package/dist/project-skills/controller.js +6 -71
- package/dist/project-skills/execution-adapter.js +7 -0
- package/dist/project-skills/execution-projection.js +163 -0
- package/dist/project-skills/initialized-reconciler.js +10 -8
- package/dist/project-skills/legacy-cleanup.js +224 -0
- package/dist/project-skills/machine-hello-publisher.js +20 -0
- package/dist/project-skills/reconciler.js +114 -500
- package/dist/project-skills/resolution-records.js +63 -0
- package/dist/project-skills/runtime-launch.js +0 -42
- package/dist/project-skills/runtime-root-bootstrap.js +0 -6
- package/dist/project-skills/runtime-root-startup.js +1 -2
- package/dist/project-skills/runtime-warning.js +1 -3
- package/dist/project-skills/scanner.js +1 -17
- package/dist/project-skills/serve-startup.js +0 -23
- package/dist/project-skills/startup-cleanup.js +24 -0
- package/dist/serve.js +44 -45
- package/package.json +1 -1
|
@@ -1,552 +1,166 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Project Skill 解析 + 执行编排。
|
|
3
|
+
* reconcile: server 同步绑定时只解析"哪些 skill 在本机可用",不碰磁盘。
|
|
4
|
+
* prepareAndLaunch: 为这一次 execution 建独占投影目录 → 启动 runtime → 进程树确认退出后删除。
|
|
5
|
+
* 没有共享目录,所以没有锁、租约、manifest、GC。ownership 未确认时目录保留,交给启动 sweep。
|
|
6
|
+
*/
|
|
7
|
+
import { lstat, stat } from "node:fs/promises";
|
|
8
|
+
import { join, resolve, sep } from "node:path";
|
|
9
|
+
import { ExecutionProjectionError, materializeExecutionProjection, removeExecutionProjection, } from "./execution-projection.js";
|
|
10
|
+
import { projectSkillResolutionRecords } from "./scanner.js";
|
|
7
11
|
import { compareProjectSkillRefs, } from "./types.js";
|
|
8
|
-
import { PROJECT_SKILL_RUNTIME_MATERIALIZATION_REVISION, ProjectSkillRuntimeStoreError, } from "./runtime-root-domain.js";
|
|
9
|
-
import { createProjectSkillRuntimeRootStore, } from "./runtime-root-store.js";
|
|
10
|
-
import { createProjectSkillRuntimeLeaseStore, } from "./runtime-root-leases.js";
|
|
11
|
-
import { createProjectSkillRuntimeRootGc, } from "./runtime-root-gc.js";
|
|
12
|
-
import { projectSkillProjectionExpectation, projectSkillResolutionRecords, } from "./scanner.js";
|
|
13
|
-
import { cleanupProjectionStaging, isManagedProjectionRoot, markProjectionStaging, PROJECTION_ROOT_MARKER_NAME, projectionTargets, recoverProjectionSwitch, switchProjectionSet, } from "./projection-set-switch.js";
|
|
14
12
|
export class ProjectProjectionError extends Error {
|
|
15
13
|
code;
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
reason;
|
|
15
|
+
constructor(code, reason) {
|
|
16
|
+
super(reason === undefined ? code : `${code}: ${reason}`);
|
|
18
17
|
this.code = code;
|
|
18
|
+
this.reason = reason;
|
|
19
19
|
this.name = "ProjectProjectionError";
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
|
-
/**
|
|
22
|
+
/** runtime 进程树停止无法证明时抛出;调用方必须保留投影目录与非终态 journal。 */
|
|
23
23
|
export class ProjectSkillRuntimeOwnershipUnverifiedError extends AggregateError {
|
|
24
24
|
code = "project_skill_runtime_ownership_unverified";
|
|
25
|
-
|
|
25
|
+
/** cause 保留触发它的原始错误(例如 exit 的 rejection),否则排查时只剩两条固定的占位错误。 */
|
|
26
|
+
constructor(cause) {
|
|
26
27
|
super([
|
|
27
28
|
new Error("project_skill_runtime_lifetime_rejected"),
|
|
28
29
|
new Error("project_skill_runtime_stop_unverified"),
|
|
29
|
-
], "project_skill_runtime_ownership_unverified");
|
|
30
|
+
], "project_skill_runtime_ownership_unverified", cause === undefined ? undefined : { cause });
|
|
30
31
|
this.name = "ProjectSkillRuntimeOwnershipUnverifiedError";
|
|
31
32
|
}
|
|
32
33
|
}
|
|
33
|
-
|
|
34
|
-
if (applied === null || expected.generation > applied.generation) {
|
|
35
|
-
return Object.freeze({ kind: "ensure-applied" });
|
|
36
|
-
}
|
|
37
|
-
if (expected.generation < applied.generation) {
|
|
38
|
-
return Object.freeze({ kind: "use-advanced", warning: "skill_projection_advanced" });
|
|
39
|
-
}
|
|
40
|
-
if (expected.bindingDigest !== applied.bindingDigest) {
|
|
41
|
-
return Object.freeze({ kind: "reject", code: "skill_projection_snapshot_corrupt" });
|
|
42
|
-
}
|
|
43
|
-
if (applied.rootId === null)
|
|
44
|
-
return Object.freeze({ kind: "ensure-applied" });
|
|
45
|
-
return expected.resolutionDigest === applied.resolutionDigest
|
|
46
|
-
? Object.freeze({ kind: "use-current" })
|
|
47
|
-
: Object.freeze({ kind: "ensure-applied" });
|
|
48
|
-
};
|
|
49
|
-
/** Unlike the v1 lock lifetime helper, a rejected v2 exit is not a full-tree stop proof. */
|
|
50
|
-
const verifiedRuntimeProjectionLifetime = (value) => {
|
|
51
|
-
if (typeof value !== "object" || value === null || !("exit" in value))
|
|
52
|
-
return Promise.resolve();
|
|
53
|
-
const exit = value.exit;
|
|
54
|
-
return exit instanceof Promise ? exit : Promise.resolve();
|
|
55
|
-
};
|
|
56
|
-
const ownershipVerifiedRuntimeProjection = (value) => {
|
|
57
|
-
if (typeof value !== "object" || value === null || !("exit" in value))
|
|
58
|
-
return value;
|
|
59
|
-
const exit = value.exit;
|
|
60
|
-
if (!(exit instanceof Promise))
|
|
61
|
-
return value;
|
|
62
|
-
return {
|
|
63
|
-
...value,
|
|
64
|
-
exit: exit.catch(() => { throw new ProjectSkillRuntimeOwnershipUnverifiedError(); }),
|
|
65
|
-
};
|
|
66
|
-
};
|
|
67
|
-
const exists = async (path) => lstat(path).then(() => true, () => false);
|
|
34
|
+
const SAFE_HANDLE = /^[A-Za-z0-9._-]{1,128}$/u;
|
|
68
35
|
const canonicalConflictFreeBindings = (bindings) => {
|
|
69
|
-
const unique = [...new Map(bindings
|
|
70
|
-
`${binding.projectId}\0${binding.skillName}`,
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const names = new Map();
|
|
36
|
+
const unique = [...new Map(bindings
|
|
37
|
+
.map((binding) => [`${binding.projectId}\0${binding.skillName}`, binding])).values()]
|
|
38
|
+
.sort(compareProjectSkillRefs);
|
|
39
|
+
const owners = new Map();
|
|
74
40
|
for (const binding of unique) {
|
|
75
|
-
const owner =
|
|
41
|
+
const owner = owners.get(binding.skillName);
|
|
76
42
|
if (owner !== undefined && owner !== binding.projectId) {
|
|
77
43
|
throw new ProjectProjectionError("skill_name_conflict");
|
|
78
44
|
}
|
|
79
|
-
|
|
45
|
+
owners.set(binding.skillName, binding.projectId);
|
|
80
46
|
}
|
|
81
47
|
return Object.freeze(unique);
|
|
82
48
|
};
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|| (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
|
|
94
|
-
};
|
|
95
|
-
export const canonicalTrainingSkillLinkTarget = (source, link, trainingSkillsRoot, platform) => {
|
|
96
|
-
const path = platform === "win32" ? win32 : posix;
|
|
97
|
-
const normalizedSource = platform === "win32" ? normalizeWindowsLinkIdentity(source) : source;
|
|
98
|
-
const normalizedLink = platform === "win32" ? normalizeWindowsLinkIdentity(link) : link;
|
|
99
|
-
const normalizedTrainingRoot = platform === "win32"
|
|
100
|
-
? normalizeWindowsLinkIdentity(trainingSkillsRoot)
|
|
101
|
-
: trainingSkillsRoot;
|
|
102
|
-
const resolvedLink = path.resolve(path.dirname(normalizedSource), normalizedLink);
|
|
103
|
-
return isSameOrWithin(path.normalize(normalizedTrainingRoot), resolvedLink, path)
|
|
104
|
-
? path.normalize(resolvedLink)
|
|
105
|
-
: null;
|
|
106
|
-
};
|
|
107
|
-
export const isTrainingSkillLink = (source, link, trainingSkillsRoot, platform) => canonicalTrainingSkillLinkTarget(source, link, trainingSkillsRoot, platform) !== null;
|
|
108
|
-
const preserveNonProjectSkills = async (projectionTarget, trainingSkillsRoot, platform, projectSkillNames) => {
|
|
109
|
-
const names = new Set();
|
|
110
|
-
if (!await exists(projectionTarget.target))
|
|
111
|
-
return names;
|
|
112
|
-
const rootInfo = await lstat(projectionTarget.target);
|
|
113
|
-
if (!rootInfo.isDirectory() || rootInfo.isSymbolicLink()) {
|
|
114
|
-
throw new ProjectProjectionError("skill_projection_failed");
|
|
49
|
+
/** 源目录必须是真实目录且有 SKILL.md,否则按 missing 处理(warning,不阻断)。 */
|
|
50
|
+
const actualResolutionRecords = async (bindings, projects, platform) => Object.freeze(await Promise.all(projectSkillResolutionRecords(bindings, projects, platform).map(async (record) => {
|
|
51
|
+
if (record.mode === "missing" || record.sourcePath === null)
|
|
52
|
+
return record;
|
|
53
|
+
const [source, skillFile] = await Promise.all([
|
|
54
|
+
lstat(record.sourcePath).catch(() => null),
|
|
55
|
+
stat(join(record.sourcePath, "SKILL.md")).catch(() => null),
|
|
56
|
+
]);
|
|
57
|
+
if (source?.isDirectory() === true && !source.isSymbolicLink() && skillFile?.isFile() === true) {
|
|
58
|
+
return record;
|
|
115
59
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
if (projectSkillNames.has(entry.name))
|
|
134
|
-
continue;
|
|
135
|
-
await symlink(platform === "win32" ? canonicalTarget : link, destination, platform === "win32" ? "junction" : "dir");
|
|
136
|
-
}
|
|
137
|
-
else {
|
|
138
|
-
await cp(source, destination, { recursive: true, preserveTimestamps: true });
|
|
139
|
-
}
|
|
140
|
-
names.add(entry.name);
|
|
141
|
-
}
|
|
142
|
-
return names;
|
|
60
|
+
return Object.freeze({
|
|
61
|
+
projectId: record.projectId,
|
|
62
|
+
skillName: record.skillName,
|
|
63
|
+
sourcePath: null,
|
|
64
|
+
mode: "missing",
|
|
65
|
+
});
|
|
66
|
+
})));
|
|
67
|
+
const toResolutions = (records) => Object.freeze(records.map((record) => Object.freeze({
|
|
68
|
+
projectId: record.projectId,
|
|
69
|
+
skillName: record.skillName,
|
|
70
|
+
status: record.mode === "resolved" ? "linked" : "unavailable",
|
|
71
|
+
})));
|
|
72
|
+
const exitPromiseOf = (value) => {
|
|
73
|
+
if (typeof value !== "object" || value === null || !("exit" in value))
|
|
74
|
+
return null;
|
|
75
|
+
const exit = value.exit;
|
|
76
|
+
return exit instanceof Promise ? exit : null;
|
|
143
77
|
};
|
|
144
78
|
export function createProjectSkillsReconciler(deps) {
|
|
145
79
|
const platform = deps.platform ?? process.platform;
|
|
146
|
-
const
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const prepareUpdate = deps.prepareAppliedManifestUpdate ?? prepareAppliedProjectSkillManifestUpdate;
|
|
151
|
-
const publishUpdate = deps.publishAppliedManifestUpdate
|
|
152
|
-
?? ((update) => publishPreparedProjectSkillManifestUpdate(update, readOptions));
|
|
153
|
-
const commitUpdate = deps.commitAppliedManifestUpdate
|
|
154
|
-
?? ((update) => commitPreparedProjectSkillManifestUpdate(update, readOptions));
|
|
155
|
-
const rollbackUpdate = deps.rollbackAppliedManifestUpdate
|
|
156
|
-
?? ((update) => rollbackPreparedProjectSkillManifestUpdate(update, readOptions));
|
|
157
|
-
const manifestOperations = Object.freeze({
|
|
158
|
-
publish: publishUpdate,
|
|
159
|
-
commit: commitUpdate,
|
|
160
|
-
rollback: rollbackUpdate,
|
|
161
|
-
});
|
|
162
|
-
const runtimeRoots = deps.runtimeRoots ?? createProjectSkillRuntimeRootStore({ platform });
|
|
163
|
-
const runtimeLeases = deps.runtimeLeases ?? createProjectSkillRuntimeLeaseStore({ platform });
|
|
164
|
-
const runtimeRootGc = deps.runtimeRootGc ?? createProjectSkillRuntimeRootGc({ platform, runtimeRoots });
|
|
165
|
-
const protectedExecutionIds = deps.protectedExecutionIds ?? new Set();
|
|
166
|
-
let lastProjectionDiagnostics = Object.freeze([]);
|
|
167
|
-
const reconcileUnlocked = async (handle, bindings, projects = deps.scannedProjects(), recover = true, manifestPublication) => {
|
|
168
|
-
const agentRoot = join(deps.agentsRoot, handle);
|
|
169
|
-
if (recover) {
|
|
170
|
-
try {
|
|
171
|
-
await recoverProjectionSwitch(agentRoot, deps.syncDirectory);
|
|
172
|
-
}
|
|
173
|
-
catch {
|
|
174
|
-
throw new ProjectProjectionError("skill_projection_failed");
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
const uniqueBindings = canonicalConflictFreeBindings(bindings);
|
|
178
|
-
const linked = [];
|
|
179
|
-
const resolutions = [];
|
|
180
|
-
const resolutionRecords = [];
|
|
181
|
-
for (const binding of uniqueBindings) {
|
|
182
|
-
const project = projects.find((candidate) => candidate.inventory.projectId === binding.projectId
|
|
183
|
-
&& candidate.inventory.status === "available");
|
|
184
|
-
const skill = project?.resolved.find((candidate) => candidate.name === binding.skillName);
|
|
185
|
-
const available = skill !== undefined && (await stat(skill.sourcePath).catch(() => null))?.isDirectory() === true;
|
|
186
|
-
resolutions.push(Object.freeze({ ...binding, status: available ? "linked" : "unavailable" }));
|
|
187
|
-
if (available && skill !== undefined) {
|
|
188
|
-
linked.push({ binding, skill });
|
|
189
|
-
resolutionRecords.push(Object.freeze({
|
|
190
|
-
...binding,
|
|
191
|
-
sourcePath: skill.sourcePath,
|
|
192
|
-
mode: "resolved",
|
|
193
|
-
}));
|
|
194
|
-
}
|
|
195
|
-
else {
|
|
196
|
-
resolutionRecords.push(Object.freeze({ ...binding, sourcePath: null, mode: "missing" }));
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
const linkedNames = new Set(linked.map((item) => item.binding.skillName));
|
|
200
|
-
const id = randomUUID();
|
|
201
|
-
const targets = projectionTargets(agentRoot, id);
|
|
202
|
-
const nextProjectionDiagnostics = [];
|
|
203
|
-
try {
|
|
204
|
-
for (const target of targets) {
|
|
205
|
-
await mkdir(dirname(target.target), { recursive: true });
|
|
206
|
-
await mkdir(target.staging, { mode: 0o700 });
|
|
207
|
-
await markProjectionStaging(target, id);
|
|
208
|
-
const preservedNames = await preserveNonProjectSkills(target, join(agentRoot, "training", "skills"), platform, linkedNames);
|
|
209
|
-
for (const item of linked) {
|
|
210
|
-
if (preservedNames.has(item.binding.skillName)) {
|
|
211
|
-
throw new ProjectProjectionError("skill_name_conflict");
|
|
212
|
-
}
|
|
213
|
-
const projectionTarget = join(target.staging, item.binding.skillName);
|
|
214
|
-
const projection = await directoryProjector(item.skill.sourcePath, projectionTarget, platform, { finalTarget: join(target.target, item.binding.skillName) });
|
|
215
|
-
nextProjectionDiagnostics.push(Object.freeze({
|
|
216
|
-
runtime: target.runtime,
|
|
217
|
-
projectId: item.binding.projectId,
|
|
218
|
-
skillName: item.binding.skillName,
|
|
219
|
-
mode: projection.mode,
|
|
220
|
-
...(projection.copyProtection === undefined
|
|
221
|
-
? {}
|
|
222
|
-
: { copyProtection: projection.copyProtection }),
|
|
223
|
-
}));
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
await writeFile(join(agentRoot, ".nowwork-root"), "", { encoding: "utf8", mode: 0o600 });
|
|
227
|
-
for (const target of targets)
|
|
228
|
-
await deps.beforeSwitch?.(target.runtime);
|
|
229
|
-
const publication = await manifestPublication?.(Object.freeze(resolutionRecords));
|
|
230
|
-
await switchProjectionSet(agentRoot, targets, id, deps.switchFault, deps.syncDirectory, publication);
|
|
231
|
-
lastProjectionDiagnostics = Object.freeze(nextProjectionDiagnostics);
|
|
232
|
-
return Object.freeze({
|
|
233
|
-
resolutions: Object.freeze(resolutions),
|
|
234
|
-
resolutionRecords: Object.freeze(resolutionRecords),
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
catch (error) {
|
|
238
|
-
try {
|
|
239
|
-
await Promise.all(targets.map((target) => cleanupProjectionStaging(target, id, deps.syncDirectory)));
|
|
240
|
-
}
|
|
241
|
-
catch {
|
|
242
|
-
throw new ProjectProjectionError("skill_projection_failed");
|
|
243
|
-
}
|
|
244
|
-
if (error instanceof ProjectProjectionError)
|
|
245
|
-
throw error;
|
|
246
|
-
throw new ProjectProjectionError("skill_projection_failed");
|
|
247
|
-
}
|
|
248
|
-
};
|
|
249
|
-
const validateExpectation = (expected) => {
|
|
250
|
-
const canonical = [...expected.bindings].sort(compareProjectSkillRefs);
|
|
251
|
-
const isCanonical = canonical.length === expected.bindings.length
|
|
252
|
-
&& canonical.every((binding, index) => binding.projectId === expected.bindings[index]?.projectId
|
|
253
|
-
&& binding.skillName === expected.bindings[index]?.skillName)
|
|
254
|
-
&& canonical.every((binding, index) => index === 0
|
|
255
|
-
|| binding.projectId !== canonical[index - 1]?.projectId
|
|
256
|
-
|| binding.skillName !== canonical[index - 1]?.skillName);
|
|
257
|
-
const digestShape = /^sha256:[a-f0-9]{64}$/u;
|
|
258
|
-
let bindingDigestMatches = false;
|
|
259
|
-
try {
|
|
260
|
-
bindingDigestMatches = computeProjectSkillBindingDigest(expected.bindings) === expected.bindingDigest;
|
|
261
|
-
}
|
|
262
|
-
catch {
|
|
263
|
-
bindingDigestMatches = false;
|
|
264
|
-
}
|
|
265
|
-
if (!isCanonical
|
|
266
|
-
|| !Number.isInteger(expected.generation)
|
|
267
|
-
|| expected.generation < 0
|
|
268
|
-
|| expected.generation > 2_147_483_647
|
|
269
|
-
|| !digestShape.test(expected.resolutionDigest)
|
|
270
|
-
|| !bindingDigestMatches) {
|
|
271
|
-
throw new ProjectProjectionError("skill_projection_snapshot_corrupt");
|
|
272
|
-
}
|
|
273
|
-
};
|
|
274
|
-
const rejectDecision = (decision) => {
|
|
275
|
-
if (decision.kind === "reject")
|
|
276
|
-
throw new ProjectProjectionError(decision.code);
|
|
277
|
-
};
|
|
278
|
-
const mapRuntimeRootError = (error) => {
|
|
279
|
-
if (error instanceof ProjectProjectionError)
|
|
280
|
-
throw error;
|
|
281
|
-
if (error instanceof ProjectSkillRuntimeStoreError
|
|
282
|
-
&& error.code === "skill_projection_snapshot_corrupt") {
|
|
283
|
-
throw new ProjectProjectionError("skill_projection_snapshot_corrupt");
|
|
80
|
+
const agentsRoot = resolve(deps.agentsRoot);
|
|
81
|
+
const agentRootFor = (handle) => {
|
|
82
|
+
if (!SAFE_HANDLE.test(handle) || handle === "." || handle === "..") {
|
|
83
|
+
throw new ProjectProjectionError("skill_projection_failed", "unsafe_handle");
|
|
284
84
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
const warningsFor = (records) => Object.freeze(records
|
|
289
|
-
.filter((record) => record.mode === "missing")
|
|
290
|
-
.map((record) => Object.freeze({
|
|
291
|
-
projectId: record.projectId,
|
|
292
|
-
skillName: record.skillName,
|
|
293
|
-
code: "project_skill_unavailable",
|
|
294
|
-
})));
|
|
295
|
-
const withWarnings = (decision, warnings) => {
|
|
296
|
-
if (decision.kind === "reject" || warnings.length === 0) {
|
|
297
|
-
return decision;
|
|
85
|
+
const agentRoot = resolve(agentsRoot, handle);
|
|
86
|
+
if (!agentRoot.startsWith(`${agentsRoot}${sep}`)) {
|
|
87
|
+
throw new ProjectProjectionError("skill_projection_failed", "unsafe_handle");
|
|
298
88
|
}
|
|
299
|
-
return
|
|
89
|
+
return agentRoot;
|
|
300
90
|
};
|
|
301
|
-
const
|
|
302
|
-
if (record.mode === "missing" || record.sourcePath === null)
|
|
303
|
-
return record;
|
|
304
|
-
const sourcePath = record.sourcePath;
|
|
305
|
-
const [source, skillFile] = await Promise.all([
|
|
306
|
-
lstat(sourcePath).catch(() => null),
|
|
307
|
-
stat(join(sourcePath, "SKILL.md")).catch(() => null),
|
|
308
|
-
]);
|
|
309
|
-
if (source?.isDirectory() === true
|
|
310
|
-
&& !source.isSymbolicLink()
|
|
311
|
-
&& skillFile?.isFile() === true) {
|
|
312
|
-
return record;
|
|
313
|
-
}
|
|
314
|
-
return Object.freeze({
|
|
315
|
-
projectId: record.projectId,
|
|
316
|
-
skillName: record.skillName,
|
|
317
|
-
sourcePath: null,
|
|
318
|
-
mode: "missing",
|
|
319
|
-
});
|
|
320
|
-
})));
|
|
321
|
-
const commitAppliedRoot = async (agentRoot, applied) => {
|
|
322
|
-
const update = await prepareUpdate(agentRoot, applied, readOptions);
|
|
323
|
-
await publishUpdate(update);
|
|
324
|
-
await deps.afterManifestPublishedBeforeCommit?.();
|
|
325
|
-
await commitUpdate(update);
|
|
326
|
-
if (!exactManifest(await readApplied(agentRoot, readOptions), applied)) {
|
|
327
|
-
throw new ProjectProjectionError("skill_projection_failed");
|
|
328
|
-
}
|
|
329
|
-
};
|
|
330
|
-
const ensureRuntimeRootLocked = async (handle, requested, options = {}) => {
|
|
331
|
-
const agentRoot = join(deps.agentsRoot, handle);
|
|
91
|
+
const cleanup = async (agentRoot, executionId) => {
|
|
332
92
|
try {
|
|
333
|
-
|
|
334
|
-
await recoverProjectionSwitch(agentRoot, deps.syncDirectory, manifestOperations);
|
|
335
|
-
await recoverAppliedProjectSkillManifestUpdates(agentRoot, readOptions);
|
|
336
|
-
await runtimeRoots.recover(agentRoot);
|
|
337
|
-
const projects = deps.scannedProjects();
|
|
338
|
-
const wireRequest = Object.freeze({
|
|
339
|
-
bindings: requested.bindings,
|
|
340
|
-
generation: requested.generation,
|
|
341
|
-
});
|
|
342
|
-
let cachedRequested;
|
|
343
|
-
try {
|
|
344
|
-
cachedRequested = projectSkillProjectionExpectation(wireRequest, projects, platform);
|
|
345
|
-
}
|
|
346
|
-
catch (error) {
|
|
347
|
-
if (error instanceof ProjectProjectionError)
|
|
348
|
-
throw error;
|
|
349
|
-
throw new ProjectProjectionError("skill_projection_snapshot_corrupt");
|
|
350
|
-
}
|
|
351
|
-
const localResolutions = await actualResolutionRecords(cachedRequested.bindings, projects);
|
|
352
|
-
const localRequested = Object.freeze({
|
|
353
|
-
...cachedRequested,
|
|
354
|
-
resolutionDigest: computeProjectSkillResolutionDigest(localResolutions, platform),
|
|
355
|
-
});
|
|
356
|
-
validateExpectation(localRequested);
|
|
357
|
-
if ("resolutionDigest" in requested)
|
|
358
|
-
validateExpectation(requested);
|
|
359
|
-
const current = await readApplied(agentRoot, readOptions);
|
|
360
|
-
const callerDecision = decideProjectSkillProjection(localRequested, current);
|
|
361
|
-
rejectDecision(callerDecision);
|
|
362
|
-
const desiredBindings = callerDecision.kind === "use-advanced" && current !== null
|
|
363
|
-
? current.bindings
|
|
364
|
-
: localRequested.bindings;
|
|
365
|
-
const desiredGeneration = callerDecision.kind === "use-advanced" && current !== null
|
|
366
|
-
? current.generation
|
|
367
|
-
: localRequested.generation;
|
|
368
|
-
const resolutions = callerDecision.kind === "use-advanced"
|
|
369
|
-
? await actualResolutionRecords(desiredBindings, projects)
|
|
370
|
-
: localResolutions;
|
|
371
|
-
const desiredExpectation = Object.freeze({
|
|
372
|
-
bindings: desiredBindings,
|
|
373
|
-
generation: desiredGeneration,
|
|
374
|
-
bindingDigest: computeProjectSkillBindingDigest(desiredBindings),
|
|
375
|
-
resolutionDigest: computeProjectSkillResolutionDigest(resolutions, platform),
|
|
376
|
-
});
|
|
377
|
-
const inventory = await runtimeRoots.list(agentRoot);
|
|
378
|
-
const currentRecord = current?.rootId === null || current?.rootId === undefined
|
|
379
|
-
? undefined
|
|
380
|
-
: inventory.find((record) => record.rootId === current.rootId);
|
|
381
|
-
const reusableIdentity = current !== null
|
|
382
|
-
&& current.rootId !== null
|
|
383
|
-
&& current.generation === desiredExpectation.generation
|
|
384
|
-
&& current.bindingDigest === desiredExpectation.bindingDigest
|
|
385
|
-
&& current.resolutionDigest === desiredExpectation.resolutionDigest
|
|
386
|
-
&& currentRecord?.materializationRevision === PROJECT_SKILL_RUNTIME_MATERIALIZATION_REVISION
|
|
387
|
-
&& currentRecord.bindingDigest === current.bindingDigest
|
|
388
|
-
&& currentRecord.resolutionDigest === current.resolutionDigest;
|
|
389
|
-
let runtimeRoot;
|
|
390
|
-
let published = false;
|
|
391
|
-
const reusableRootId = reusableIdentity ? current.rootId : null;
|
|
392
|
-
const currentInspection = reusableRootId === null
|
|
393
|
-
? null
|
|
394
|
-
: await runtimeRoots.inspect(agentRoot, reusableRootId);
|
|
395
|
-
const reusable = currentInspection !== null
|
|
396
|
-
&& (!options.refreshCopyProjection || !currentInspection.containsCopyProjection);
|
|
397
|
-
if (reusable) {
|
|
398
|
-
runtimeRoot = currentInspection.descriptor;
|
|
399
|
-
}
|
|
400
|
-
else {
|
|
401
|
-
runtimeRoot = await runtimeRoots.publish({
|
|
402
|
-
agentRoot,
|
|
403
|
-
bindingDigest: desiredExpectation.bindingDigest,
|
|
404
|
-
resolutionDigest: desiredExpectation.resolutionDigest,
|
|
405
|
-
resolutions,
|
|
406
|
-
});
|
|
407
|
-
published = true;
|
|
408
|
-
await deps.afterRootPublishedBeforeAppliedCommit?.(runtimeRoot);
|
|
409
|
-
const applied = createAppliedProjectSkillManifest({
|
|
410
|
-
bindings: desiredBindings,
|
|
411
|
-
generation: desiredGeneration,
|
|
412
|
-
resolutions,
|
|
413
|
-
rootId: runtimeRoot.rootId,
|
|
414
|
-
platform,
|
|
415
|
-
});
|
|
416
|
-
await commitAppliedRoot(agentRoot, applied);
|
|
417
|
-
}
|
|
418
|
-
const baseDecision = callerDecision.kind === "use-advanced"
|
|
419
|
-
? callerDecision
|
|
420
|
-
: Object.freeze({ kind: published ? "ensure-applied" : "use-current" });
|
|
421
|
-
return Object.freeze({
|
|
422
|
-
decision: withWarnings(baseDecision, warningsFor(resolutions)),
|
|
423
|
-
runtimeRoot,
|
|
424
|
-
});
|
|
93
|
+
await removeExecutionProjection(agentRoot, executionId);
|
|
425
94
|
}
|
|
426
95
|
catch (error) {
|
|
427
|
-
|
|
96
|
+
deps.onCleanupFailed?.(executionId, error);
|
|
428
97
|
}
|
|
429
98
|
};
|
|
430
|
-
const ensureApplied = async (handle, expected, options = {}) => {
|
|
431
|
-
validateExpectation(expected);
|
|
432
|
-
return deps.coordinator.runExclusive(deps.agentsRoot, handle, async () => (await ensureRuntimeRootLocked(handle, expected, options)).decision);
|
|
433
|
-
};
|
|
434
|
-
const ensureSnapshot = async (handle, snapshot, options = {}) => deps.coordinator.runExclusive(deps.agentsRoot, handle, async () => (await ensureRuntimeRootLocked(handle, snapshot, options)).decision);
|
|
435
|
-
const reconcileLegacyUnlocked = async (handle, bindings) => {
|
|
436
|
-
const agentRoot = join(deps.agentsRoot, handle);
|
|
437
|
-
try {
|
|
438
|
-
await recoverProjectionSwitch(agentRoot, deps.syncDirectory, manifestOperations);
|
|
439
|
-
}
|
|
440
|
-
catch {
|
|
441
|
-
throw new ProjectProjectionError("skill_projection_failed");
|
|
442
|
-
}
|
|
443
|
-
return reconcileUnlocked(handle, bindings, deps.scannedProjects(), false, async () => {
|
|
444
|
-
const update = await prepareRemoval(agentRoot, readOptions);
|
|
445
|
-
return Object.freeze({
|
|
446
|
-
update,
|
|
447
|
-
operations: manifestOperations,
|
|
448
|
-
verify: async () => {
|
|
449
|
-
if (await readApplied(agentRoot, readOptions) !== null) {
|
|
450
|
-
throw new ProjectProjectionError("skill_projection_failed");
|
|
451
|
-
}
|
|
452
|
-
},
|
|
453
|
-
...(deps.afterManifestPublishedBeforeCommit === undefined
|
|
454
|
-
? {}
|
|
455
|
-
: { afterPublishedBeforeCommit: deps.afterManifestPublishedBeforeCommit }),
|
|
456
|
-
...(deps.unlinkSwitchJournal === undefined
|
|
457
|
-
? {}
|
|
458
|
-
: { unlinkJournal: deps.unlinkSwitchJournal }),
|
|
459
|
-
});
|
|
460
|
-
});
|
|
461
|
-
};
|
|
462
99
|
return {
|
|
463
|
-
reconcile(handle, bindings) {
|
|
464
|
-
|
|
100
|
+
async reconcile(handle, bindings) {
|
|
101
|
+
agentRootFor(handle);
|
|
102
|
+
const canonical = canonicalConflictFreeBindings(bindings);
|
|
103
|
+
return toResolutions(await actualResolutionRecords(canonical, deps.scannedProjects(), platform));
|
|
465
104
|
},
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
105
|
+
async prepareAndLaunch(requestedAgentsRoot, executionId, handle, projection, launch, onWarning) {
|
|
106
|
+
if (resolve(requestedAgentsRoot) !== agentsRoot) {
|
|
107
|
+
throw new ProjectProjectionError("skill_projection_failed", "foreign_agents_root");
|
|
108
|
+
}
|
|
109
|
+
const agentRoot = agentRootFor(handle);
|
|
110
|
+
const bindings = Array.isArray(projection)
|
|
111
|
+
? projection
|
|
112
|
+
: projection.bindings;
|
|
113
|
+
const canonical = canonicalConflictFreeBindings(bindings);
|
|
114
|
+
const records = await actualResolutionRecords(canonical, deps.scannedProjects(), platform);
|
|
115
|
+
for (const record of records) {
|
|
116
|
+
if (record.mode === "missing") {
|
|
117
|
+
onWarning?.({
|
|
118
|
+
code: "project_skill_unavailable",
|
|
119
|
+
projectId: record.projectId,
|
|
120
|
+
skillName: record.skillName,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
478
123
|
}
|
|
479
|
-
let
|
|
124
|
+
let root;
|
|
480
125
|
try {
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
lease = await runtimeLeases.acquire(agentRoot, {
|
|
489
|
-
executionId,
|
|
490
|
-
rootId: ensured.runtimeRoot.rootId,
|
|
491
|
-
});
|
|
492
|
-
}
|
|
493
|
-
catch (error) {
|
|
494
|
-
let durableLeaseMayExist = true;
|
|
495
|
-
try {
|
|
496
|
-
durableLeaseMayExist = (await runtimeLeases.protectedExecutionIds(agentRoot))
|
|
497
|
-
.includes(executionId);
|
|
498
|
-
}
|
|
499
|
-
catch {
|
|
500
|
-
// Inventory ambiguity retains protection until startup recovery can fail closed.
|
|
501
|
-
}
|
|
502
|
-
if (!durableLeaseMayExist && !alreadyProtected) {
|
|
503
|
-
protectedExecutionIds.delete(executionId);
|
|
504
|
-
}
|
|
505
|
-
throw error;
|
|
506
|
-
}
|
|
507
|
-
return Object.freeze({ ...ensured, lease });
|
|
508
|
-
});
|
|
126
|
+
root = (await materializeExecutionProjection({
|
|
127
|
+
agentRoot,
|
|
128
|
+
executionId,
|
|
129
|
+
resolutions: records,
|
|
130
|
+
platform,
|
|
131
|
+
...(deps.projectDirectory === undefined ? {} : { projectDirectory: deps.projectDirectory }),
|
|
132
|
+
})).root;
|
|
509
133
|
}
|
|
510
134
|
catch (error) {
|
|
511
|
-
|
|
135
|
+
if (error instanceof ExecutionProjectionError) {
|
|
136
|
+
throw new ProjectProjectionError("skill_projection_failed", error.reason);
|
|
137
|
+
}
|
|
138
|
+
throw new ProjectProjectionError("skill_projection_failed");
|
|
512
139
|
}
|
|
513
|
-
|
|
514
|
-
await deps.coordinator.runExclusive(deps.agentsRoot, handle, async () => {
|
|
515
|
-
await captured.lease.release();
|
|
516
|
-
protectedExecutionIds.delete(captured.lease.executionId);
|
|
517
|
-
try {
|
|
518
|
-
const agentRoot = join(deps.agentsRoot, handle);
|
|
519
|
-
const leases = await runtimeLeases.list(agentRoot);
|
|
520
|
-
const applied = await readApplied(agentRoot, readOptions);
|
|
521
|
-
await runtimeRootGc.collect(agentRoot, {
|
|
522
|
-
currentRootId: applied?.rootId ?? null,
|
|
523
|
-
leasedRootIds: leases.map(({ rootId }) => rootId),
|
|
524
|
-
keepHistory: 2,
|
|
525
|
-
});
|
|
526
|
-
}
|
|
527
|
-
catch {
|
|
528
|
-
// GC is best-effort after a successful lease release.
|
|
529
|
-
}
|
|
530
|
-
});
|
|
531
|
-
};
|
|
140
|
+
let child;
|
|
532
141
|
try {
|
|
533
|
-
|
|
534
|
-
onWarning?.({ code: captured.decision.warning });
|
|
535
|
-
}
|
|
536
|
-
if ("warnings" in captured.decision) {
|
|
537
|
-
for (const warning of captured.decision.warnings ?? [])
|
|
538
|
-
onWarning?.(warning);
|
|
539
|
-
}
|
|
540
|
-
const child = ownershipVerifiedRuntimeProjection(await launch(captured.runtimeRoot));
|
|
541
|
-
void verifiedRuntimeProjectionLifetime(child).then(releaseLease, () => undefined).catch(() => undefined);
|
|
542
|
-
return child;
|
|
142
|
+
child = await launch(root);
|
|
543
143
|
}
|
|
544
144
|
catch (error) {
|
|
145
|
+
// launch 的 owner 契约:抛错要么发生在 spawn 前,要么已证明进程树停止;唯一例外是 ownership 未确认。
|
|
545
146
|
if (error instanceof ProjectSkillRuntimeOwnershipUnverifiedError)
|
|
546
147
|
throw error;
|
|
547
|
-
await
|
|
148
|
+
await cleanup(agentRoot, executionId);
|
|
548
149
|
throw error;
|
|
549
150
|
}
|
|
151
|
+
const exit = exitPromiseOf(child);
|
|
152
|
+
if (exit === null) {
|
|
153
|
+
await cleanup(agentRoot, executionId);
|
|
154
|
+
return child;
|
|
155
|
+
}
|
|
156
|
+
const guardedExit = exit.then(async (value) => {
|
|
157
|
+
await cleanup(agentRoot, executionId);
|
|
158
|
+
return value;
|
|
159
|
+
}, (error) => {
|
|
160
|
+
throw new ProjectSkillRuntimeOwnershipUnverifiedError(error);
|
|
161
|
+
});
|
|
162
|
+
guardedExit.catch(() => undefined);
|
|
163
|
+
return { ...child, exit: guardedExit };
|
|
550
164
|
},
|
|
551
165
|
};
|
|
552
166
|
}
|