@timurproko/a1 0.1.8-dev.218 → 0.1.8-dev.224

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 (42) hide show
  1. package/bin/cli.js +6 -0
  2. package/bin/guardian.js +3 -0
  3. package/bin/release-cleanup.js +8 -0
  4. package/bin/ui.js +4 -0
  5. package/bin/warmup.js +18 -0
  6. package/dist/features/owned-ui/run.js +2 -0
  7. package/dist/foundation/launch-guardian/main.js +2 -0
  8. package/dist/foundation/release/bootstrap.d.ts +3 -1
  9. package/dist/foundation/release/bootstrap.js +9 -0
  10. package/dist/foundation/release/cohort-state.d.ts +61 -1
  11. package/dist/foundation/release/cohort-state.js +192 -11
  12. package/dist/foundation/release/dependency-layer.d.ts +78 -0
  13. package/dist/foundation/release/dependency-layer.js +487 -0
  14. package/dist/foundation/release/endpoints.d.ts +1 -1
  15. package/dist/foundation/release/endpoints.js +8 -8
  16. package/dist/foundation/release/index.d.ts +2 -0
  17. package/dist/foundation/release/index.js +2 -0
  18. package/dist/foundation/release/release-gc.d.ts +59 -7
  19. package/dist/foundation/release/release-gc.js +533 -19
  20. package/dist/foundation/release/release-store.d.ts +3 -1
  21. package/dist/foundation/release/release-store.js +60 -10
  22. package/dist/foundation/release/release.d.ts +4 -1
  23. package/dist/foundation/release/release.js +9 -2
  24. package/dist/foundation/release/update.d.ts +10 -2
  25. package/dist/foundation/release/update.js +27 -4
  26. package/dist/foundation/release/warmup.d.ts +3 -0
  27. package/dist/foundation/release/warmup.js +35 -0
  28. package/dist/foundation/startup/index.d.ts +1 -0
  29. package/dist/foundation/startup/index.js +1 -0
  30. package/dist/foundation/startup/startup-runtime.d.ts +36 -0
  31. package/dist/foundation/startup/startup-runtime.js +140 -0
  32. package/dist/integrations/pi/engine/runtime-integration.js +5 -0
  33. package/dist/native/darwin-arm64/manifest.json +3 -3
  34. package/dist/native/darwin-arm64/process-guardian +0 -0
  35. package/dist/native/linux-x64/manifest.json +1 -1
  36. package/dist/native/win32-x64/manifest.json +2 -2
  37. package/dist/native/win32-x64/process-guardian.exe +0 -0
  38. package/dist/product-identity.d.ts +2 -2
  39. package/dist/product-identity.js +4 -1
  40. package/dist/product-identity.json +8 -1
  41. package/dist/runtime-payload-inventory.json +122328 -0
  42. package/package.json +2 -2
@@ -1,24 +1,538 @@
1
- import { rm } from "node:fs/promises";
2
- import { resolve } from "node:path";
3
- import { CohortStateStore } from "./cohort-state.js";
1
+ import { randomUUID } from "node:crypto";
2
+ import { spawn } from "node:child_process";
3
+ import { lstat, mkdir, readFile, readdir, realpath, rename, rm } from "node:fs/promises";
4
+ import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { CohortStateStore, planProtectedReleases, } from "./cohort-state.js";
4
7
  import { liveReleaseIds } from "./endpoints.js";
