@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.
Files changed (40) hide show
  1. package/dist/atomic-no-replace-rename.js +91 -0
  2. package/dist/completion-retransmitter-logging.js +16 -0
  3. package/dist/completion-retransmitter.js +39 -4
  4. package/dist/control-plane-url.js +4 -2
  5. package/dist/directory-projection-publication.js +105 -0
  6. package/dist/directory-projection.js +20 -4
  7. package/dist/execution-journal.js +40 -4
  8. package/dist/execution-posix-stop-proof.js +82 -0
  9. package/dist/execution-runner.js +68 -8
  10. package/dist/local-executor.js +67 -52
  11. package/dist/machine-info.js +8 -5
  12. package/dist/project-skills/capability.js +109 -0
  13. package/dist/project-skills/controller-convergence.js +57 -0
  14. package/dist/project-skills/controller.js +80 -24
  15. package/dist/project-skills/initialized-reconciler.js +4 -4
  16. package/dist/project-skills/projection-state-domain.js +19 -2
  17. package/dist/project-skills/projection-state-store.js +3 -2
  18. package/dist/project-skills/projection-state-transaction.js +5 -1
  19. package/dist/project-skills/projection-state.js +1 -1
  20. package/dist/project-skills/reconciler.js +275 -102
  21. package/dist/project-skills/runtime-launch.js +102 -0
  22. package/dist/project-skills/runtime-root-bootstrap.js +47 -0
  23. package/dist/project-skills/runtime-root-domain.js +268 -0
  24. package/dist/project-skills/runtime-root-gc.js +293 -0
  25. package/dist/project-skills/runtime-root-lease-artifact.js +46 -0
  26. package/dist/project-skills/runtime-root-leases.js +487 -0
  27. package/dist/project-skills/runtime-root-source-identity.js +60 -0
  28. package/dist/project-skills/runtime-root-startup.js +49 -0
  29. package/dist/project-skills/runtime-root-state-artifact-domain.js +143 -0
  30. package/dist/project-skills/runtime-root-state-index.js +356 -0
  31. package/dist/project-skills/runtime-root-store.js +722 -0
  32. package/dist/project-skills/serve-capability.js +28 -0
  33. package/dist/project-skills/serve-startup.js +22 -0
  34. package/dist/project-skills/types.js +1 -0
  35. package/dist/runtimes/codex-home-migration-cli.js +26 -0
  36. package/dist/runtimes/codex-home-migration.js +112 -0
  37. package/dist/runtimes/codex-home.js +200 -17
  38. package/dist/serve.js +60 -79
  39. package/dist/supervised-runtime.js +1 -5
  40. 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 = [...new Map(bindings.map((binding) => [
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 ensureAppliedLocked = async (handle, expected, projectsSnapshot) => {
233
- const agentRoot = join(deps.agentsRoot, handle);
234
- try {
235
- await recoverProjectionSwitch(agentRoot, deps.syncDirectory, manifestOperations);
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
- catch {
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
- const current = await readApplied(agentRoot, readOptions);
241
- const lockedDecision = decideProjectSkillProjection(expected, current);
242
- rejectDecision(lockedDecision);
243
- if (lockedDecision.kind !== "ensure-applied")
244
- return lockedDecision;
245
- const projects = projectsSnapshot ?? deps.scannedProjects();
246
- const reconciled = await reconcileUnlocked(handle, expected.bindings, projects, false, async (records) => {
247
- const applied = createAppliedProjectSkillManifest({
248
- bindings: expected.bindings,
249
- generation: expected.generation,
250
- resolutions: records,
251
- platform,
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
- const lockedExpectation = Object.freeze({
254
- bindings: expected.bindings,
255
- generation: expected.generation,
256
- bindingDigest: expected.bindingDigest,
257
- resolutionDigest: applied.resolutionDigest,
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 update = await prepareUpdate(agentRoot, applied, readOptions);
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
- update,
262
- operations: manifestOperations,
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
- if (error instanceof ProjectProjectionError)
303
- throw error;
304
- throw new ProjectProjectionError("skill_projection_snapshot_corrupt");
424
+ return mapRuntimeRootError(error);
305
425
  }
306
- const decision = await ensureAppliedLocked(handle, expected, projects);
307
- if (decision.kind !== "use-current")
308
- return decision;
309
- const warnings = projectSkillResolutionRecords(snapshot.bindings, projects, platform)
310
- .filter((record) => record.mode === "missing")
311
- .map((record) => Object.freeze({
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
- const decision = "resolutionDigest" in projection
365
- ? await ensureApplied(handle, projection)
366
- : await ensureSnapshot(handle, projection);
367
- if (decision.kind === "use-advanced")
368
- onWarning?.({ code: decision.warning });
369
- if ("warnings" in decision) {
370
- for (const warning of decision.warnings ?? [])
371
- onWarning?.(warning);
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
+ }