@nowcrew/daemon 0.6.18 → 0.6.20
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/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 +73 -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/provider-env.js +3 -0
- package/dist/runtimes/codex-home.js +50 -0
- package/dist/serve.js +58 -73
- package/dist/supervised-runtime.js +1 -5
- package/package.json +2 -2
|
@@ -10,11 +10,11 @@ export function initializedProjectSkillsReconciler(ensureInitialized, reconciler
|
|
|
10
10
|
async reconcile(handle, bindings) {
|
|
11
11
|
return (await ready()).reconcile(handle, bindings);
|
|
12
12
|
},
|
|
13
|
-
async ensureSnapshot(handle, snapshot) {
|
|
14
|
-
return (await ready()).ensureSnapshot(handle, snapshot);
|
|
13
|
+
async ensureSnapshot(handle, snapshot, options) {
|
|
14
|
+
return (await ready()).ensureSnapshot(handle, snapshot, options);
|
|
15
15
|
},
|
|
16
|
-
async prepareAndLaunch(agentsRoot, handle, projection, launch, onWarning) {
|
|
17
|
-
return (await ready()).prepareAndLaunch(agentsRoot, handle, projection, launch, onWarning);
|
|
16
|
+
async prepareAndLaunch(agentsRoot, executionId, handle, projection, launch, onWarning) {
|
|
17
|
+
return (await ready()).prepareAndLaunch(agentsRoot, executionId, handle, projection, launch, onWarning);
|
|
18
18
|
},
|
|
19
19
|
};
|
|
20
20
|
}
|
|
@@ -6,6 +6,7 @@ const MANAGED_BY = "nowcrew-project-skills-v2";
|
|
|
6
6
|
const MAX_SOURCE_PATH_BYTES = 16 * 1024;
|
|
7
7
|
const MAX_GENERATION = 2_147_483_647;
|
|
8
8
|
const DIGEST = /^sha256:[a-f0-9]{64}$/u;
|
|
9
|
+
const CANONICAL_ROOT_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u;
|
|
9
10
|
export class ProjectSkillProjectionStateError extends Error {
|
|
10
11
|
code;
|
|
11
12
|
constructor(code) {
|
|
@@ -106,7 +107,8 @@ export function createAppliedProjectSkillManifest(input) {
|
|
|
106
107
|
const bindings = canonicalBindings(input.bindings);
|
|
107
108
|
const resolutions = normalizeProjectSkillResolutionRecords(input.resolutions, input.platform);
|
|
108
109
|
if (bindings.length !== resolutions.length
|
|
109
|
-
|| bindings.some((binding, index) => !sameBinding(binding, resolutions[index]))
|
|
110
|
+
|| bindings.some((binding, index) => !sameBinding(binding, resolutions[index]))
|
|
111
|
+
|| !CANONICAL_ROOT_ID.test(input.rootId)) {
|
|
110
112
|
return corrupt();
|
|
111
113
|
}
|
|
112
114
|
return Object.freeze({
|
|
@@ -115,19 +117,26 @@ export function createAppliedProjectSkillManifest(input) {
|
|
|
115
117
|
bindings,
|
|
116
118
|
bindingDigest: digest(bindings),
|
|
117
119
|
resolutionDigest: digest(resolutions),
|
|
120
|
+
rootId: input.rootId,
|
|
118
121
|
});
|
|
119
122
|
}
|
|
120
123
|
const ProjectSkillRefSchema = z.object({
|
|
121
124
|
projectId: z.string().min(1).max(MAX_PROJECT_ID_LENGTH),
|
|
122
125
|
skillName: z.string().min(1).max(MAX_PROJECT_SKILL_NAME_LENGTH),
|
|
123
126
|
}).strict();
|
|
124
|
-
const
|
|
127
|
+
const LegacyAppliedManifestSchema = z.object({
|
|
125
128
|
managedBy: z.literal(MANAGED_BY),
|
|
126
129
|
generation: z.number().int().min(0).max(MAX_GENERATION),
|
|
127
130
|
bindings: z.array(ProjectSkillRefSchema).max(MAX_AGENT_PROJECT_SKILL_BINDINGS),
|
|
128
131
|
bindingDigest: z.string().regex(DIGEST),
|
|
129
132
|
resolutionDigest: z.string().regex(DIGEST),
|
|
130
133
|
}).strict();
|
|
134
|
+
const AppliedManifestSchema = z.union([
|
|
135
|
+
LegacyAppliedManifestSchema.extend({
|
|
136
|
+
rootId: z.string().regex(CANONICAL_ROOT_ID),
|
|
137
|
+
}).strict(),
|
|
138
|
+
LegacyAppliedManifestSchema,
|
|
139
|
+
]);
|
|
131
140
|
/** Strict local-store boundary parser; not re-exported from the public projection-state facade. */
|
|
132
141
|
export const parseAppliedProjectSkillManifest = (candidate) => {
|
|
133
142
|
if (candidate && typeof candidate === "object" && "managedBy" in candidate
|
|
@@ -149,5 +158,13 @@ export const parseAppliedProjectSkillManifest = (candidate) => {
|
|
|
149
158
|
bindings,
|
|
150
159
|
bindingDigest: parsed.data.bindingDigest,
|
|
151
160
|
resolutionDigest: parsed.data.resolutionDigest,
|
|
161
|
+
rootId: "rootId" in parsed.data ? parsed.data.rootId : null,
|
|
152
162
|
});
|
|
153
163
|
};
|
|
164
|
+
/** Publication boundary parser; it must run before creating or settling any filesystem state. */
|
|
165
|
+
export const parsePublishableAppliedProjectSkillManifest = (candidate) => {
|
|
166
|
+
const parsed = parseAppliedProjectSkillManifest(candidate);
|
|
167
|
+
if (parsed.rootId === null)
|
|
168
|
+
return corrupt();
|
|
169
|
+
return Object.freeze({ ...parsed, rootId: parsed.rootId });
|
|
170
|
+
};
|
|
@@ -3,7 +3,7 @@ import { chmod, link, lstat, mkdir, open, opendir, rename, rmdir, unlink } from
|
|
|
3
3
|
import { basename, dirname, join } from "node:path";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { durableDirectorySync } from "../atomic-private-write.js";
|
|
6
|
-
import { parseAppliedProjectSkillManifest, ProjectSkillProjectionStateError, } from "./projection-state-domain.js";
|
|
6
|
+
import { parseAppliedProjectSkillManifest, parsePublishableAppliedProjectSkillManifest, ProjectSkillProjectionStateError, } from "./projection-state-domain.js";
|
|
7
7
|
const MANIFEST_FILE_NAME = "project-skills-v2.json";
|
|
8
8
|
const MAX_MANIFEST_BYTES = 256 * 1024;
|
|
9
9
|
const MAX_CONSISTENT_READ_ATTEMPTS = 6;
|
|
@@ -665,7 +665,7 @@ export async function readAppliedProjectSkillManifest(agentRoot, options = {}) {
|
|
|
665
665
|
: parseManifestRaw(snapshot.raw);
|
|
666
666
|
}
|
|
667
667
|
export async function writeAppliedProjectSkillManifest(agentRoot, manifest, options = {}) {
|
|
668
|
-
const parsed =
|
|
668
|
+
const parsed = parsePublishableAppliedProjectSkillManifest(manifest);
|
|
669
669
|
const syncDirectory = projectionDirectorySync(options);
|
|
670
670
|
await realOwnedDirectory(agentRoot, false);
|
|
671
671
|
const stateDirectory = join(agentRoot, ".crew");
|
|
@@ -827,6 +827,7 @@ export const projectSkillManifestStoreInternals = Object.freeze({
|
|
|
827
827
|
isAbsenceMarkerRaw,
|
|
828
828
|
loadOperation,
|
|
829
829
|
parseAppliedProjectSkillManifest,
|
|
830
|
+
parsePublishableAppliedProjectSkillManifest,
|
|
830
831
|
parseManifestRaw,
|
|
831
832
|
pathUnmanaged,
|
|
832
833
|
projectionDirectorySync,
|
|
@@ -190,11 +190,15 @@ const prepareManifestTransition = async (agentRoot, next, options = {}) => {
|
|
|
190
190
|
return Object.freeze({ agentRoot, operationId: operation.intent.nonce, previous, next });
|
|
191
191
|
};
|
|
192
192
|
export async function prepareAppliedProjectSkillManifestUpdate(agentRoot, manifest, options = {}) {
|
|
193
|
-
return prepareManifestTransition(agentRoot, store.
|
|
193
|
+
return prepareManifestTransition(agentRoot, store.parsePublishableAppliedProjectSkillManifest(manifest), options);
|
|
194
194
|
}
|
|
195
195
|
export async function prepareAppliedProjectSkillManifestRemoval(agentRoot, options = {}) {
|
|
196
196
|
return prepareManifestTransition(agentRoot, null, options);
|
|
197
197
|
}
|
|
198
|
+
/** Settles a durable standalone applied-manifest operation before current-state selection. */
|
|
199
|
+
export async function recoverAppliedProjectSkillManifestUpdates(agentRoot, options = {}) {
|
|
200
|
+
await stateContext(agentRoot, options, true);
|
|
201
|
+
}
|
|
198
202
|
export async function publishPreparedProjectSkillManifestUpdate(update, options = {}) {
|
|
199
203
|
if (await completedWithoutOperation(update, update.next, options))
|
|
200
204
|
return;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export { computeProjectSkillBindingDigest, computeProjectSkillResolutionDigest, createAppliedProjectSkillManifest, normalizeProjectSkillResolutionRecords, ProjectSkillProjectionStateError, } from "./projection-state-domain.js";
|
|
2
2
|
export { appliedProjectSkillManifestPath, readAppliedProjectSkillManifest, writeAppliedProjectSkillManifest, } from "./projection-state-store.js";
|
|
3
|
-
export { commitPreparedProjectSkillManifestUpdate, parseProjectSkillManifestUpdateRecord, prepareAppliedProjectSkillManifestRemoval, prepareAppliedProjectSkillManifestUpdate, preparedProjectSkillManifestUpdateFromRecord, projectSkillManifestUpdateRecord, publishPreparedProjectSkillManifestUpdate, rollbackPreparedProjectSkillManifestUpdate, } from "./projection-state-transaction.js";
|
|
3
|
+
export { commitPreparedProjectSkillManifestUpdate, parseProjectSkillManifestUpdateRecord, prepareAppliedProjectSkillManifestRemoval, prepareAppliedProjectSkillManifestUpdate, preparedProjectSkillManifestUpdateFromRecord, projectSkillManifestUpdateRecord, publishPreparedProjectSkillManifestUpdate, recoverAppliedProjectSkillManifestUpdates, rollbackPreparedProjectSkillManifestUpdate, } from "./projection-state-transaction.js";
|
|
@@ -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
|
}
|