5
- import { verifyMaterializedRelease } from "./release-store.js";
6
- /** Remove only a release that has no active, pending, approval, rollback,
7
- * retention, process, agent, or migration reference. State is detached first;
8
- * a filesystem failure can leave a safe orphan but never a dangling selector. */
9
- export async function collectRelease(store, dataDir, releaseId, externalReferences,
10
- /** Where endpoints are recorded, so a release a live cohort runs from is retained. */
11
- paths) {
8
+ import { resolveProductPaths } from "../lifecycle/index.js";
9
+ import { RELEASE_MANIFEST_FILENAME } from "./release-store.js";
10
+ import { DEPENDENCY_LAYER_MANIFEST, dependencyLayerCertificationPath } from "./dependency-layer.js";
11
+ import { UpdateTransactionStore } from "./update-transaction.js";
12
+ import { PRODUCT_IDENTITY } from "../../product-identity.js";
13
+ import { collectCompileCaches, startupCompileCachePath } from "../startup/index.js";
14
+ const DEFAULT_CANDIDATE_AGE_MS = 60 * 60 * 1_000;
15
+ const DEFAULT_LIMITS = { maxItems: 8, maxDurationMs: 2_000, concurrency: 1 };
16
+ const WORKER_HOLDS_ENV = "A1_RELEASE_CLEANUP_HOLDS";
17
+ /**
18
+ * Reconcile current ownership under the cohort-state lock and durably detach obsolete releases.
19
+ * This operation intentionally reads only manifests and directory metadata, never payload bytes.
20
+ */
21
+ export async function prepareReleaseCleanup(dataDir, paths, options = {}) {
22
+ const store = new CohortStateStore(dataDir);
23
+ const discovered = await discoverCleanupInputs(dataDir, store, options);
24
+ const transactionStore = options.transactionStore ?? new UpdateTransactionStore(dataDir);
25
+ await store.reconcileRetention(async () => await protectionInputs(paths, transactionStore, options.externalHolds ?? []), discovered.orphans);
26
+ for (const diagnostic of discovered.diagnostics.slice(0, 16)) {
27
+ await store.recordCleanupFailure(diagnostic.releaseId, diagnostic.stage, diagnostic.error);
28
+ }
29
+ return { store, discovered };
30
+ }
31
+ /** Run a bounded, restart-safe physical cleanup pass over detached releases and abandoned artifacts. */
32
+ export async function runBoundedReleaseCleanup(dataDir, paths, options = {}) {
33
+ const startedAt = (options.now ?? Date.now)();
34
+ const now = options.now ?? Date.now;
35
+ const limits = normalizeLimits(options.limits);
36
+ const { store, discovered } = await prepareReleaseCleanup(dataDir, paths, options);
37
+ const initial = await store.read();
38
+ const pending = Object.values(initial.cleanup.pending).sort((left, right) => left.release.releaseId.localeCompare(right.release.releaseId));
39
+ let attempted = 0;
40
+ let completed = 0;
41
+ for (let offset = 0; offset < pending.length && attempted < limits.maxItems; offset += limits.concurrency) {
42
+ if (now() - startedAt >= limits.maxDurationMs)
43
+ break;
44
+ const batch = pending.slice(offset, Math.min(offset + limits.concurrency, offset + (limits.maxItems - attempted)));
45
+ attempted += batch.length;
46
+ const results = await Promise.all(batch.map(async (disposition) => await collectDisposition(store, dataDir, paths, disposition, options)));
47
+ completed += results.filter(Boolean).length;
48
+ }
49
+ const transaction = await (options.transactionStore ?? new UpdateTransactionStore(dataDir)).read();
50
+ const activeTransaction = transaction?.status === "active";
51
+ const artifactBudget = Math.max(0, limits.maxItems - attempted);
52
+ const artifacts = [
53
+ ...(!activeTransaction ? discovered.candidates : []),
54
+ ...discovered.unmanagedTrash,
55
+ ...discovered.certifications,
56
+ ].slice(0, artifactBudget);
57
+ for (const artifact of artifacts) {
58
+ if (now() - startedAt >= limits.maxDurationMs)
59
+ break;
60
+ const certificationId = certificationReleaseId(artifact);
61
+ if (certificationId !== null) {
62
+ const inputs = await protectionInputs(paths, options.transactionStore ?? new UpdateTransactionStore(dataDir), options.externalHolds ?? []);
63
+ if (isProtectedDetachedRelease(await store.read(), inputs, certificationId))
64
+ continue;
65
+ }
66
+ attempted += 1;
67
+ try {
68
+ await removeAbandonedArtifact(dataDir, artifact, options.operations);
69
+ completed += 1;
70
+ }
71
+ catch (error) {
72
+ await store.recordCleanupFailure(artifactName(artifact), "artifact-delete", error);
73
+ }
74
+ }
75
+ if (!activeTransaction && attempted < limits.maxItems && now() - startedAt < limits.maxDurationMs) {
76
+ const layerResult = await collectUnreferencedDependencyLayers(store, dataDir, limits.maxItems - attempted, options);
77
+ attempted += layerResult.attempted;
78
+ completed += layerResult.completed;
79
+ await collectCompileCaches(dataDir, await protectedCompileCachePaths(dataDir, await store.read())).catch(() => { });
80
+ }
81
+ const remaining = Object.keys((await store.read()).cleanup.pending).length;
82
+ return { planned: pending.length + artifacts.length, attempted, completed, remaining, durationMs: Math.max(0, now() - startedAt) };
83
+ }
84
+ /**
85
+ * Commit cleanup intent synchronously, then let a dependency-light detached worker perform
86
+ * potentially slow recursive deletion. A later launch/update resumes work if spawning fails.
87
+ */
88
+ export async function scheduleReleaseCleanup(dataDir, paths, options = {}) {
89
+ const { store } = await prepareReleaseCleanup(dataDir, paths, options);
90
+ const entry = fileURLToPath(new URL("../../../bin/release-cleanup.js", import.meta.url));
91
+ try {
92
+ const child = spawn(process.execPath, [entry], {
93
+ detached: true,
94
+ windowsHide: true,
95
+ stdio: "ignore",
96
+ env: {
97
+ ...process.env,
98
+ [PRODUCT_IDENTITY.environment.dataDir]: dataDir,
99
+ [PRODUCT_IDENTITY.environment.runtimeDir]: paths.runtimeDir,
100
+ [WORKER_HOLDS_ENV]: JSON.stringify(options.externalHolds ?? []),
101
+ },
102
+ });
103
+ await new Promise((resolvePromise, rejectPromise) => {
104
+ child.once("spawn", resolvePromise);
105
+ child.once("error", rejectPromise);
106
+ });
107
+ child.unref();
108
+ }
109
+ catch (error) {
110
+ await store.recordCleanupFailure("worker", "spawn", error);
111
+ }
112
+ }
113
+ /** Entry used by the private cleanup executable shipped in every immutable release. */
114
+ export async function runReleaseCleanupWorker(environment = process.env) {
115
+ const paths = resolveProductPaths(environment);
116
+ return await runBoundedReleaseCleanup(paths.dataDir, paths, { externalHolds: parseWorkerHolds(environment[WORKER_HOLDS_ENV]) });
117
+ }
118
+ /** Compatibility helper: detach one requested release and execute a one-item pass. */
119
+ export async function collectRelease(store, dataDir, releaseId, externalReferences, paths) {
120
+ const holds = externalReferences.map(referenced => ({ authority: "migration", releaseId: referenced }));
12
121
  const state = await store.read();
13
- const release = state.releases[releaseId];
14
- if (!release)
122
+ if (!state.releases[releaseId])
15
123
  throw new Error(`unknown release ${releaseId}`);
16
- // Invariant: a superseded cohort keeps working until its last session leaves, and it runs from this
17
- // content while it does. Collecting it would pull the release out from under live work.
18
- if (paths && (await liveReleaseIds(paths)).includes(releaseId)) {
19
- throw new Error(`release ${releaseId} is still running a live cohort`);
124
+ await store.reconcileRetention(async () => ({ ...(await protectionInputs(paths, new UpdateTransactionStore(dataDir), holds)) }));
125
+ const disposition = (await store.read()).cleanup.pending[releaseId];
126
+ if (!disposition)
127
+ throw new Error(`release ${releaseId} is still referenced and cannot be collected`);
128
+ const removed = await collectDisposition(store, dataDir, paths, disposition, { externalHolds: holds, limits: { maxItems: 1 } });
129
+ if (!removed)
130
+ throw new Error(`release ${releaseId} could not be collected`);
131
+ }
132
+ async function collectDisposition(store, dataDir, paths, disposition, options) {
133
+ const releaseId = disposition.release.releaseId;
134
+ try {
135
+ const inputs = await protectionInputs(paths, options.transactionStore ?? new UpdateTransactionStore(dataDir), options.externalHolds ?? []);
136
+ const current = await store.read();
137
+ if (isProtectedDetachedRelease(current, inputs, releaseId))
138
+ return false;
139
+ const storeRoot = await canonicalReleaseStore(dataDir);
140
+ const trashRoot = await canonicalTrashRoot(storeRoot);
141
+ let trashPath = disposition.trashPath;
142
+ if (disposition.stage === "detached") {
143
+ const source = resolve(storeRoot, releaseId);
144
+ const sourceMetadata = await lstat(source).catch(error => missingOrThrow(error));
145
+ if (sourceMetadata === null) {
146
+ trashPath = await findReleaseTrash(trashRoot, releaseId);
147
+ if (trashPath === null) {
148
+ await removeCertificationEvidence(dataDir, disposition.release, store);
149
+ await store.completeCleanup(releaseId);
150
+ return true;
151
+ }
152
+ }
153
+ else {
154
+ await assertReleaseDirectory(source, storeRoot, disposition.release, releaseId);
155
+ trashPath = resolve(trashRoot, `${releaseId}--${randomUUID()}`);
156
+ await (options.operations?.rename ?? rename)(source, trashPath);
157
+ }
158
+ await store.markCleanupTrash(releaseId, trashPath);
159
+ }
160
+ if (trashPath === null)
161
+ throw new Error(`release ${releaseId} has no managed trash path`);
162
+ await assertReleaseDirectory(trashPath, trashRoot, disposition.release, `${releaseId}--`);
163
+ await (options.operations?.remove ?? rm)(trashPath, { recursive: true, force: false, maxRetries: 0 });
164
+ await removeCertificationEvidence(dataDir, disposition.release, store);
165
+ await store.completeCleanup(releaseId);
166
+ return true;
167
+ }
168
+ catch (error) {
169
+ await store.recordCleanupFailure(releaseId, disposition.stage, error);
170
+ return false;
171
+ }
172
+ }
173
+ function isProtectedDetachedRelease(state, inputs, releaseId) {
174
+ const selected = [state.references.active, state.references.pending, state.references.approved, state.references.rollback]
175
+ .some(reference => reference === releaseId);
176
+ return selected
177
+ || (inputs.liveReleaseIds ?? []).includes(releaseId)
178
+ || (inputs.externalHolds ?? []).some(hold => hold.releaseId === releaseId)
179
+ || (inputs.transaction?.status === "active" && inputs.transaction.priorActiveReleaseId === releaseId);
180
+ }
181
+ async function protectionInputs(paths, transactionStore, externalHolds) {
182
+ const [liveIds, transaction] = await Promise.all([
183
+ paths ? liveReleaseIds(paths) : Promise.resolve([]),
184
+ transactionStore.read(),
185
+ ]);
186
+ return { liveReleaseIds: liveIds, externalHolds, transaction: transactionReference(transaction) };
187
+ }
188
+ function transactionReference(transaction) {
189
+ return transaction === null ? null : { status: transaction.status, priorActiveReleaseId: transaction.priorActiveReleaseId };
190
+ }
191
+ async function discoverCleanupInputs(dataDir, store, options) {
192
+ const releaseRoot = resolve(dataDir, "releases");
193
+ await mkdir(releaseRoot, { recursive: true, mode: 0o700 });
194
+ const state = await store.read();
195
+ const orphans = [];
196
+ const candidates = [];
197
+ const unmanagedTrash = [];
198
+ const certifications = [];
199
+ const diagnostics = [];
200
+ const candidateAgeMs = options.candidateAgeMs ?? DEFAULT_CANDIDATE_AGE_MS;
201
+ const now = options.now ?? Date.now;
202
+ for (const entry of await readdir(releaseRoot, { withFileTypes: true })) {
203
+ const path = resolve(releaseRoot, entry.name);
204
+ if (entry.name === ".trash")
205
+ continue;
206
+ if (entry.name.startsWith(".candidate-")) {
207
+ try {
208
+ const metadata = await lstat(path);
209
+ if (metadata.isDirectory() && !metadata.isSymbolicLink() && now() - metadata.mtimeMs >= candidateAgeMs)
210
+ candidates.push(path);
211
+ }
212
+ catch (error) {
213
+ diagnostics.push({ releaseId: entry.name, stage: "candidate-discovery", error: errorMessage(error) });
214
+ }
215
+ continue;
216
+ }
217
+ if (state.releases[entry.name] || state.cleanup.pending[entry.name])
218
+ continue;
219
+ try {
220
+ const release = await readReleaseRecord(path, releaseRoot, entry.name, dataDir);
221
+ orphans.push({ release, stage: "detached" });
222
+ }
223
+ catch (error) {
224
+ diagnostics.push({ releaseId: entry.name, stage: "orphan-discovery", error: errorMessage(error) });
225
+ }
226
+ }
227
+ const trashRoot = resolve(releaseRoot, ".trash");
228
+ const trashMetadata = await lstat(trashRoot).catch(error => missingOrThrow(error));
229
+ if (trashMetadata !== null) {
230
+ if (!trashMetadata.isDirectory() || trashMetadata.isSymbolicLink()) {
231
+ diagnostics.push({ releaseId: ".trash", stage: "trash-discovery", error: "managed trash is not a direct non-link directory" });
232
+ }
233
+ else {
234
+ for (const entry of await readdir(trashRoot, { withFileTypes: true })) {
235
+ const path = resolve(trashRoot, entry.name);
236
+ const releaseId = entry.name.split("--", 1)[0];
237
+ if (entry.name.startsWith(".candidate--")) {
238
+ unmanagedTrash.push(path);
239
+ }
240
+ else if (entry.name.includes("--") && !state.cleanup.pending[releaseId]) {
241
+ try {
242
+ const release = await readReleaseRecord(path, trashRoot, `${releaseId}--`, dataDir);
243
+ orphans.push({ release, stage: "trash", trashPath: path });
244
+ }
245
+ catch (error) {
246
+ diagnostics.push({ releaseId, stage: "trash-discovery", error: errorMessage(error) });
247
+ }
248
+ }
249
+ }
250
+ }
251
+ }
252
+ for (const entry of await readdir(dataDir, { withFileTypes: true })) {
253
+ const match = /^certification-(.+)\.json$/.exec(entry.name);
254
+ if (!match || !entry.isFile())
255
+ continue;
256
+ const releaseId = match[1];
257
+ if (!state.releases[releaseId] && !state.cleanup.pending[releaseId] && !orphans.some(orphan => orphan.release.releaseId === releaseId)) {
258
+ certifications.push(resolve(dataDir, entry.name));
259
+ }
260
+ }
261
+ return { orphans, candidates, unmanagedTrash, certifications, diagnostics };
262
+ }
263
+ async function readReleaseRecord(path, parent, expectedName, dataDir) {
264
+ const metadata = await lstat(path);
265
+ if (!metadata.isDirectory() || metadata.isSymbolicLink())
266
+ throw new Error("release path is not a non-link directory");
267
+ const [canonicalParent, canonicalPath] = await Promise.all([realpath(parent), realpath(path)]);
268
+ assertDirectChild(canonicalParent, canonicalPath);
269
+ const name = canonicalPath.split(sep).at(-1) ?? "";
270
+ if (expectedName.endsWith("--") ? !name.startsWith(expectedName) : name !== expectedName)
271
+ throw new Error("release directory name does not match its disposition");
272
+ const manifest = JSON.parse(await readFile(resolve(canonicalPath, RELEASE_MANIFEST_FILENAME), "utf8"));
273
+ if (typeof manifest.releaseId !== "string" || !/^[0-9A-Za-z.+_-]+-[a-f0-9]{20}$/.test(manifest.releaseId)
274
+ || typeof manifest.packageVersion !== "string" || typeof manifest.contentDigest !== "string"
275
+ || !/^[a-f0-9]{64}$/.test(manifest.contentDigest) || !name.startsWith(manifest.releaseId)) {
276
+ throw new Error("release manifest identity is invalid for cleanup");
277
+ }
278
+ return {
279
+ releaseId: manifest.releaseId,
280
+ releaseRoot: resolve(parent, manifest.releaseId),
281
+ packageVersion: manifest.packageVersion,
282
+ contentDigest: manifest.contentDigest,
283
+ approval: "approved",
284
+ materializedAt: new Date(0).toISOString(),
285
+ certifiedAt: null,
286
+ diagnosticsPath: resolve(dataDir, `certification-${manifest.releaseId}.json`),
287
+ };
288
+ }
289
+ async function assertReleaseDirectory(path, parent, release, expectedName) {
290
+ const metadata = await lstat(path);
291
+ if (!metadata.isDirectory() || metadata.isSymbolicLink())
292
+ throw new Error(`refused cleanup of linked or non-directory release ${release.releaseId}`);
293
+ const [canonicalParent, canonicalPath] = await Promise.all([realpath(parent), realpath(path)]);
294
+ assertDirectChild(canonicalParent, canonicalPath);
295
+ const name = canonicalPath.split(sep).at(-1) ?? "";
296
+ if (expectedName.endsWith("--") ? !name.startsWith(expectedName) : name !== expectedName)
297
+ throw new Error(`release directory does not match identity ${release.releaseId}`);
298
+ if (!expectedName.endsWith("--") && canonicalPath !== await realpath(release.releaseRoot))
299
+ throw new Error(`recorded release path differs from managed identity ${release.releaseId}`);
300
+ const manifest = JSON.parse(await readFile(resolve(canonicalPath, RELEASE_MANIFEST_FILENAME), "utf8"));
301
+ if (manifest.releaseId !== release.releaseId || manifest.contentDigest !== release.contentDigest || manifest.packageVersion !== release.packageVersion) {
302
+ throw new Error(`release manifest metadata differs from detached record ${release.releaseId}`);
303
+ }
304
+ }
305
+ async function removeCertificationEvidence(dataDir, release, store) {
306
+ if (release.diagnosticsPath === null)
307
+ return;
308
+ const expected = resolve(dataDir, `certification-${release.releaseId}.json`);
309
+ if (resolve(release.diagnosticsPath) !== expected) {
310
+ await store.recordCleanupFailure(release.releaseId, "certification", "refused to delete certification evidence outside its managed identity path");
311
+ return;
312
+ }
313
+ const metadata = await lstat(expected).catch(error => missingOrThrow(error));
314
+ if (metadata === null)
315
+ return;
316
+ if (!metadata.isFile() || metadata.isSymbolicLink()) {
317
+ await store.recordCleanupFailure(release.releaseId, "certification", "refused to delete linked or non-file certification evidence");
318
+ return;
20
319
  }
21
- await verifyMaterializedRelease(release.releaseRoot, undefined, resolve(dataDir, "releases"));
22
- await store.removeUnreferencedRelease(releaseId, externalReferences);
23
- await rm(release.releaseRoot, { recursive: true, force: false });
320
+ await rm(expected, { force: true });
321
+ }
322
+ async function canonicalReleaseStore(dataDir) {
323
+ const root = resolve(dataDir, "releases");
324
+ await mkdir(root, { recursive: true, mode: 0o700 });
325
+ const metadata = await lstat(root);
326
+ if (!metadata.isDirectory() || metadata.isSymbolicLink())
327
+ throw new Error("release store is not a non-link directory");
328
+ return await realpath(root);
329
+ }
330
+ async function canonicalTrashRoot(storeRoot) {
331
+ const root = resolve(storeRoot, ".trash");
332
+ await mkdir(root, { recursive: true, mode: 0o700 });
333
+ const metadata = await lstat(root);
334
+ if (!metadata.isDirectory() || metadata.isSymbolicLink())
335
+ throw new Error("managed release trash is not a non-link directory");
336
+ const canonical = await realpath(root);
337
+ assertDirectChild(storeRoot, canonical);
338
+ return canonical;
339
+ }
340
+ async function findReleaseTrash(trashRoot, releaseId) {
341
+ const names = (await readdir(trashRoot)).filter(name => name.startsWith(`${releaseId}--`)).sort();
342
+ return names.length === 0 ? null : resolve(trashRoot, names[0]);
343
+ }
344
+ async function removeAbandonedArtifact(dataDir, path, operations) {
345
+ const storeRoot = await canonicalReleaseStore(dataDir);
346
+ const metadata = await lstat(path);
347
+ if (metadata.isSymbolicLink())
348
+ throw new Error("abandoned artifact is a link");
349
+ const canonical = await realpath(path);
350
+ const parent = dirname(canonical);
351
+ const canonicalDataDir = await realpath(dataDir);
352
+ if (parent === canonicalDataDir && certificationReleaseId(canonical) !== null && metadata.isFile()) {
353
+ await (operations?.remove ?? rm)(canonical, { force: true });
354
+ return;
355
+ }
356
+ if (!metadata.isDirectory())
357
+ throw new Error("abandoned artifact is not a managed directory or certification file");
358
+ if (parent === storeRoot && canonical.split(sep).at(-1)?.startsWith(".candidate-")) {
359
+ const trashRoot = await canonicalTrashRoot(storeRoot);
360
+ const moved = resolve(trashRoot, `.candidate--${randomUUID()}`);
361
+ await (operations?.rename ?? rename)(canonical, moved);
362
+ await (operations?.remove ?? rm)(moved, { recursive: true, force: false, maxRetries: 0 });
363
+ return;
364
+ }
365
+ const trashRoot = await canonicalTrashRoot(storeRoot);
366
+ if (parent === trashRoot && canonical.split(sep).at(-1)?.startsWith(".candidate--")) {
367
+ await (operations?.remove ?? rm)(canonical, { recursive: true, force: false, maxRetries: 0 });
368
+ return;
369
+ }
370
+ throw new Error("abandoned artifact is outside managed candidate, trash, or certification paths");
371
+ }
372
+ async function collectUnreferencedDependencyLayers(store, dataDir, maxItems, options) {
373
+ const layersRoot = resolve(dataDir, "dependency-layers");
374
+ const layersMetadata = await lstat(layersRoot).catch(error => missingOrThrow(error));
375
+ if (layersMetadata === null)
376
+ return { attempted: 0, completed: 0 };
377
+ if (!layersMetadata.isDirectory() || layersMetadata.isSymbolicLink())
378
+ throw new Error("dependency-layer store is not a managed directory");
379
+ const referenced = await referencedDependencyLayerIds(dataDir);
380
+ const trashRoot = resolve(layersRoot, ".trash");
381
+ await mkdir(trashRoot, { recursive: true, mode: 0o700 });
382
+ let attempted = 0;
383
+ let completed = 0;
384
+ const entries = await readdir(layersRoot, { withFileTypes: true });
385
+ const candidates = [];
386
+ for (const entry of entries) {
387
+ if (entry.name === ".trash")
388
+ continue;
389
+ const path = resolve(layersRoot, entry.name);
390
+ if (entry.name.startsWith(".candidate-")) {
391
+ const metadata = await lstat(path);
392
+ const currentTime = options.now?.() ?? Date.now();
393
+ if (currentTime - metadata.mtimeMs >= (options.candidateAgeMs ?? DEFAULT_CANDIDATE_AGE_MS)) {
394
+ candidates.push({ path, layerId: null, inTrash: false });
395
+ }
396
+ continue;
397
+ }
398
+ if (!referenced.has(entry.name))
399
+ candidates.push({ path, layerId: entry.name, inTrash: false });
400
+ }
401
+ for (const entry of await readdir(trashRoot, { withFileTypes: true })) {
402
+ candidates.push({ path: resolve(trashRoot, entry.name), layerId: entry.name.split("--", 1)[0] ?? null, inTrash: true });
403
+ }
404
+ for (const candidate of candidates.slice(0, maxItems)) {
405
+ if (candidate.layerId !== null && referenced.has(candidate.layerId))
406
+ continue;
407
+ attempted += 1;
408
+ try {
409
+ const metadata = await lstat(candidate.path);
410
+ if (!metadata.isDirectory() || metadata.isSymbolicLink())
411
+ throw new Error("dependency-layer cleanup candidate is not a non-link directory");
412
+ let removalPath = candidate.path;
413
+ if (!candidate.inTrash) {
414
+ const canonicalRoot = await realpath(layersRoot);
415
+ const canonical = await realpath(candidate.path);
416
+ assertDirectChild(canonicalRoot, canonical);
417
+ if (candidate.layerId !== null) {
418
+ const manifest = JSON.parse(await readFile(resolve(canonical, DEPENDENCY_LAYER_MANIFEST), "utf8"));
419
+ if (manifest.layerId !== candidate.layerId || typeof manifest.contentDigest !== "string")
420
+ throw new Error("dependency-layer manifest does not match its directory");
421
+ }
422
+ removalPath = resolve(trashRoot, `${candidate.layerId ?? ".candidate"}--${randomUUID()}`);
423
+ await (options.operations?.rename ?? rename)(canonical, removalPath);
424
+ }
425
+ else {
426
+ const canonicalTrash = await realpath(trashRoot);
427
+ assertDirectChild(canonicalTrash, await realpath(removalPath));
428
+ }
429
+ await (options.operations?.remove ?? rm)(removalPath, { recursive: true, force: false, maxRetries: 0 });
430
+ if (candidate.layerId !== null && /^dependencies-[a-f0-9]{32}$/.test(candidate.layerId)) {
431
+ await rm(dependencyLayerCertificationPath(dataDir, candidate.layerId), { force: true });
432
+ }
433
+ completed += 1;
434
+ }
435
+ catch (error) {
436
+ // Platform: the pass is retry based, so a sharing violation leaves managed trash.
437
+ await store.recordCleanupFailure(candidate.layerId ?? artifactName(candidate.path), "dependency-layer-delete", error);
438
+ }
439
+ }
440
+ if (attempted < maxItems) {
441
+ for (const entry of await readdir(dataDir, { withFileTypes: true })) {
442
+ const match = /^dependency-layer-certification-(dependencies-[a-f0-9]{32})\.json$/.exec(entry.name);
443
+ if (!match || !entry.isFile() || referenced.has(match[1]))
444
+ continue;
445
+ if (await lstat(resolve(layersRoot, match[1])).catch(error => missingOrThrow(error)) !== null)
446
+ continue;
447
+ attempted += 1;
448
+ try {
449
+ await (options.operations?.remove ?? rm)(resolve(dataDir, entry.name), { force: true });
450
+ completed += 1;
451
+ }
452
+ catch (error) {
453
+ await store.recordCleanupFailure(match[1], "dependency-layer-certification", error);
454
+ }
455
+ if (attempted >= maxItems)
456
+ break;
457
+ }
458
+ }
459
+ return { attempted, completed };
460
+ }
461
+ async function protectedCompileCachePaths(dataDir, state) {
462
+ const paths = [];
463
+ for (const release of Object.values(state.releases)) {
464
+ try {
465
+ const manifest = JSON.parse(await readFile(resolve(release.releaseRoot, RELEASE_MANIFEST_FILENAME), "utf8"));
466
+ const layers = (manifest.dependencyLayers ?? []).map(layer => layer.layerId).filter((id) => typeof id === "string");
467
+ paths.push(startupCompileCachePath(dataDir, release.releaseId, layers));
468
+ }
469
+ catch {
470
+ paths.push(startupCompileCachePath(dataDir, release.releaseId, []));
471
+ }
472
+ }
473
+ return paths;
474
+ }
475
+ async function referencedDependencyLayerIds(dataDir) {
476
+ const referenced = new Set();
477
+ const releasesRoot = resolve(dataDir, "releases");
478
+ const roots = [];
479
+ for (const entry of await readdir(releasesRoot, { withFileTypes: true }).catch(() => [])) {
480
+ if (entry.name === ".trash") {
481
+ for (const trash of await readdir(resolve(releasesRoot, entry.name), { withFileTypes: true }).catch(() => [])) {
482
+ if (trash.isDirectory())
483
+ roots.push(resolve(releasesRoot, entry.name, trash.name));
484
+ }
485
+ }
486
+ else if (entry.isDirectory() && !entry.name.startsWith(".candidate-"))
487
+ roots.push(resolve(releasesRoot, entry.name));
488
+ }
489
+ for (const root of roots) {
490
+ try {
491
+ const manifest = JSON.parse(await readFile(resolve(root, RELEASE_MANIFEST_FILENAME), "utf8"));
492
+ for (const layer of manifest.dependencyLayers ?? [])
493
+ if (typeof layer.layerId === "string")
494
+ referenced.add(layer.layerId);
495
+ }
496
+ catch { }
497
+ }
498
+ return referenced;
499
+ }
500
+ function assertDirectChild(parent, child) {
501
+ const fromParent = relative(parent, child);
502
+ if (fromParent.length === 0 || fromParent === ".." || fromParent.startsWith(`..${sep}`) || isAbsolute(fromParent) || fromParent.includes(sep)) {
503
+ throw new Error(`cleanup path is not a direct child of its managed root: ${child}`);
504
+ }
505
+ }
506
+ function normalizeLimits(input) {
507
+ const limits = { ...DEFAULT_LIMITS, ...input };
508
+ for (const [name, value] of Object.entries(limits)) {
509
+ if (!Number.isSafeInteger(value) || value < 1)
510
+ throw new Error(`invalid release cleanup ${name}: ${value}`);
511
+ }
512
+ return limits;
513
+ }
514
+ function parseWorkerHolds(value) {
515
+ if (!value)
516
+ return [];
517
+ try {
518
+ const parsed = JSON.parse(value);
519
+ if (!Array.isArray(parsed))
520
+ return [];
521
+ return parsed.filter((hold) => Boolean(hold) && typeof hold === "object"
522
+ && (hold.authority === "agent" || hold.authority === "migration")
523
+ && typeof hold.releaseId === "string");
524
+ }
525
+ catch {
526
+ return [];
527
+ }
528
+ }
529
+ function missingOrThrow(error) {
530
+ if (error instanceof Error && "code" in error && error.code === "ENOENT")
531
+ return null;
532
+ throw error;
533
+ }
534
+ function artifactName(path) { return path.split(sep).at(-1) ?? "artifact"; }
535
+ function certificationReleaseId(path) {
536
+ return /^certification-(.+)\.json$/.exec(artifactName(path))?.[1] ?? null;
24
537
  }
538
+ function errorMessage(error) { return error instanceof Error ? error.message : String(error); }
@@ -1,9 +1,10 @@
1
1
  import { type ReleaseIdentity } from "./release.js";
2
+ import { type RuntimePayloadInventory } from "./dependency-layer.js";
2
3
  export declare const RELEASE_MANIFEST_FILENAME: string;
3
4
  export interface MaterializedRelease extends ReleaseIdentity {
4
5
  readonly releaseRoot: string;
5
6
  }
6
- export type ReleaseContentOperation = "source-read" | "candidate-write" | "verification-read";
7
+ export type ReleaseContentOperation = "source-read" | "candidate-write" | "layer-write" | "layer-reuse" | "verification-read";
7
8
  export interface ReleaseContentOperationEvent {
8
9
  readonly operation: ReleaseContentOperation;
9
10
  readonly path: string;
@@ -15,6 +16,7 @@ export interface MaterializeReleaseOptions {
15
16
  readonly fileCount: number;
16
17
  }) => void;
17
18
  readonly onOperation?: (event: ReleaseContentOperationEvent) => void;
19
+ readonly onRuntimeInventory?: (inventory: RuntimePayloadInventory) => void;
18
20
  /** Test seam for deterministic write-failure coverage. */
19
21
  readonly writeCandidateFile?: (path: string, bytes: Uint8Array, mode: number) => Promise<void>;
20
22
  }