@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.
Files changed (37) hide show
  1. package/dist/atomic-no-replace-rename.js +91 -0
  2. package/dist/control-plane-url.js +4 -2
  3. package/dist/directory-projection-publication.js +105 -0
  4. package/dist/directory-projection.js +20 -4
  5. package/dist/execution-journal.js +40 -4
  6. package/dist/execution-posix-stop-proof.js +82 -0
  7. package/dist/execution-runner.js +68 -8
  8. package/dist/local-executor.js +73 -52
  9. package/dist/machine-info.js +8 -5
  10. package/dist/project-skills/capability.js +109 -0
  11. package/dist/project-skills/controller-convergence.js +57 -0
  12. package/dist/project-skills/controller.js +80 -24
  13. package/dist/project-skills/initialized-reconciler.js +4 -4
  14. package/dist/project-skills/projection-state-domain.js +19 -2
  15. package/dist/project-skills/projection-state-store.js +3 -2
  16. package/dist/project-skills/projection-state-transaction.js +5 -1
  17. package/dist/project-skills/projection-state.js +1 -1
  18. package/dist/project-skills/reconciler.js +275 -102
  19. package/dist/project-skills/runtime-launch.js +102 -0
  20. package/dist/project-skills/runtime-root-bootstrap.js +47 -0
  21. package/dist/project-skills/runtime-root-domain.js +268 -0
  22. package/dist/project-skills/runtime-root-gc.js +293 -0
  23. package/dist/project-skills/runtime-root-lease-artifact.js +46 -0
  24. package/dist/project-skills/runtime-root-leases.js +487 -0
  25. package/dist/project-skills/runtime-root-source-identity.js +60 -0
  26. package/dist/project-skills/runtime-root-startup.js +49 -0
  27. package/dist/project-skills/runtime-root-state-artifact-domain.js +143 -0
  28. package/dist/project-skills/runtime-root-state-index.js +356 -0
  29. package/dist/project-skills/runtime-root-store.js +722 -0
  30. package/dist/project-skills/serve-capability.js +28 -0
  31. package/dist/project-skills/serve-startup.js +22 -0
  32. package/dist/project-skills/types.js +1 -0
  33. package/dist/provider-env.js +3 -0
  34. package/dist/runtimes/codex-home.js +50 -0
  35. package/dist/serve.js +58 -73
  36. package/dist/supervised-runtime.js +1 -5
  37. 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 AppliedManifestSchema = z.object({
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 = parseAppliedProjectSkillManifest(manifest);
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.parseAppliedProjectSkillManifest(manifest), options);
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 = [...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
  }