@nowcrew/daemon 0.6.4 → 0.6.6
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 +175 -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,166 @@ 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
|
+
const discardRoot = trainingDiscardRoot(agentRoot);
|
|
356
|
+
await assertManagedTrainingTreeIfPresent(discardRoot, "ability_training_discard_invalid");
|
|
357
|
+
await removeManagedTree(discardRoot);
|
|
358
|
+
await writeTrainingSwitchJournal(agentRoot, { operation: "restore", phase: "prepared" });
|
|
359
|
+
await finishTrainingRestore(agentRoot);
|
|
360
|
+
dslog("ability.workspace.rollback_restored", "Previous Agent training directory restored locally", {
|
|
361
|
+
level: "INFO",
|
|
362
|
+
from_release_id: typeof currentCatalog?.releaseId === "string" ? currentCatalog.releaseId : "unknown",
|
|
363
|
+
release_id: ability.releaseId,
|
|
364
|
+
root_commit: ability.rootCommit,
|
|
365
|
+
artifact_digest: ability.artifactDigest,
|
|
366
|
+
});
|
|
367
|
+
return join(agentRoot, "training");
|
|
368
|
+
};
|
|
211
369
|
const activeTrainingRoot = async (agentsRoot, handle, ability) => {
|
|
212
370
|
const trainingRoot = join(agentsRoot, handle, "training");
|
|
371
|
+
if (!await isRealDirectory(trainingRoot))
|
|
372
|
+
return null;
|
|
213
373
|
const catalog = await readManagedCatalog(trainingRoot).catch(() => null);
|
|
214
374
|
return catalog?.managedBy === "nowcrew-agent-training"
|
|
215
375
|
&& catalog.releaseId === ability.releaseId
|
|
@@ -248,6 +408,8 @@ const materializeWorkspaceTraining = async (agentsRoot, handle, ability) => {
|
|
|
248
408
|
}));
|
|
249
409
|
let previousCatalog = null;
|
|
250
410
|
if (await exists(trainingRoot)) {
|
|
411
|
+
if (!await isRealDirectory(trainingRoot))
|
|
412
|
+
throw new Error("ability_workspace_training_conflict");
|
|
251
413
|
previousCatalog = await readManagedCatalog(trainingRoot).catch(() => null);
|
|
252
414
|
if (previousCatalog?.managedBy !== "nowcrew-agent-training") {
|
|
253
415
|
throw new Error("ability_workspace_training_conflict");
|
|
@@ -259,7 +421,6 @@ const materializeWorkspaceTraining = async (agentsRoot, handle, ability) => {
|
|
|
259
421
|
}
|
|
260
422
|
await mkdir(agentRoot, { recursive: true });
|
|
261
423
|
const staging = join(agentRoot, `.training-next-${ability.releaseId}-${randomUUID()}`);
|
|
262
|
-
const backup = join(agentRoot, `.training-previous-${ability.releaseId}-${randomUUID()}`);
|
|
263
424
|
await mkdir(staging, { recursive: true, mode: 0o700 });
|
|
264
425
|
try {
|
|
265
426
|
const instructionsTarget = join(staging, "instructions", layerRelativePath(ability.instructions.path, "instructions"));
|
|
@@ -333,7 +494,7 @@ const materializeWorkspaceTraining = async (agentsRoot, handle, ability) => {
|
|
|
333
494
|
await writableTree(join(staging, "workspace", abilityWorkspaceProjectionPath(asset.target)));
|
|
334
495
|
}
|
|
335
496
|
}
|
|
336
|
-
await
|
|
497
|
+
await switchTrainingDirectory(agentRoot, staging);
|
|
337
498
|
}
|
|
338
499
|
catch (error) {
|
|
339
500
|
await removeManagedTree(staging).catch(() => { });
|
|
@@ -341,6 +502,14 @@ const materializeWorkspaceTraining = async (agentsRoot, handle, ability) => {
|
|
|
341
502
|
}
|
|
342
503
|
return trainingRoot;
|
|
343
504
|
};
|
|
505
|
+
const ensureWorkspaceTraining = async (agentsRoot, handle, ability) => {
|
|
506
|
+
const agentRoot = join(agentsRoot, handle);
|
|
507
|
+
await mkdir(agentRoot, { recursive: true });
|
|
508
|
+
await recoverTrainingSwitch(agentRoot);
|
|
509
|
+
return await activeTrainingRoot(agentsRoot, handle, ability)
|
|
510
|
+
?? await restoreTrainingRollback(agentRoot, ability)
|
|
511
|
+
?? await materializeWorkspaceTraining(agentsRoot, handle, ability);
|
|
512
|
+
};
|
|
344
513
|
const projectWorkspaceTraining = async (agentsRoot, handle, ability, trainingRoot) => {
|
|
345
514
|
const agentRoot = join(agentsRoot, handle);
|
|
346
515
|
await projectNativeSkills(agentRoot, trainingRoot, ability.skills);
|
|
@@ -355,7 +524,7 @@ export function createAgentAbilityMaterializer(expectedAgentsRoot, coordinator =
|
|
|
355
524
|
const apply = async (agentsRoot, handle, ability) => {
|
|
356
525
|
if (agentsRoot !== expectedAgentsRoot)
|
|
357
526
|
throw new Error("ability_agents_root_mismatch");
|
|
358
|
-
const trainingRoot = await
|
|
527
|
+
const trainingRoot = await ensureWorkspaceTraining(agentsRoot, handle, ability);
|
|
359
528
|
await projectWorkspaceTraining(agentsRoot, handle, ability, trainingRoot);
|
|
360
529
|
return { directory: "training", releaseId: ability.releaseId, artifactDigest: ability.artifactDigest };
|
|
361
530
|
};
|
|
@@ -388,16 +557,8 @@ export function createAgentAbilityMaterializer(expectedAgentsRoot, coordinator =
|
|
|
388
557
|
return coordinator.runExclusiveUntil(expectedAgentsRoot, handle, async () => {
|
|
389
558
|
const started = Date.now();
|
|
390
559
|
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
|
-
}
|
|
560
|
+
const trainingRoot = await ensureWorkspaceTraining(agentsRoot, handle, ability);
|
|
561
|
+
await projectWorkspaceTraining(agentsRoot, handle, ability, trainingRoot);
|
|
401
562
|
const context = await loadAgentAbilityRuntimeContext(join(agentsRoot, handle), trainingRoot, ability);
|
|
402
563
|
dslog("ability.execution.materialized", "Agent ability release materialized", {
|
|
403
564
|
level: "INFO", agent_handle: handle, release_id: ability.releaseId,
|