@nowcrew/daemon 0.6.19 → 0.6.21
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/atomic-no-replace-rename.js +91 -0
- package/dist/completion-retransmitter-logging.js +16 -0
- package/dist/completion-retransmitter.js +39 -4
- package/dist/control-plane-url.js +4 -2
- package/dist/directory-projection-publication.js +105 -0
- package/dist/directory-projection.js +20 -4
- package/dist/execution-journal.js +40 -4
- package/dist/execution-posix-stop-proof.js +82 -0
- package/dist/execution-runner.js +68 -8
- package/dist/local-executor.js +67 -52
- package/dist/machine-info.js +8 -5
- package/dist/project-skills/capability.js +109 -0
- package/dist/project-skills/controller-convergence.js +57 -0
- package/dist/project-skills/controller.js +80 -24
- package/dist/project-skills/initialized-reconciler.js +4 -4
- package/dist/project-skills/projection-state-domain.js +19 -2
- package/dist/project-skills/projection-state-store.js +3 -2
- package/dist/project-skills/projection-state-transaction.js +5 -1
- package/dist/project-skills/projection-state.js +1 -1
- package/dist/project-skills/reconciler.js +275 -102
- package/dist/project-skills/runtime-launch.js +102 -0
- package/dist/project-skills/runtime-root-bootstrap.js +47 -0
- package/dist/project-skills/runtime-root-domain.js +268 -0
- package/dist/project-skills/runtime-root-gc.js +293 -0
- package/dist/project-skills/runtime-root-lease-artifact.js +46 -0
- package/dist/project-skills/runtime-root-leases.js +487 -0
- package/dist/project-skills/runtime-root-source-identity.js +60 -0
- package/dist/project-skills/runtime-root-startup.js +49 -0
- package/dist/project-skills/runtime-root-state-artifact-domain.js +143 -0
- package/dist/project-skills/runtime-root-state-index.js +356 -0
- package/dist/project-skills/runtime-root-store.js +722 -0
- package/dist/project-skills/serve-capability.js +28 -0
- package/dist/project-skills/serve-startup.js +22 -0
- package/dist/project-skills/types.js +1 -0
- package/dist/runtimes/codex-home-migration-cli.js +26 -0
- package/dist/runtimes/codex-home-migration.js +112 -0
- package/dist/runtimes/codex-home.js +200 -17
- package/dist/serve.js +60 -79
- package/dist/supervised-runtime.js +1 -5
- package/package.json +2 -1
|
@@ -3,8 +3,12 @@ import { randomUUID } from "node:crypto";
|
|
|
3
3
|
import { dirname, join, posix, win32 } from "node:path";
|
|
4
4
|
import { isManagedDirectoryProjectionCopy, projectDirectory, } from "../directory-projection.js";
|
|
5
5
|
import { runtimeProjectionLifetime, } from "./agent-projection-coordinator.js";
|
|
6
|
-
import { commitPreparedProjectSkillManifestUpdate, computeProjectSkillBindingDigest, createAppliedProjectSkillManifest, prepareAppliedProjectSkillManifestRemoval, prepareAppliedProjectSkillManifestUpdate, publishPreparedProjectSkillManifestUpdate, readAppliedProjectSkillManifest, rollbackPreparedProjectSkillManifestUpdate, } from "./projection-state.js";
|
|
6
|
+
import { commitPreparedProjectSkillManifestUpdate, computeProjectSkillBindingDigest, computeProjectSkillResolutionDigest, createAppliedProjectSkillManifest, prepareAppliedProjectSkillManifestRemoval, prepareAppliedProjectSkillManifestUpdate, publishPreparedProjectSkillManifestUpdate, readAppliedProjectSkillManifest, recoverAppliedProjectSkillManifestUpdates, rollbackPreparedProjectSkillManifestUpdate, } from "./projection-state.js";
|
|
7
7
|
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";
|
|
8
12
|
import { projectSkillProjectionExpectation, projectSkillResolutionRecords, } from "./scanner.js";
|
|
9
13
|
import { cleanupProjectionStaging, isManagedProjectionRoot, markProjectionStaging, PROJECTION_ROOT_MARKER_NAME, projectionTargets, recoverProjectionSwitch, switchProjectionSet, } from "./projection-set-switch.js";
|
|
10
14
|
export class ProjectProjectionError extends Error {
|
|
@@ -15,6 +19,17 @@ export class ProjectProjectionError extends Error {
|
|
|
15
19
|
this.name = "ProjectProjectionError";
|
|
16
20
|
}
|
|
17
21
|
}
|
|
22
|
+
/** A process may still own its leased root; only startup/journal recovery may clear this state. */
|
|
23
|
+
export class ProjectSkillRuntimeOwnershipUnverifiedError extends AggregateError {
|
|
24
|
+
code = "project_skill_runtime_ownership_unverified";
|
|
25
|
+
constructor() {
|
|
26
|
+
super([
|
|
27
|
+
new Error("project_skill_runtime_lifetime_rejected"),
|
|
28
|
+
new Error("project_skill_runtime_stop_unverified"),
|
|
29
|
+
], "project_skill_runtime_ownership_unverified");
|
|
30
|
+
this.name = "ProjectSkillRuntimeOwnershipUnverifiedError";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
18
33
|
export const decideProjectSkillProjection = (expected, applied) => {
|
|
19
34
|
if (applied === null || expected.generation > applied.generation) {
|
|
20
35
|
return Object.freeze({ kind: "ensure-applied" });
|
|
@@ -25,11 +40,46 @@ export const decideProjectSkillProjection = (expected, applied) => {
|
|
|
25
40
|
if (expected.bindingDigest !== applied.bindingDigest) {
|
|
26
41
|
return Object.freeze({ kind: "reject", code: "skill_projection_snapshot_corrupt" });
|
|
27
42
|
}
|
|
43
|
+
if (applied.rootId === null)
|
|
44
|
+
return Object.freeze({ kind: "ensure-applied" });
|
|
28
45
|
return expected.resolutionDigest === applied.resolutionDigest
|
|
29
46
|
? Object.freeze({ kind: "use-current" })
|
|
30
47
|
: Object.freeze({ kind: "ensure-applied" });
|
|
31
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
|
+
};
|
|
32
67
|
const exists = async (path) => lstat(path).then(() => true, () => false);
|
|
68
|
+
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();
|
|
74
|
+
for (const binding of unique) {
|
|
75
|
+
const owner = names.get(binding.skillName);
|
|
76
|
+
if (owner !== undefined && owner !== binding.projectId) {
|
|
77
|
+
throw new ProjectProjectionError("skill_name_conflict");
|
|
78
|
+
}
|
|
79
|
+
names.set(binding.skillName, binding.projectId);
|
|
80
|
+
}
|
|
81
|
+
return Object.freeze(unique);
|
|
82
|
+
};
|
|
33
83
|
const normalizeWindowsLinkIdentity = (value) => {
|
|
34
84
|
if (/^\\\\\?\\UNC\\/iu.test(value))
|
|
35
85
|
return `\\\\${value.slice(8)}`;
|
|
@@ -107,6 +157,10 @@ export function createProjectSkillsReconciler(deps) {
|
|
|
107
157
|
commit: commitUpdate,
|
|
108
158
|
rollback: rollbackUpdate,
|
|
109
159
|
});
|
|
160
|
+
const runtimeRoots = deps.runtimeRoots ?? createProjectSkillRuntimeRootStore({ platform });
|
|
161
|
+
const runtimeLeases = deps.runtimeLeases ?? createProjectSkillRuntimeLeaseStore({ platform });
|
|
162
|
+
const runtimeRootGc = deps.runtimeRootGc ?? createProjectSkillRuntimeRootGc({ platform, runtimeRoots });
|
|
163
|
+
const protectedExecutionIds = deps.protectedExecutionIds ?? new Set();
|
|
110
164
|
let lastProjectionDiagnostics = Object.freeze([]);
|
|
111
165
|
const reconcileUnlocked = async (handle, bindings, projects = deps.scannedProjects(), recover = true, manifestPublication) => {
|
|
112
166
|
const agentRoot = join(deps.agentsRoot, handle);
|
|
@@ -118,18 +172,7 @@ export function createProjectSkillsReconciler(deps) {
|
|
|
118
172
|
throw new ProjectProjectionError("skill_projection_failed");
|
|
119
173
|
}
|
|
120
174
|
}
|
|
121
|
-
const uniqueBindings =
|
|
122
|
-
`${binding.projectId}\0${binding.skillName}`,
|
|
123
|
-
binding,
|
|
124
|
-
])).values()].sort(compareProjectSkillRefs);
|
|
125
|
-
const names = new Map();
|
|
126
|
-
for (const binding of uniqueBindings) {
|
|
127
|
-
const owner = names.get(binding.skillName);
|
|
128
|
-
if (owner !== undefined && owner !== binding.projectId) {
|
|
129
|
-
throw new ProjectProjectionError("skill_name_conflict");
|
|
130
|
-
}
|
|
131
|
-
names.set(binding.skillName, binding.projectId);
|
|
132
|
-
}
|
|
175
|
+
const uniqueBindings = canonicalConflictFreeBindings(bindings);
|
|
133
176
|
const linked = [];
|
|
134
177
|
const resolutions = [];
|
|
135
178
|
const resolutionRecords = [];
|
|
@@ -229,94 +272,163 @@ export function createProjectSkillsReconciler(deps) {
|
|
|
229
272
|
if (decision.kind === "reject")
|
|
230
273
|
throw new ProjectProjectionError(decision.code);
|
|
231
274
|
};
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
275
|
+
const mapRuntimeRootError = (error) => {
|
|
276
|
+
if (error instanceof ProjectProjectionError)
|
|
277
|
+
throw error;
|
|
278
|
+
if (error instanceof ProjectSkillRuntimeStoreError
|
|
279
|
+
&& error.code === "skill_projection_snapshot_corrupt") {
|
|
280
|
+
throw new ProjectProjectionError("skill_projection_snapshot_corrupt");
|
|
236
281
|
}
|
|
237
|
-
|
|
282
|
+
throw new ProjectProjectionError("skill_projection_failed");
|
|
283
|
+
};
|
|
284
|
+
const exactManifest = (left, right) => left !== null && JSON.stringify(left) === JSON.stringify(right);
|
|
285
|
+
const warningsFor = (records) => Object.freeze(records
|
|
286
|
+
.filter((record) => record.mode === "missing")
|
|
287
|
+
.map((record) => Object.freeze({
|
|
288
|
+
projectId: record.projectId,
|
|
289
|
+
skillName: record.skillName,
|
|
290
|
+
code: "project_skill_unavailable",
|
|
291
|
+
})));
|
|
292
|
+
const withWarnings = (decision, warnings) => {
|
|
293
|
+
if (decision.kind === "reject" || warnings.length === 0) {
|
|
294
|
+
return decision;
|
|
295
|
+
}
|
|
296
|
+
return Object.freeze({ ...decision, warnings });
|
|
297
|
+
};
|
|
298
|
+
const actualResolutionRecords = async (bindings, projects) => Object.freeze(await Promise.all(projectSkillResolutionRecords(bindings, projects, platform).map(async (record) => {
|
|
299
|
+
if (record.mode === "missing" || record.sourcePath === null)
|
|
300
|
+
return record;
|
|
301
|
+
const sourcePath = record.sourcePath;
|
|
302
|
+
const [source, skillFile] = await Promise.all([
|
|
303
|
+
lstat(sourcePath).catch(() => null),
|
|
304
|
+
stat(join(sourcePath, "SKILL.md")).catch(() => null),
|
|
305
|
+
]);
|
|
306
|
+
if (source?.isDirectory() === true
|
|
307
|
+
&& !source.isSymbolicLink()
|
|
308
|
+
&& skillFile?.isFile() === true) {
|
|
309
|
+
return record;
|
|
310
|
+
}
|
|
311
|
+
return Object.freeze({
|
|
312
|
+
projectId: record.projectId,
|
|
313
|
+
skillName: record.skillName,
|
|
314
|
+
sourcePath: null,
|
|
315
|
+
mode: "missing",
|
|
316
|
+
});
|
|
317
|
+
})));
|
|
318
|
+
const commitAppliedRoot = async (agentRoot, applied) => {
|
|
319
|
+
const update = await prepareUpdate(agentRoot, applied, readOptions);
|
|
320
|
+
await publishUpdate(update);
|
|
321
|
+
await deps.afterManifestPublishedBeforeCommit?.();
|
|
322
|
+
await commitUpdate(update);
|
|
323
|
+
if (!exactManifest(await readApplied(agentRoot, readOptions), applied)) {
|
|
238
324
|
throw new ProjectProjectionError("skill_projection_failed");
|
|
239
325
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
326
|
+
};
|
|
327
|
+
const ensureRuntimeRootLocked = async (handle, requested, options = {}) => {
|
|
328
|
+
const agentRoot = join(deps.agentsRoot, handle);
|
|
329
|
+
try {
|
|
330
|
+
canonicalConflictFreeBindings(requested.bindings);
|
|
331
|
+
await recoverProjectionSwitch(agentRoot, deps.syncDirectory, manifestOperations);
|
|
332
|
+
await recoverAppliedProjectSkillManifestUpdates(agentRoot, readOptions);
|
|
333
|
+
await runtimeRoots.recover(agentRoot);
|
|
334
|
+
const projects = deps.scannedProjects();
|
|
335
|
+
const wireRequest = Object.freeze({
|
|
336
|
+
bindings: requested.bindings,
|
|
337
|
+
generation: requested.generation,
|
|
338
|
+
});
|
|
339
|
+
let cachedRequested;
|
|
340
|
+
try {
|
|
341
|
+
cachedRequested = projectSkillProjectionExpectation(wireRequest, projects, platform);
|
|
342
|
+
}
|
|
343
|
+
catch (error) {
|
|
344
|
+
if (error instanceof ProjectProjectionError)
|
|
345
|
+
throw error;
|
|
346
|
+
throw new ProjectProjectionError("skill_projection_snapshot_corrupt");
|
|
347
|
+
}
|
|
348
|
+
const localResolutions = await actualResolutionRecords(cachedRequested.bindings, projects);
|
|
349
|
+
const localRequested = Object.freeze({
|
|
350
|
+
...cachedRequested,
|
|
351
|
+
resolutionDigest: computeProjectSkillResolutionDigest(localResolutions, platform),
|
|
252
352
|
});
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
353
|
+
validateExpectation(localRequested);
|
|
354
|
+
if ("resolutionDigest" in requested)
|
|
355
|
+
validateExpectation(requested);
|
|
356
|
+
const current = await readApplied(agentRoot, readOptions);
|
|
357
|
+
const callerDecision = decideProjectSkillProjection(localRequested, current);
|
|
358
|
+
rejectDecision(callerDecision);
|
|
359
|
+
const desiredBindings = callerDecision.kind === "use-advanced" && current !== null
|
|
360
|
+
? current.bindings
|
|
361
|
+
: localRequested.bindings;
|
|
362
|
+
const desiredGeneration = callerDecision.kind === "use-advanced" && current !== null
|
|
363
|
+
? current.generation
|
|
364
|
+
: localRequested.generation;
|
|
365
|
+
const resolutions = callerDecision.kind === "use-advanced"
|
|
366
|
+
? await actualResolutionRecords(desiredBindings, projects)
|
|
367
|
+
: localResolutions;
|
|
368
|
+
const desiredExpectation = Object.freeze({
|
|
369
|
+
bindings: desiredBindings,
|
|
370
|
+
generation: desiredGeneration,
|
|
371
|
+
bindingDigest: computeProjectSkillBindingDigest(desiredBindings),
|
|
372
|
+
resolutionDigest: computeProjectSkillResolutionDigest(resolutions, platform),
|
|
258
373
|
});
|
|
259
|
-
const
|
|
374
|
+
const inventory = await runtimeRoots.list(agentRoot);
|
|
375
|
+
const currentRecord = current?.rootId === null || current?.rootId === undefined
|
|
376
|
+
? undefined
|
|
377
|
+
: inventory.find((record) => record.rootId === current.rootId);
|
|
378
|
+
const reusableIdentity = current !== null
|
|
379
|
+
&& current.rootId !== null
|
|
380
|
+
&& current.generation === desiredExpectation.generation
|
|
381
|
+
&& current.bindingDigest === desiredExpectation.bindingDigest
|
|
382
|
+
&& current.resolutionDigest === desiredExpectation.resolutionDigest
|
|
383
|
+
&& currentRecord?.materializationRevision === PROJECT_SKILL_RUNTIME_MATERIALIZATION_REVISION
|
|
384
|
+
&& currentRecord.bindingDigest === current.bindingDigest
|
|
385
|
+
&& currentRecord.resolutionDigest === current.resolutionDigest;
|
|
386
|
+
let runtimeRoot;
|
|
387
|
+
let published = false;
|
|
388
|
+
const reusableRootId = reusableIdentity ? current.rootId : null;
|
|
389
|
+
const currentInspection = reusableRootId === null
|
|
390
|
+
? null
|
|
391
|
+
: await runtimeRoots.inspect(agentRoot, reusableRootId);
|
|
392
|
+
const reusable = currentInspection !== null
|
|
393
|
+
&& (!options.refreshCopyProjection || !currentInspection.containsCopyProjection);
|
|
394
|
+
if (reusable) {
|
|
395
|
+
runtimeRoot = currentInspection.descriptor;
|
|
396
|
+
}
|
|
397
|
+
else {
|
|
398
|
+
runtimeRoot = await runtimeRoots.publish({
|
|
399
|
+
agentRoot,
|
|
400
|
+
bindingDigest: desiredExpectation.bindingDigest,
|
|
401
|
+
resolutionDigest: desiredExpectation.resolutionDigest,
|
|
402
|
+
resolutions,
|
|
403
|
+
});
|
|
404
|
+
published = true;
|
|
405
|
+
await deps.afterRootPublishedBeforeAppliedCommit?.(runtimeRoot);
|
|
406
|
+
const applied = createAppliedProjectSkillManifest({
|
|
407
|
+
bindings: desiredBindings,
|
|
408
|
+
generation: desiredGeneration,
|
|
409
|
+
resolutions,
|
|
410
|
+
rootId: runtimeRoot.rootId,
|
|
411
|
+
platform,
|
|
412
|
+
});
|
|
413
|
+
await commitAppliedRoot(agentRoot, applied);
|
|
414
|
+
}
|
|
415
|
+
const baseDecision = callerDecision.kind === "use-advanced"
|
|
416
|
+
? callerDecision
|
|
417
|
+
: Object.freeze({ kind: published ? "ensure-applied" : "use-current" });
|
|
260
418
|
return Object.freeze({
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
verify: async () => {
|
|
264
|
-
const rechecked = await readApplied(agentRoot, readOptions);
|
|
265
|
-
const finalDecision = decideProjectSkillProjection(lockedExpectation, rechecked);
|
|
266
|
-
rejectDecision(finalDecision);
|
|
267
|
-
if (finalDecision.kind !== "use-current") {
|
|
268
|
-
throw new ProjectProjectionError("skill_projection_failed");
|
|
269
|
-
}
|
|
270
|
-
},
|
|
271
|
-
...(deps.afterManifestPublishedBeforeCommit === undefined
|
|
272
|
-
? {}
|
|
273
|
-
: { afterPublishedBeforeCommit: deps.afterManifestPublishedBeforeCommit }),
|
|
274
|
-
...(deps.unlinkSwitchJournal === undefined
|
|
275
|
-
? {}
|
|
276
|
-
: { unlinkJournal: deps.unlinkSwitchJournal }),
|
|
419
|
+
decision: withWarnings(baseDecision, warningsFor(resolutions)),
|
|
420
|
+
runtimeRoot,
|
|
277
421
|
});
|
|
278
|
-
});
|
|
279
|
-
const warnings = reconciled.resolutions
|
|
280
|
-
.filter((resolution) => resolution.status === "unavailable")
|
|
281
|
-
.map((resolution) => Object.freeze({
|
|
282
|
-
projectId: resolution.projectId,
|
|
283
|
-
skillName: resolution.skillName,
|
|
284
|
-
code: "project_skill_unavailable",
|
|
285
|
-
}));
|
|
286
|
-
return warnings.length === 0
|
|
287
|
-
? Object.freeze({ kind: "ensure-applied" })
|
|
288
|
-
: Object.freeze({ kind: "ensure-applied", warnings: Object.freeze(warnings) });
|
|
289
|
-
};
|
|
290
|
-
const ensureApplied = async (handle, expected) => {
|
|
291
|
-
validateExpectation(expected);
|
|
292
|
-
return deps.coordinator.runExclusive(deps.agentsRoot, handle, () => ensureAppliedLocked(handle, expected));
|
|
293
|
-
};
|
|
294
|
-
const ensureSnapshot = async (handle, snapshot) => deps.coordinator.runExclusive(deps.agentsRoot, handle, async () => {
|
|
295
|
-
const projects = deps.scannedProjects();
|
|
296
|
-
let expected;
|
|
297
|
-
try {
|
|
298
|
-
expected = projectSkillProjectionExpectation(snapshot, projects, platform);
|
|
299
|
-
validateExpectation(expected);
|
|
300
422
|
}
|
|
301
423
|
catch (error) {
|
|
302
|
-
|
|
303
|
-
throw error;
|
|
304
|
-
throw new ProjectProjectionError("skill_projection_snapshot_corrupt");
|
|
424
|
+
return mapRuntimeRootError(error);
|
|
305
425
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
projectId: record.projectId,
|
|
313
|
-
skillName: record.skillName,
|
|
314
|
-
code: "project_skill_unavailable",
|
|
315
|
-
}));
|
|
316
|
-
return warnings.length === 0
|
|
317
|
-
? decision
|
|
318
|
-
: Object.freeze({ kind: "use-current", warnings: Object.freeze(warnings) });
|
|
319
|
-
});
|
|
426
|
+
};
|
|
427
|
+
const ensureApplied = async (handle, expected, options = {}) => {
|
|
428
|
+
validateExpectation(expected);
|
|
429
|
+
return deps.coordinator.runExclusive(deps.agentsRoot, handle, async () => (await ensureRuntimeRootLocked(handle, expected, options)).decision);
|
|
430
|
+
};
|
|
431
|
+
const ensureSnapshot = async (handle, snapshot, options = {}) => deps.coordinator.runExclusive(deps.agentsRoot, handle, async () => (await ensureRuntimeRootLocked(handle, snapshot, options)).decision);
|
|
320
432
|
const reconcileLegacyUnlocked = async (handle, bindings) => {
|
|
321
433
|
const agentRoot = join(deps.agentsRoot, handle);
|
|
322
434
|
try {
|
|
@@ -351,26 +463,87 @@ export function createProjectSkillsReconciler(deps) {
|
|
|
351
463
|
ensureSnapshot,
|
|
352
464
|
ensureApplied,
|
|
353
465
|
projectionDiagnostics: () => lastProjectionDiagnostics,
|
|
354
|
-
async prepareAndLaunch(agentsRoot, handle, projection, launch, onWarning) {
|
|
466
|
+
async prepareAndLaunch(agentsRoot, executionId, handle, projection, launch, onWarning) {
|
|
355
467
|
if (agentsRoot !== deps.agentsRoot) {
|
|
356
468
|
throw new ProjectProjectionError("skill_projection_failed");
|
|
357
469
|
}
|
|
358
470
|
if (Array.isArray(projection)) {
|
|
359
471
|
return deps.coordinator.runExclusiveUntil(deps.agentsRoot, handle, async () => {
|
|
360
472
|
await reconcileLegacyUnlocked(handle, projection);
|
|
361
|
-
return launch();
|
|
473
|
+
return launch(undefined);
|
|
362
474
|
}, runtimeProjectionLifetime);
|
|
363
475
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
476
|
+
let captured;
|
|
477
|
+
try {
|
|
478
|
+
captured = await deps.coordinator.runExclusive(deps.agentsRoot, handle, async () => {
|
|
479
|
+
const ensured = await ensureRuntimeRootLocked(handle, projection);
|
|
480
|
+
const agentRoot = join(deps.agentsRoot, handle);
|
|
481
|
+
const alreadyProtected = protectedExecutionIds.has(executionId);
|
|
482
|
+
protectedExecutionIds.add(executionId);
|
|
483
|
+
let lease;
|
|
484
|
+
try {
|
|
485
|
+
lease = await runtimeLeases.acquire(agentRoot, {
|
|
486
|
+
executionId,
|
|
487
|
+
rootId: ensured.runtimeRoot.rootId,
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
catch (error) {
|
|
491
|
+
let durableLeaseMayExist = true;
|
|
492
|
+
try {
|
|
493
|
+
durableLeaseMayExist = (await runtimeLeases.protectedExecutionIds(agentRoot))
|
|
494
|
+
.includes(executionId);
|
|
495
|
+
}
|
|
496
|
+
catch {
|
|
497
|
+
// Inventory ambiguity retains protection until startup recovery can fail closed.
|
|
498
|
+
}
|
|
499
|
+
if (!durableLeaseMayExist && !alreadyProtected) {
|
|
500
|
+
protectedExecutionIds.delete(executionId);
|
|
501
|
+
}
|
|
502
|
+
throw error;
|
|
503
|
+
}
|
|
504
|
+
return Object.freeze({ ...ensured, lease });
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
catch (error) {
|
|
508
|
+
return mapRuntimeRootError(error);
|
|
509
|
+
}
|
|
510
|
+
const releaseLease = async () => {
|
|
511
|
+
await deps.coordinator.runExclusive(deps.agentsRoot, handle, async () => {
|
|
512
|
+
await captured.lease.release();
|
|
513
|
+
protectedExecutionIds.delete(captured.lease.executionId);
|
|
514
|
+
try {
|
|
515
|
+
const agentRoot = join(deps.agentsRoot, handle);
|
|
516
|
+
const leases = await runtimeLeases.list(agentRoot);
|
|
517
|
+
const applied = await readApplied(agentRoot, readOptions);
|
|
518
|
+
await runtimeRootGc.collect(agentRoot, {
|
|
519
|
+
currentRootId: applied?.rootId ?? null,
|
|
520
|
+
leasedRootIds: leases.map(({ rootId }) => rootId),
|
|
521
|
+
keepHistory: 2,
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
catch {
|
|
525
|
+
// GC is best-effort after a successful lease release.
|
|
526
|
+
}
|
|
527
|
+
});
|
|
528
|
+
};
|
|
529
|
+
try {
|
|
530
|
+
if (captured.decision.kind === "use-advanced") {
|
|
531
|
+
onWarning?.({ code: captured.decision.warning });
|
|
532
|
+
}
|
|
533
|
+
if ("warnings" in captured.decision) {
|
|
534
|
+
for (const warning of captured.decision.warnings ?? [])
|
|
535
|
+
onWarning?.(warning);
|
|
536
|
+
}
|
|
537
|
+
const child = ownershipVerifiedRuntimeProjection(await launch(captured.runtimeRoot));
|
|
538
|
+
void verifiedRuntimeProjectionLifetime(child).then(releaseLease, () => undefined).catch(() => undefined);
|
|
539
|
+
return child;
|
|
540
|
+
}
|
|
541
|
+
catch (error) {
|
|
542
|
+
if (error instanceof ProjectSkillRuntimeOwnershipUnverifiedError)
|
|
543
|
+
throw error;
|
|
544
|
+
await releaseLease().catch(() => undefined);
|
|
545
|
+
throw error;
|
|
372
546
|
}
|
|
373
|
-
return launch();
|
|
374
547
|
},
|
|
375
548
|
};
|
|
376
549
|
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { resolveOrderedUniqueClaudeDirectories } from "../runtimes/claude.js";
|
|
3
|
+
import { ProjectProjectionError, } from "./reconciler.js";
|
|
4
|
+
const runtimeRootSecrets = (root) => Object.freeze([...new Set([
|
|
5
|
+
root.rootDirectory,
|
|
6
|
+
root.codexSkillRoot,
|
|
7
|
+
root.claudeAdditionalDirectory,
|
|
8
|
+
root.rootId,
|
|
9
|
+
].flatMap((value) => [
|
|
10
|
+
value,
|
|
11
|
+
value.replaceAll("\\", "/"),
|
|
12
|
+
value.replaceAll("/", "\\"),
|
|
13
|
+
]))].sort((left, right) => right.length - left.length));
|
|
14
|
+
export function redactProjectSkillRuntimeRootText(text, root) {
|
|
15
|
+
if (root === undefined)
|
|
16
|
+
return text;
|
|
17
|
+
return runtimeRootSecrets(root)
|
|
18
|
+
.reduce((redacted, secret) => redacted.replaceAll(secret, "[project-skill-root]"), text);
|
|
19
|
+
}
|
|
20
|
+
export function redactProjectSkillRuntimeRootError(error, root) {
|
|
21
|
+
if (!(error instanceof Error)) {
|
|
22
|
+
return typeof error === "string" ? redactProjectSkillRuntimeRootText(error, root) : error;
|
|
23
|
+
}
|
|
24
|
+
const message = redactProjectSkillRuntimeRootText(error.message, root);
|
|
25
|
+
if (message === error.message)
|
|
26
|
+
return error;
|
|
27
|
+
const redacted = new Error(message);
|
|
28
|
+
redacted.name = error.name;
|
|
29
|
+
return redacted;
|
|
30
|
+
}
|
|
31
|
+
/** Recursively redacts exact v2 root identities before Runtime output crosses daemon boundaries. */
|
|
32
|
+
export function redactProjectSkillRuntimeRootValue(value, root) {
|
|
33
|
+
if (root === undefined)
|
|
34
|
+
return value;
|
|
35
|
+
if (typeof value === "string")
|
|
36
|
+
return redactProjectSkillRuntimeRootText(value, root);
|
|
37
|
+
if (Array.isArray(value)) {
|
|
38
|
+
return value.map((item) => redactProjectSkillRuntimeRootValue(item, root));
|
|
39
|
+
}
|
|
40
|
+
if (typeof value === "object" && value !== null) {
|
|
41
|
+
return Object.fromEntries(Object.entries(value)
|
|
42
|
+
.map(([key, item]) => [key, redactProjectSkillRuntimeRootValue(item, root)]));
|
|
43
|
+
}
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
/** Maps one captured daemon-local root to the exact native Runtime registration seam. */
|
|
47
|
+
export function projectSkillRuntimeLaunchFacts(runtime, root) {
|
|
48
|
+
if (root === undefined)
|
|
49
|
+
return Object.freeze({});
|
|
50
|
+
if (runtime === "codex") {
|
|
51
|
+
return Object.freeze({ codexSkillRoot: root.codexSkillRoot });
|
|
52
|
+
}
|
|
53
|
+
if (runtime === "claude") {
|
|
54
|
+
return Object.freeze({ claudeAdditionalDirectory: root.claudeAdditionalDirectory });
|
|
55
|
+
}
|
|
56
|
+
throw new ProjectProjectionError("skill_projection_failed");
|
|
57
|
+
}
|
|
58
|
+
/** Builds Runtime-native arrays while keeping v1 shared roots separate from v2 exact roots. */
|
|
59
|
+
export async function projectSkillRuntimeDirectories(input) {
|
|
60
|
+
const facts = projectSkillRuntimeLaunchFacts(input.runtime, input.runtimeRoot);
|
|
61
|
+
const codexSkillRoots = input.runtime === "codex"
|
|
62
|
+
&& (input.runtimeRoot !== undefined || input.projectContext !== undefined)
|
|
63
|
+
? Object.freeze([
|
|
64
|
+
...(facts.codexSkillRoot === undefined
|
|
65
|
+
? input.projectSkillsPresent ? [join(input.agentRoot, ".agents", "skills")] : []
|
|
66
|
+
: [facts.codexSkillRoot]),
|
|
67
|
+
...(input.abilitySkillRoot === undefined ? [] : [input.abilitySkillRoot]),
|
|
68
|
+
])
|
|
69
|
+
: Object.freeze([]);
|
|
70
|
+
if (input.runtime !== "claude") {
|
|
71
|
+
return Object.freeze({ codexSkillRoots, claudeAdditionalDirectories: Object.freeze([]) });
|
|
72
|
+
}
|
|
73
|
+
if (input.runtimeRoot !== undefined) {
|
|
74
|
+
if (facts.claudeAdditionalDirectory === undefined) {
|
|
75
|
+
throw new ProjectProjectionError("skill_projection_failed");
|
|
76
|
+
}
|
|
77
|
+
return Object.freeze({
|
|
78
|
+
codexSkillRoots,
|
|
79
|
+
claudeAdditionalDirectories: await resolveOrderedUniqueClaudeDirectories([
|
|
80
|
+
...(input.projectContext?.secondary.map((project) => project.root) ?? []),
|
|
81
|
+
facts.claudeAdditionalDirectory,
|
|
82
|
+
...(input.abilitySkillRoot === undefined ? [] : [input.abilitySkillRoot]),
|
|
83
|
+
], input.projectContext?.primary === undefined ? [] : [input.projectContext.primary.root]),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
const claudeAdditionalDirectories = input.projectContext === undefined
|
|
87
|
+
? !input.projectSkillsPresent && input.abilitySkillRoot === undefined
|
|
88
|
+
? Object.freeze([])
|
|
89
|
+
: await resolveOrderedUniqueClaudeDirectories([
|
|
90
|
+
join(input.agentRoot, ".crew", "claude-skills"),
|
|
91
|
+
input.agentRoot,
|
|
92
|
+
...(input.abilitySkillRoot === undefined ? [] : [input.abilitySkillRoot]),
|
|
93
|
+
])
|
|
94
|
+
: await resolveOrderedUniqueClaudeDirectories([
|
|
95
|
+
...input.projectContext.secondary.map((project) => project.root),
|
|
96
|
+
...(input.projectSkillsPresent
|
|
97
|
+
? [join(input.agentRoot, ".crew", "claude-skills")]
|
|
98
|
+
: []),
|
|
99
|
+
...(input.abilitySkillRoot === undefined ? [] : [input.abilitySkillRoot]),
|
|
100
|
+
], input.projectContext.primary === undefined ? [] : [input.projectContext.primary.root]);
|
|
101
|
+
return Object.freeze({ codexSkillRoots, claudeAdditionalDirectories });
|
|
102
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { ProjectSkillRuntimeStoreError } from "./runtime-root-domain.js";
|
|
2
|
+
/**
|
|
3
|
+
* Generic startup hard gate. The callback is the only place a journal owner may be constructed or
|
|
4
|
+
* initialized, and is unreachable until every durable lease inventory has populated the synchronous
|
|
5
|
+
* retention snapshot. Task 9 wires this gate into `serve`; Task 5 deliberately does not advertise v2.
|
|
6
|
+
*/
|
|
7
|
+
export async function initializeAfterProjectSkillLeaseProtection(input) {
|
|
8
|
+
const protectedExecutionIds = new Set();
|
|
9
|
+
for (const agentRoot of input.agentRoots) {
|
|
10
|
+
for (const executionId of await input.leases.protectedExecutionIds(agentRoot)) {
|
|
11
|
+
protectedExecutionIds.add(executionId);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return input.initialize(protectedExecutionIds);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Runs only after execution-journal restart reconciliation has settled. A lease without a terminal
|
|
18
|
+
* journal fact remains durable and prevents the caller from marking Project Skill v2 ready.
|
|
19
|
+
*/
|
|
20
|
+
export async function recoverProjectSkillRuntimeRoots(input) {
|
|
21
|
+
for (const agentRoot of input.agentRoots) {
|
|
22
|
+
await input.roots.recover(agentRoot);
|
|
23
|
+
let unresolvedLease = false;
|
|
24
|
+
await input.leases.recover(agentRoot, async (executionId) => {
|
|
25
|
+
const entry = await input.journal.get(executionId);
|
|
26
|
+
const releasable = entry?.state === "completed" || entry?.state === "interrupted";
|
|
27
|
+
if (!releasable)
|
|
28
|
+
unresolvedLease = true;
|
|
29
|
+
return releasable;
|
|
30
|
+
});
|
|
31
|
+
if (unresolvedLease) {
|
|
32
|
+
throw new ProjectSkillRuntimeStoreError("skill_projection_snapshot_corrupt");
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
await input.afterLeaseRecovery?.();
|
|
36
|
+
for (const agentRoot of input.agentRoots) {
|
|
37
|
+
if (input.gc !== undefined && input.leases.list !== undefined && input.currentRootId !== undefined) {
|
|
38
|
+
const leased = await input.leases.list(agentRoot);
|
|
39
|
+
const currentRootId = await input.currentRootId(agentRoot);
|
|
40
|
+
await input.gc.collect(agentRoot, {
|
|
41
|
+
currentRootId,
|
|
42
|
+
leasedRootIds: leased.map(({ rootId }) => rootId),
|
|
43
|
+
keepHistory: 2,
|
|
44
|
+
}).catch(() => undefined);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|