@tangle-network/agent-profile-materialize 0.23.1 → 0.25.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/candidate.js CHANGED
@@ -208,6 +208,7 @@ function candidateResourcesForClassicMaterializer(resources, resolvedResources)
208
208
  };
209
209
  };
210
210
  return {
211
+ ...resources,
211
212
  files: resources.files?.map((mount) => ({
212
213
  path: mount.path,
213
214
  resource: convert(mount.resource),
@@ -29,8 +29,8 @@ export declare function claimProfileBundle(root: string, claim: ProfileBundleCla
29
29
  }) => Promise<boolean>): Promise<ProfileBundleClaimResult>;
30
30
  /**
31
31
  * Open a host-side cache handle over `root`, creating the layout and sweeping
32
- * stale `incoming/` residue synchronously so the cache is deterministic to
33
- * use immediately afterwards.
32
+ * stale `incoming/` residue synchronously when the root is idle. A busy root
33
+ * leaves cleanup to its serialized admissions and sweeps.
34
34
  */
35
35
  export declare function openProfileBundleCache(root: string, options?: {
36
36
  maxBytes?: number;
@@ -3,7 +3,7 @@ import { mkdirSync, readdirSync, rmSync } from "node:fs";
3
3
  import { mkdir, open, readFile, rename, rm, stat, writeFile, } from "node:fs/promises";
4
4
  import { join } from "node:path";
5
5
  import { PROFILE_BUNDLE_DEFAULT_MAX_BYTES, PROFILE_BUNDLE_MAX_BYTES, PROFILE_BUNDLE_MAX_FILE_BYTES, PROFILE_BUNDLE_MAX_FILES, PROFILE_BUNDLE_SCHEMA, } from "./profile-bundle-key.js";
6
- import { evictProfileBundles, evictUnlocked, fsyncDir, loadLru, pathExists, profileBundleManifestPath, readManifestFile, saveLru, sweepProfileBundleCache, withRootLock, } from "./profile-bundle-lru.js";
6
+ import { evictProfileBundles, evictUnlocked, fsyncDir, hasPendingRootWork, loadLru, pathExists, profileBundleManifestPath, readManifestFile, saveLru, sweepProfileBundleCache, sweepUnlocked, withRootLock, } from "./profile-bundle-lru.js";
7
7
  import { hasControlCharacter, normalizeWorkspaceRelativePath, validateFileMode, } from "./workspace-files.js";
8
8
  export * from "./profile-bundle-key.js";
9
9
  export { evictProfileBundles, profileBundleManifestPath, sweepProfileBundleCache, } from "./profile-bundle-lru.js";
@@ -116,11 +116,11 @@ function validateManifestFiles(manifest) {
116
116
  export async function admitProfileBundle(root, manifest, readObject) {
117
117
  validateManifestShape(manifest);
118
118
  const entries = validateManifestFiles(manifest);
119
- await sweepProfileBundleCache(root);
120
119
  await mkdir(join(root, "objects"), { recursive: true });
121
120
  await mkdir(join(root, "bundles"), { recursive: true });
122
121
  await mkdir(join(root, "incoming"), { recursive: true });
123
122
  await withRootLock(root, async () => {
123
+ await sweepUnlocked(root);
124
124
  const manifestPath = profileBundleManifestPath(root, manifest.bundle);
125
125
  const existing = await readManifestFile(manifestPath);
126
126
  if (existing && existing.bundle !== manifest.bundle) {
@@ -293,8 +293,8 @@ export async function claimProfileBundle(root, claim, linkFile) {
293
293
  }
294
294
  /**
295
295
  * Open a host-side cache handle over `root`, creating the layout and sweeping
296
- * stale `incoming/` residue synchronously so the cache is deterministic to
297
- * use immediately afterwards.
296
+ * stale `incoming/` residue synchronously when the root is idle. A busy root
297
+ * leaves cleanup to its serialized admissions and sweeps.
298
298
  */
299
299
  export function openProfileBundleCache(root, options) {
300
300
  const maxBytes = options?.maxBytes ?? PROFILE_BUNDLE_DEFAULT_MAX_BYTES;
@@ -305,8 +305,10 @@ export function openProfileBundleCache(root, options) {
305
305
  mkdirSync(join(root, "bundles"), { recursive: true });
306
306
  const incoming = join(root, "incoming");
307
307
  mkdirSync(incoming, { recursive: true });
308
- for (const entry of readdirSync(incoming)) {
309
- rmSync(join(incoming, entry), { recursive: true, force: true });
308
+ if (!hasPendingRootWork(root)) {
309
+ for (const entry of readdirSync(incoming)) {
310
+ rmSync(join(incoming, entry), { recursive: true, force: true });
311
+ }
310
312
  }
311
313
  return {
312
314
  root,
@@ -20,6 +20,8 @@ export interface ProfileBundleLru {
20
20
  export declare function pathExists(path: string): Promise<boolean>;
21
21
  export declare function fsyncDir(dir: string): Promise<void>;
22
22
  export declare function withRootLock<T>(root: string, run: () => Promise<T>): Promise<T>;
23
+ /** Synchronous open must not sweep a root with pending asynchronous work. */
24
+ export declare function hasPendingRootWork(root: string): boolean;
23
25
  export declare function readManifestFile(path: string): Promise<{
24
26
  schema?: unknown;
25
27
  bundle?: unknown;
@@ -32,13 +34,10 @@ export declare function readManifestFile(path: string): Promise<{
32
34
  * The raw key is always recoverable from the manifest's `bundle` field.
33
35
  */
34
36
  export declare function profileBundleManifestPath(root: string, bundle: string): string;
35
- /** Rebuild advisory lru accounting from a disk scan (objects are immutable). */
36
- /**
37
- * Remove stale `incoming/` admission directories. An admit only ever renames
38
- * OUT of `incoming/`, so anything found here is residue from an interrupted
39
- * admission and is safe to delete; objects and manifests are untouched.
40
- */
37
+ /** Sweep only while holding the per-root lock; active admissions own incoming/. */
41
38
  export declare function sweepProfileBundleCache(root: string): Promise<void>;
39
+ /** Remove interrupted admission residue; the caller must hold the per-root lock. */
40
+ export declare function sweepUnlocked(root: string): Promise<void>;
42
41
  export declare function loadLru(root: string): Promise<ProfileBundleLru>;
43
42
  /** Best-effort by contract: lru corruption or a full cache must never break admit/claim. */
44
43
  export declare function saveLru(root: string, lru: ProfileBundleLru): Promise<void>;
@@ -1,6 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { open, readdir, readFile, rename, rm, stat, writeFile, } from "node:fs/promises";
3
- import { join } from "node:path";
3
+ import { join, resolve } from "node:path";
4
4
  import { compareStrings } from "./workspace-files.js";
5
5
  export async function pathExists(path) {
6
6
  try {
@@ -25,14 +25,24 @@ export async function fsyncDir(dir) {
25
25
  // Best-effort durability: a cache without directory fsync still verifies.
26
26
  }
27
27
  }
28
- /** Serialize admits/evictions per cache root so lru read-modify-write stays deterministic. */
28
+ /** Serialize admissions, sweeps, and evictions per cache root. */
29
29
  const rootQueues = new Map();
30
30
  export function withRootLock(root, run) {
31
- const previous = rootQueues.get(root) ?? Promise.resolve();
31
+ const key = resolve(root);
32
+ const previous = rootQueues.get(key) ?? Promise.resolve();
32
33
  const next = previous.then(run, run);
33
- rootQueues.set(root, next.catch(() => undefined));
34
+ const settled = next.then(() => undefined, () => undefined);
35
+ rootQueues.set(key, settled);
36
+ void settled.then(() => {
37
+ if (rootQueues.get(key) === settled)
38
+ rootQueues.delete(key);
39
+ });
34
40
  return next;
35
41
  }
42
+ /** Synchronous open must not sweep a root with pending asynchronous work. */
43
+ export function hasPendingRootWork(root) {
44
+ return rootQueues.has(resolve(root));
45
+ }
36
46
  export async function readManifestFile(path) {
37
47
  try {
38
48
  return JSON.parse(await readFile(path, "utf8"));
@@ -50,13 +60,12 @@ export function profileBundleManifestPath(root, bundle) {
50
60
  const filename = bundle.replace(/[^A-Za-z0-9._-]/g, (char) => `_${char.codePointAt(0)?.toString(16) ?? "3f"}_`);
51
61
  return join(root, "bundles", `${filename}.json`);
52
62
  }
53
- /** Rebuild advisory lru accounting from a disk scan (objects are immutable). */
54
- /**
55
- * Remove stale `incoming/` admission directories. An admit only ever renames
56
- * OUT of `incoming/`, so anything found here is residue from an interrupted
57
- * admission and is safe to delete; objects and manifests are untouched.
58
- */
63
+ /** Sweep only while holding the per-root lock; active admissions own incoming/. */
59
64
  export async function sweepProfileBundleCache(root) {
65
+ await withRootLock(root, () => sweepUnlocked(root));
66
+ }
67
+ /** Remove interrupted admission residue; the caller must hold the per-root lock. */
68
+ export async function sweepUnlocked(root) {
60
69
  const incoming = join(root, "incoming");
61
70
  let entries;
62
71
  try {
@@ -40,6 +40,10 @@ export function executionControlConflict(profile, harness) {
40
40
  export function materializeProfile(inputProfile, harness, opts = {}) {
41
41
  assertMaterializerHarness(harness);
42
42
  const profile = agentProfileSchema.parse(inputProfile);
43
+ const knowledge = profile.resources && Reflect.get(profile.resources, "knowledge");
44
+ if (Array.isArray(knowledge) && knowledge.length > 0) {
45
+ throw new Error("Knowledge source generations must be owner-authorized and resolved by the execution runtime before materialization");
46
+ }
43
47
  const skip = new Set(assertSkippableDimensions(opts.skip));
44
48
  const binds = assertBindableLaunchControls(opts.binds);
45
49
  const instructionsRoot = opts.instructionsRoot ?? "workspace";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/agent-profile-materialize",
3
- "version": "0.23.1",
3
+ "version": "0.25.2",
4
4
  "description": "Materialize an AgentProfile into the native per-harness workspace files (the one shared materializer for cli-bridge, agent-dev-container, and VB).",
5
5
  "type": "module",
6
6
  "repository": {
@@ -25,14 +25,14 @@
25
25
  "dist"
26
26
  ],
27
27
  "devDependencies": {
28
- "@tangle-network/agent-interface": "3.0.0",
29
- "@tangle-network/agent-interface-current": "npm:@tangle-network/agent-interface@3.0.0",
28
+ "@tangle-network/agent-interface": "5.0.0",
29
+ "@tangle-network/agent-interface-current": "npm:@tangle-network/agent-interface@5.0.0",
30
30
  "@types/node": "25.6.0",
31
31
  "typescript": "^6.0.3",
32
32
  "vitest": "^4.1.11"
33
33
  },
34
34
  "peerDependencies": {
35
- "@tangle-network/agent-interface": "^3.0.0"
35
+ "@tangle-network/agent-interface": "^3.0.0 || ^4.0.0 || ^5.0.0"
36
36
  },
37
37
  "scripts": {
38
38
  "build": "tsc -p tsconfig.json",