@nowcrew/daemon 0.6.4 → 0.6.5
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/agent-ability/materializer.js +173 -14
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { chmod, cp, lstat, mkdir, readFile, readdir, readlink, rename, rm, symlink, writeFile } from "node:fs/promises";
|
|
3
|
-
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
3
|
+
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { parse } from "yaml";
|
|
5
5
|
import { dslog } from "../slog.js";
|
|
6
6
|
import { abilitySha256, abilityTreeDigest, collectAbilityFiles, resolveAbilityRelease, safeAbilityName, } from "./resolver.js";
|
|
@@ -10,6 +10,8 @@ import { parseSkillFrontmatter } from "../skill-frontmatter.js";
|
|
|
10
10
|
import { isProjectSkillName, MAX_PROJECT_SKILL_DESCRIPTION_LENGTH, MAX_PROJECT_SKILL_FILE_BYTES, } from "../project-skills/types.js";
|
|
11
11
|
import { createAgentProjectionCoordinator, runtimeProjectionLifetime, } from "../project-skills/agent-projection-coordinator.js";
|
|
12
12
|
const exists = (path) => lstat(path).then(() => true, () => false);
|
|
13
|
+
const isRealDirectory = (path) => lstat(path)
|
|
14
|
+
.then((info) => info.isDirectory() && !info.isSymbolicLink(), () => false);
|
|
13
15
|
const repositoryKey = (url) => createHash("sha256").update(url).digest("hex");
|
|
14
16
|
const posixPath = (value) => value.split(sep).join("/");
|
|
15
17
|
const sameSnapshot = (left, right) => JSON.stringify(left) === JSON.stringify(right);
|
|
@@ -208,8 +210,164 @@ const verifyCache = async (agentsRoot, ability) => {
|
|
|
208
210
|
};
|
|
209
211
|
const layerRelativePath = (path, layer) => path.startsWith(`${layer}/`) ? path.slice(layer.length + 1) : path;
|
|
210
212
|
const readManagedCatalog = async (trainingRoot) => JSON.parse(await readFile(join(trainingRoot, "ACTIVE_RELEASE.json"), "utf8"));
|
|
213
|
+
const trainingSwitchJournalPath = (agentRoot) => join(agentRoot, ".training-switch.json");
|
|
214
|
+
const trainingRollbackRoot = (agentRoot) => join(agentRoot, ".training-rollback");
|
|
215
|
+
const trainingDiscardRoot = (agentRoot) => join(agentRoot, ".training-discard");
|
|
216
|
+
const writeTrainingSwitchJournal = async (agentRoot, journal) => {
|
|
217
|
+
const target = trainingSwitchJournalPath(agentRoot);
|
|
218
|
+
const staging = `${target}.next-${randomUUID()}`;
|
|
219
|
+
await writeFile(staging, JSON.stringify(journal), { encoding: "utf8", mode: 0o600 });
|
|
220
|
+
await rename(staging, target);
|
|
221
|
+
};
|
|
222
|
+
const readTrainingSwitchJournal = async (agentRoot) => {
|
|
223
|
+
const raw = await readFile(trainingSwitchJournalPath(agentRoot), "utf8").catch((error) => {
|
|
224
|
+
if (error.code === "ENOENT")
|
|
225
|
+
return null;
|
|
226
|
+
throw error;
|
|
227
|
+
});
|
|
228
|
+
if (raw === null)
|
|
229
|
+
return null;
|
|
230
|
+
let value;
|
|
231
|
+
try {
|
|
232
|
+
value = JSON.parse(raw);
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
throw new Error("ability_training_switch_journal_invalid");
|
|
236
|
+
}
|
|
237
|
+
if (typeof value !== "object" || value === null)
|
|
238
|
+
throw new Error("ability_training_switch_journal_invalid");
|
|
239
|
+
const journal = value;
|
|
240
|
+
if (journal.operation === "install"
|
|
241
|
+
&& (journal.phase === "prepared" || journal.phase === "previous-renamed")
|
|
242
|
+
&& typeof journal.stagingName === "string"
|
|
243
|
+
&& basename(journal.stagingName) === journal.stagingName
|
|
244
|
+
&& journal.stagingName.startsWith(".training-next-")) {
|
|
245
|
+
return { operation: "install", phase: journal.phase, stagingName: journal.stagingName };
|
|
246
|
+
}
|
|
247
|
+
if (journal.operation === "restore"
|
|
248
|
+
&& (journal.phase === "prepared" || journal.phase === "current-moved" || journal.phase === "restored")) {
|
|
249
|
+
return { operation: "restore", phase: journal.phase };
|
|
250
|
+
}
|
|
251
|
+
throw new Error("ability_training_switch_journal_invalid");
|
|
252
|
+
};
|
|
253
|
+
const clearTrainingSwitchJournal = async (agentRoot) => {
|
|
254
|
+
await rm(trainingSwitchJournalPath(agentRoot), { force: true });
|
|
255
|
+
};
|
|
256
|
+
const assertManagedTrainingTreeIfPresent = async (root, errorCode) => {
|
|
257
|
+
if (!await exists(root))
|
|
258
|
+
return false;
|
|
259
|
+
if (!await isRealDirectory(root))
|
|
260
|
+
throw new Error(errorCode);
|
|
261
|
+
const catalog = await readManagedCatalog(root).catch(() => null);
|
|
262
|
+
if (catalog?.managedBy !== "nowcrew-agent-training")
|
|
263
|
+
throw new Error(errorCode);
|
|
264
|
+
return true;
|
|
265
|
+
};
|
|
266
|
+
const finishTrainingRestore = async (agentRoot) => {
|
|
267
|
+
const trainingRoot = join(agentRoot, "training");
|
|
268
|
+
const rollbackRoot = trainingRollbackRoot(agentRoot);
|
|
269
|
+
const discardRoot = trainingDiscardRoot(agentRoot);
|
|
270
|
+
const hasTraining = await assertManagedTrainingTreeIfPresent(trainingRoot, "ability_workspace_training_conflict");
|
|
271
|
+
const hasRollback = await assertManagedTrainingTreeIfPresent(rollbackRoot, "ability_training_rollback_invalid");
|
|
272
|
+
const hasDiscard = await assertManagedTrainingTreeIfPresent(discardRoot, "ability_training_discard_invalid");
|
|
273
|
+
if (hasTraining && hasRollback && !hasDiscard) {
|
|
274
|
+
await rename(trainingRoot, discardRoot);
|
|
275
|
+
await writeTrainingSwitchJournal(agentRoot, { operation: "restore", phase: "current-moved" });
|
|
276
|
+
}
|
|
277
|
+
if (!await exists(trainingRoot) && await exists(rollbackRoot)) {
|
|
278
|
+
await rename(rollbackRoot, trainingRoot);
|
|
279
|
+
await writeTrainingSwitchJournal(agentRoot, { operation: "restore", phase: "restored" });
|
|
280
|
+
}
|
|
281
|
+
if (!await exists(trainingRoot) && !await exists(rollbackRoot) && await exists(discardRoot)) {
|
|
282
|
+
await rename(discardRoot, trainingRoot);
|
|
283
|
+
}
|
|
284
|
+
if (!await exists(trainingRoot))
|
|
285
|
+
throw new Error("ability_training_restore_incomplete");
|
|
286
|
+
await removeManagedTree(discardRoot);
|
|
287
|
+
await clearTrainingSwitchJournal(agentRoot);
|
|
288
|
+
};
|
|
289
|
+
const recoverTrainingSwitch = async (agentRoot) => {
|
|
290
|
+
const journal = await readTrainingSwitchJournal(agentRoot);
|
|
291
|
+
if (journal === null)
|
|
292
|
+
return;
|
|
293
|
+
const trainingRoot = join(agentRoot, "training");
|
|
294
|
+
if (journal.operation === "restore") {
|
|
295
|
+
await finishTrainingRestore(agentRoot);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
const staging = join(agentRoot, journal.stagingName);
|
|
299
|
+
const rollbackRoot = trainingRollbackRoot(agentRoot);
|
|
300
|
+
await assertManagedTrainingTreeIfPresent(trainingRoot, "ability_workspace_training_conflict");
|
|
301
|
+
await assertManagedTrainingTreeIfPresent(staging, "ability_training_switch_staging_invalid");
|
|
302
|
+
await assertManagedTrainingTreeIfPresent(rollbackRoot, "ability_training_rollback_invalid");
|
|
303
|
+
if (await exists(trainingRoot)) {
|
|
304
|
+
await removeManagedTree(staging);
|
|
305
|
+
}
|
|
306
|
+
else if (await exists(staging)) {
|
|
307
|
+
await rename(staging, trainingRoot);
|
|
308
|
+
}
|
|
309
|
+
else if (await exists(rollbackRoot)) {
|
|
310
|
+
await rename(rollbackRoot, trainingRoot);
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
throw new Error("ability_training_switch_recovery_failed");
|
|
314
|
+
}
|
|
315
|
+
await clearTrainingSwitchJournal(agentRoot);
|
|
316
|
+
};
|
|
317
|
+
const switchTrainingDirectory = async (agentRoot, staging) => {
|
|
318
|
+
await recoverTrainingSwitch(agentRoot);
|
|
319
|
+
const trainingRoot = join(agentRoot, "training");
|
|
320
|
+
const rollbackRoot = trainingRollbackRoot(agentRoot);
|
|
321
|
+
await assertManagedTrainingTreeIfPresent(staging, "ability_training_switch_staging_invalid");
|
|
322
|
+
await assertManagedTrainingTreeIfPresent(trainingRoot, "ability_workspace_training_conflict");
|
|
323
|
+
await assertManagedTrainingTreeIfPresent(rollbackRoot, "ability_training_rollback_invalid");
|
|
324
|
+
await removeManagedTree(rollbackRoot);
|
|
325
|
+
await writeTrainingSwitchJournal(agentRoot, {
|
|
326
|
+
operation: "install", phase: "prepared", stagingName: basename(staging),
|
|
327
|
+
});
|
|
328
|
+
try {
|
|
329
|
+
if (await exists(trainingRoot)) {
|
|
330
|
+
await rename(trainingRoot, rollbackRoot);
|
|
331
|
+
await writeTrainingSwitchJournal(agentRoot, {
|
|
332
|
+
operation: "install", phase: "previous-renamed", stagingName: basename(staging),
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
await rename(staging, trainingRoot);
|
|
336
|
+
await clearTrainingSwitchJournal(agentRoot);
|
|
337
|
+
}
|
|
338
|
+
catch (error) {
|
|
339
|
+
await recoverTrainingSwitch(agentRoot).catch(() => { });
|
|
340
|
+
throw error;
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
const restoreTrainingRollback = async (agentRoot, ability) => {
|
|
344
|
+
await recoverTrainingSwitch(agentRoot);
|
|
345
|
+
const currentCatalog = await readManagedCatalog(join(agentRoot, "training")).catch(() => null);
|
|
346
|
+
const rollbackRoot = trainingRollbackRoot(agentRoot);
|
|
347
|
+
if (!await isRealDirectory(rollbackRoot))
|
|
348
|
+
return null;
|
|
349
|
+
const catalog = await readManagedCatalog(rollbackRoot).catch(() => null);
|
|
350
|
+
if (catalog?.managedBy !== "nowcrew-agent-training"
|
|
351
|
+
|| catalog.releaseId !== ability.releaseId
|
|
352
|
+
|| catalog.rootCommit !== ability.rootCommit
|
|
353
|
+
|| catalog.artifactDigest !== ability.artifactDigest)
|
|
354
|
+
return null;
|
|
355
|
+
await removeManagedTree(trainingDiscardRoot(agentRoot));
|
|
356
|
+
await writeTrainingSwitchJournal(agentRoot, { operation: "restore", phase: "prepared" });
|
|
357
|
+
await finishTrainingRestore(agentRoot);
|
|
358
|
+
dslog("ability.workspace.rollback_restored", "Previous Agent training directory restored locally", {
|
|
359
|
+
level: "INFO",
|
|
360
|
+
from_release_id: typeof currentCatalog?.releaseId === "string" ? currentCatalog.releaseId : "unknown",
|
|
361
|
+
release_id: ability.releaseId,
|
|
362
|
+
root_commit: ability.rootCommit,
|
|
363
|
+
artifact_digest: ability.artifactDigest,
|
|
364
|
+
});
|
|
365
|
+
return join(agentRoot, "training");
|
|
366
|
+
};
|
|
211
367
|
const activeTrainingRoot = async (agentsRoot, handle, ability) => {
|
|
212
368
|
const trainingRoot = join(agentsRoot, handle, "training");
|
|
369
|
+
if (!await isRealDirectory(trainingRoot))
|
|
370
|
+
return null;
|
|
213
371
|
const catalog = await readManagedCatalog(trainingRoot).catch(() => null);
|
|
214
372
|
return catalog?.managedBy === "nowcrew-agent-training"
|
|
215
373
|
&& catalog.releaseId === ability.releaseId
|
|
@@ -248,6 +406,8 @@ const materializeWorkspaceTraining = async (agentsRoot, handle, ability) => {
|
|
|
248
406
|
}));
|
|
249
407
|
let previousCatalog = null;
|
|
250
408
|
if (await exists(trainingRoot)) {
|
|
409
|
+
if (!await isRealDirectory(trainingRoot))
|
|
410
|
+
throw new Error("ability_workspace_training_conflict");
|
|
251
411
|
previousCatalog = await readManagedCatalog(trainingRoot).catch(() => null);
|
|
252
412
|
if (previousCatalog?.managedBy !== "nowcrew-agent-training") {
|
|
253
413
|
throw new Error("ability_workspace_training_conflict");
|
|
@@ -259,7 +419,6 @@ const materializeWorkspaceTraining = async (agentsRoot, handle, ability) => {
|
|
|
259
419
|
}
|
|
260
420
|
await mkdir(agentRoot, { recursive: true });
|
|
261
421
|
const staging = join(agentRoot, `.training-next-${ability.releaseId}-${randomUUID()}`);
|
|
262
|
-
const backup = join(agentRoot, `.training-previous-${ability.releaseId}-${randomUUID()}`);
|
|
263
422
|
await mkdir(staging, { recursive: true, mode: 0o700 });
|
|
264
423
|
try {
|
|
265
424
|
const instructionsTarget = join(staging, "instructions", layerRelativePath(ability.instructions.path, "instructions"));
|
|
@@ -333,7 +492,7 @@ const materializeWorkspaceTraining = async (agentsRoot, handle, ability) => {
|
|
|
333
492
|
await writableTree(join(staging, "workspace", abilityWorkspaceProjectionPath(asset.target)));
|
|
334
493
|
}
|
|
335
494
|
}
|
|
336
|
-
await
|
|
495
|
+
await switchTrainingDirectory(agentRoot, staging);
|
|
337
496
|
}
|
|
338
497
|
catch (error) {
|
|
339
498
|
await removeManagedTree(staging).catch(() => { });
|
|
@@ -341,6 +500,14 @@ const materializeWorkspaceTraining = async (agentsRoot, handle, ability) => {
|
|
|
341
500
|
}
|
|
342
501
|
return trainingRoot;
|
|
343
502
|
};
|
|
503
|
+
const ensureWorkspaceTraining = async (agentsRoot, handle, ability) => {
|
|
504
|
+
const agentRoot = join(agentsRoot, handle);
|
|
505
|
+
await mkdir(agentRoot, { recursive: true });
|
|
506
|
+
await recoverTrainingSwitch(agentRoot);
|
|
507
|
+
return await activeTrainingRoot(agentsRoot, handle, ability)
|
|
508
|
+
?? await restoreTrainingRollback(agentRoot, ability)
|
|
509
|
+
?? await materializeWorkspaceTraining(agentsRoot, handle, ability);
|
|
510
|
+
};
|
|
344
511
|
const projectWorkspaceTraining = async (agentsRoot, handle, ability, trainingRoot) => {
|
|
345
512
|
const agentRoot = join(agentsRoot, handle);
|
|
346
513
|
await projectNativeSkills(agentRoot, trainingRoot, ability.skills);
|
|
@@ -355,7 +522,7 @@ export function createAgentAbilityMaterializer(expectedAgentsRoot, coordinator =
|
|
|
355
522
|
const apply = async (agentsRoot, handle, ability) => {
|
|
356
523
|
if (agentsRoot !== expectedAgentsRoot)
|
|
357
524
|
throw new Error("ability_agents_root_mismatch");
|
|
358
|
-
const trainingRoot = await
|
|
525
|
+
const trainingRoot = await ensureWorkspaceTraining(agentsRoot, handle, ability);
|
|
359
526
|
await projectWorkspaceTraining(agentsRoot, handle, ability, trainingRoot);
|
|
360
527
|
return { directory: "training", releaseId: ability.releaseId, artifactDigest: ability.artifactDigest };
|
|
361
528
|
};
|
|
@@ -388,16 +555,8 @@ export function createAgentAbilityMaterializer(expectedAgentsRoot, coordinator =
|
|
|
388
555
|
return coordinator.runExclusiveUntil(expectedAgentsRoot, handle, async () => {
|
|
389
556
|
const started = Date.now();
|
|
390
557
|
try {
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
if (existingTrainingRoot !== null) {
|
|
394
|
-
trainingRoot = existingTrainingRoot;
|
|
395
|
-
await projectWorkspaceTraining(agentsRoot, handle, ability, existingTrainingRoot);
|
|
396
|
-
}
|
|
397
|
-
else {
|
|
398
|
-
await apply(agentsRoot, handle, ability);
|
|
399
|
-
trainingRoot = join(agentsRoot, handle, "training");
|
|
400
|
-
}
|
|
558
|
+
const trainingRoot = await ensureWorkspaceTraining(agentsRoot, handle, ability);
|
|
559
|
+
await projectWorkspaceTraining(agentsRoot, handle, ability, trainingRoot);
|
|
401
560
|
const context = await loadAgentAbilityRuntimeContext(join(agentsRoot, handle), trainingRoot, ability);
|
|
402
561
|
dslog("ability.execution.materialized", "Agent ability release materialized", {
|
|
403
562
|
level: "INFO", agent_handle: handle, release_id: ability.releaseId,
|