@nowcrew/daemon 0.6.3 → 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.
|
@@ -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,6 +210,172 @@ 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
|
+
};
|
|
367
|
+
const activeTrainingRoot = async (agentsRoot, handle, ability) => {
|
|
368
|
+
const trainingRoot = join(agentsRoot, handle, "training");
|
|
369
|
+
if (!await isRealDirectory(trainingRoot))
|
|
370
|
+
return null;
|
|
371
|
+
const catalog = await readManagedCatalog(trainingRoot).catch(() => null);
|
|
372
|
+
return catalog?.managedBy === "nowcrew-agent-training"
|
|
373
|
+
&& catalog.releaseId === ability.releaseId
|
|
374
|
+
&& catalog.rootCommit === ability.rootCommit
|
|
375
|
+
&& catalog.artifactDigest === ability.artifactDigest
|
|
376
|
+
? trainingRoot
|
|
377
|
+
: null;
|
|
378
|
+
};
|
|
211
379
|
const materializeWorkspaceTraining = async (agentsRoot, handle, ability) => {
|
|
212
380
|
let verified;
|
|
213
381
|
try {
|
|
@@ -238,6 +406,8 @@ const materializeWorkspaceTraining = async (agentsRoot, handle, ability) => {
|
|
|
238
406
|
}));
|
|
239
407
|
let previousCatalog = null;
|
|
240
408
|
if (await exists(trainingRoot)) {
|
|
409
|
+
if (!await isRealDirectory(trainingRoot))
|
|
410
|
+
throw new Error("ability_workspace_training_conflict");
|
|
241
411
|
previousCatalog = await readManagedCatalog(trainingRoot).catch(() => null);
|
|
242
412
|
if (previousCatalog?.managedBy !== "nowcrew-agent-training") {
|
|
243
413
|
throw new Error("ability_workspace_training_conflict");
|
|
@@ -249,7 +419,6 @@ const materializeWorkspaceTraining = async (agentsRoot, handle, ability) => {
|
|
|
249
419
|
}
|
|
250
420
|
await mkdir(agentRoot, { recursive: true });
|
|
251
421
|
const staging = join(agentRoot, `.training-next-${ability.releaseId}-${randomUUID()}`);
|
|
252
|
-
const backup = join(agentRoot, `.training-previous-${ability.releaseId}-${randomUUID()}`);
|
|
253
422
|
await mkdir(staging, { recursive: true, mode: 0o700 });
|
|
254
423
|
try {
|
|
255
424
|
const instructionsTarget = join(staging, "instructions", layerRelativePath(ability.instructions.path, "instructions"));
|
|
@@ -323,7 +492,7 @@ const materializeWorkspaceTraining = async (agentsRoot, handle, ability) => {
|
|
|
323
492
|
await writableTree(join(staging, "workspace", abilityWorkspaceProjectionPath(asset.target)));
|
|
324
493
|
}
|
|
325
494
|
}
|
|
326
|
-
await
|
|
495
|
+
await switchTrainingDirectory(agentRoot, staging);
|
|
327
496
|
}
|
|
328
497
|
catch (error) {
|
|
329
498
|
await removeManagedTree(staging).catch(() => { });
|
|
@@ -331,6 +500,14 @@ const materializeWorkspaceTraining = async (agentsRoot, handle, ability) => {
|
|
|
331
500
|
}
|
|
332
501
|
return trainingRoot;
|
|
333
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
|
+
};
|
|
334
511
|
const projectWorkspaceTraining = async (agentsRoot, handle, ability, trainingRoot) => {
|
|
335
512
|
const agentRoot = join(agentsRoot, handle);
|
|
336
513
|
await projectNativeSkills(agentRoot, trainingRoot, ability.skills);
|
|
@@ -345,7 +522,7 @@ export function createAgentAbilityMaterializer(expectedAgentsRoot, coordinator =
|
|
|
345
522
|
const apply = async (agentsRoot, handle, ability) => {
|
|
346
523
|
if (agentsRoot !== expectedAgentsRoot)
|
|
347
524
|
throw new Error("ability_agents_root_mismatch");
|
|
348
|
-
const trainingRoot = await
|
|
525
|
+
const trainingRoot = await ensureWorkspaceTraining(agentsRoot, handle, ability);
|
|
349
526
|
await projectWorkspaceTraining(agentsRoot, handle, ability, trainingRoot);
|
|
350
527
|
return { directory: "training", releaseId: ability.releaseId, artifactDigest: ability.artifactDigest };
|
|
351
528
|
};
|
|
@@ -378,7 +555,8 @@ export function createAgentAbilityMaterializer(expectedAgentsRoot, coordinator =
|
|
|
378
555
|
return coordinator.runExclusiveUntil(expectedAgentsRoot, handle, async () => {
|
|
379
556
|
const started = Date.now();
|
|
380
557
|
try {
|
|
381
|
-
const trainingRoot = await
|
|
558
|
+
const trainingRoot = await ensureWorkspaceTraining(agentsRoot, handle, ability);
|
|
559
|
+
await projectWorkspaceTraining(agentsRoot, handle, ability, trainingRoot);
|
|
382
560
|
const context = await loadAgentAbilityRuntimeContext(join(agentsRoot, handle), trainingRoot, ability);
|
|
383
561
|
dslog("ability.execution.materialized", "Agent ability release materialized", {
|
|
384
562
|
level: "INFO", agent_handle: handle, release_id: ability.releaseId,
|