@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.
@@ -1,552 +1,166 @@
1
- import { cp, lstat, mkdir, readdir, readlink, stat, symlink, writeFile } from "node:fs/promises";
2
- import { randomUUID } from "node:crypto";
3
- import { dirname, join, posix, win32 } from "node:path";
4
- import { isManagedDirectoryProjectionCopy, projectDirectory, } from "../directory-projection.js";
5
- import { runtimeProjectionLifetime, } from "./agent-projection-coordinator.js";
6
- import { commitPreparedProjectSkillManifestUpdate, computeProjectSkillBindingDigest, computeProjectSkillResolutionDigest, createAppliedProjectSkillManifest, prepareAppliedProjectSkillManifestRemoval, prepareAppliedProjectSkillManifestUpdate, publishPreparedProjectSkillManifestUpdate, readAppliedProjectSkillManifest, recoverAppliedProjectSkillManifestUpdates, rollbackPreparedProjectSkillManifestUpdate, } from "./projection-state.js";
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
- constructor(code) {
17
- super(code);
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
- /** A process may still own its leased root; only startup/journal recovery may clear this state. */
22
+ /** runtime 进程树停止无法证明时抛出;调用方必须保留投影目录与非终态 journal。 */
23
23
  export class ProjectSkillRuntimeOwnershipUnverifiedError extends AggregateError {
24
24
  code = "project_skill_runtime_ownership_unverified";
25
- constructor() {
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
- export const decideProjectSkillProjection = (expected, applied) => {
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.map((binding) => [
70
- `${binding.projectId}\0${binding.skillName}`,
71
- binding,
72
- ])).values()].sort(compareProjectSkillRefs);
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 = names.get(binding.skillName);
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
- names.set(binding.skillName, binding.projectId);
45
+ owners.set(binding.skillName, binding.projectId);
80
46
  }
81
47
  return Object.freeze(unique);
82
48
  };
83
- const normalizeWindowsLinkIdentity = (value) => {
84
- if (/^\\\\\?\\UNC\\/iu.test(value))
85
- return `\\\\${value.slice(8)}`;
86
- if (/^\\\\\?\\[A-Za-z]:\\/u.test(value))
87
- return value.slice(4);
88
- return value;
89
- };
90
- const isSameOrWithin = (root, candidate, path) => {
91
- const relative = path.relative(root, candidate);
92
- return relative === ""
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
- const managedRoot = await isManagedProjectionRoot(projectionTarget.target, projectionTarget);
117
- for (const entry of await readdir(projectionTarget.target, { withFileTypes: true })) {
118
- if (entry.name === PROJECTION_ROOT_MARKER_NAME) {
119
- if (managedRoot)
120
- continue;
121
- throw new ProjectProjectionError("skill_projection_failed");
122
- }
123
- const source = join(projectionTarget.target, entry.name);
124
- const destination = join(projectionTarget.staging, entry.name);
125
- if (platform === "win32" && await isManagedDirectoryProjectionCopy(source, platform))
126
- continue;
127
- const info = await lstat(source);
128
- if (info.isSymbolicLink()) {
129
- const link = await readlink(source);
130
- const canonicalTarget = canonicalTrainingSkillLinkTarget(source, link, trainingSkillsRoot, platform);
131
- if (canonicalTarget === null)
132
- continue;
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 directoryProjector = deps.projectDirectory ?? projectDirectory;
147
- const readOptions = { platform };
148
- const readApplied = deps.readAppliedManifest ?? readAppliedProjectSkillManifest;
149
- const prepareRemoval = deps.prepareAppliedManifestRemoval ?? prepareAppliedProjectSkillManifestRemoval;
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
- throw new ProjectProjectionError("skill_projection_failed");
286
- };
287
- const exactManifest = (left, right) => left !== null && JSON.stringify(left) === JSON.stringify(right);
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 Object.freeze({ ...decision, warnings });
89
+ return agentRoot;
300
90
  };
301
- const actualResolutionRecords = async (bindings, projects) => Object.freeze(await Promise.all(projectSkillResolutionRecords(bindings, projects, platform).map(async (record) => {
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
- canonicalConflictFreeBindings(requested.bindings);
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
- return mapRuntimeRootError(error);
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
- return deps.coordinator.runExclusive(deps.agentsRoot, handle, async () => (await reconcileLegacyUnlocked(handle, bindings)).resolutions);
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
- ensureSnapshot,
467
- ensureApplied,
468
- projectionDiagnostics: () => lastProjectionDiagnostics,
469
- async prepareAndLaunch(agentsRoot, executionId, handle, projection, launch, onWarning) {
470
- if (agentsRoot !== deps.agentsRoot) {
471
- throw new ProjectProjectionError("skill_projection_failed");
472
- }
473
- if (Array.isArray(projection)) {
474
- return deps.coordinator.runExclusiveUntil(deps.agentsRoot, handle, async () => {
475
- await reconcileLegacyUnlocked(handle, projection);
476
- return launch(undefined);
477
- }, runtimeProjectionLifetime);
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 captured;
124
+ let root;
480
125
  try {
481
- captured = await deps.coordinator.runExclusive(deps.agentsRoot, handle, async () => {
482
- const ensured = await ensureRuntimeRootLocked(handle, projection);
483
- const agentRoot = join(deps.agentsRoot, handle);
484
- const alreadyProtected = protectedExecutionIds.has(executionId);
485
- protectedExecutionIds.add(executionId);
486
- let lease;
487
- try {
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
- return mapRuntimeRootError(error);
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
- const releaseLease = async () => {
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
- if (captured.decision.kind === "use-advanced") {
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 releaseLease().catch(() => undefined);
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
  }