perf(skills): store repository packages outside Postgres

Convert legacy packages automatically, restore exact commits on cache misses, and expire unused recoverable packages while keeping warm turn checks metadata-only.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-28 17:15:19 -05:00
1 parent 65fa1cd617
commit 0bb36b2c6b
53 files changed
+13120 -203

No files matched your search

@@ -0,0 +1,57 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { Db } from "@paperclipai/db";
import { resolvePaperclipInstanceRoot } from "../../home-paths.js";
import { materializeAsset } from "./runtime-context.js";
import { removeRuntimeAssetTree, runtimeAssetRetention, RUNTIME_ASSET_RETENTION_MS } from "./runtime-asset-retention.js";
let home: string;
const originalHome = process.env.PAPERCLIP_HOME;
beforeEach(async () => { home = await fs.mkdtemp(path.join(os.tmpdir(), "runtime-asset-gc-")); process.env.PAPERCLIP_HOME = home; });
afterEach(async () => {
await removeRuntimeAssetTree(home);
if (originalHome === undefined) delete process.env.PAPERCLIP_HOME; else process.env.PAPERCLIP_HOME = originalHome;
vi.restoreAllMocks();
});
function database(rows: unknown[] = []) {
const select = vi.fn(() => ({ from: () => ({ where: () => ({ limit: async () => rows }) }) }));
return { db: { select } as unknown as Db, select };
}
async function age(digest: string) {
const manifest = path.join(resolvePaperclipInstanceRoot(), "runtime-context-assets", "manifests", `${digest}.json`);
const old = new Date(Date.now() - RUNTIME_ASSET_RETENTION_MS - 1_000);
await fs.utimes(manifest, old, old);
}
describe("runtime asset retention", () => {
it("has no database work when empty/fresh, removes expired copies and rebuilds the same digest", async () => {
const { db, select } = database();
const worker = runtimeAssetRetention(db);
await worker.tick(new AbortController().signal);
expect(worker.nextRunAt()).toBeNull();
expect(select).not.toHaveBeenCalled();
const files = [{ path: "SKILL.md", content: Buffer.from("immutable skill"), mode: 0o444 }];
const asset = await materializeAsset(files);
await worker.tick(new AbortController().signal);
expect(worker.nextRunAt()).toBeGreaterThan(Date.now());
expect(select).not.toHaveBeenCalled();
await age(asset.digest);
await worker.tick(new AbortController().signal);
expect(select).toHaveBeenCalledOnce();
expect(await fs.stat(asset.rootPath).catch(() => null)).toBeNull();
expect(worker.nextRunAt()).toBeNull();
await worker.tick(new AbortController().signal);
expect(select).toHaveBeenCalledOnce();
expect(await materializeAsset(files)).toEqual(asset);
});
it("keeps files when an unfinished run has no trustworthy input", async () => {
const asset = await materializeAsset([{ path: "SKILL.md", content: Buffer.from("pinned skill"), mode: 0o444 }]);
await age(asset.digest);
const { db } = database([{ id: "run", companyId: "company", agentId: "agent", input: null }]);
const worker = runtimeAssetRetention(db);
await worker.tick(new AbortController().signal);
expect((await fs.stat(asset.rootPath)).isDirectory()).toBe(true);
expect(worker.nextRunAt()).toBeNull();
});
});
@@ -0,0 +1,145 @@
import fs from "node:fs/promises";
import path from "node:path";
import { and, eq, sql } from "drizzle-orm";
import { heartbeatRuns, type Db } from "@paperclipai/db";
import { withDirectoryPublicationLock } from "@paperclipai/adapter-utils/workspace-restore-merge";
import { resolvePaperclipInstanceRoot } from "../../home-paths.js";
import { parseNativeExecutionInput } from "../../vendor/paperclip-runner/index.js";
import { ensureSkillCacheDirectory, skillRunNeedsFilesSql } from "../skill-version-cache.js";
import type { createDeliveryWorkCoordinator } from "../delivery-work-coordinator.js";
import { DELIVERY_QUEUES } from "../delivery-work-notifications.js";
export const RUNTIME_ASSET_RETENTION_MS = 30 * 24 * 60 * 60_000;
const TOUCH_MS = 60 * 60_000;
const touched = new Map<string, number>();
const observers = new Set<() => void>();
const root = () => path.join(resolvePaperclipInstanceRoot(), "runtime-context-assets");
function paths(digest: string) {
if (!/^[a-f0-9]{64}$/.test(digest)) throw new Error("Invalid runtime asset digest");
return { bundle: path.join(root(), "bundles", digest), manifest: path.join(root(), "manifests", `${digest}.json`) };
}
/** Persist an hourly access lease, under the same filesystem lock as deletion.
* Warm turns inside that lease add no I/O or database calls. The long grace also
* protects the short interval between preparation and durable run pinning. */
export async function withRuntimeAssetUse<T>(digest: string, consume: (locked: boolean) => Promise<T>, mutation = false): Promise<T> {
const target = paths(digest);
const last = touched.get(target.manifest);
const fresh = last !== undefined && Date.now() - last < TOUCH_MS;
if (fresh && !mutation) return consume(false);
await ensureSkillCacheDirectory(path.dirname(target.bundle), true);
return withDirectoryPublicationLock(target.bundle, async () => {
const result = await consume(true);
if (!fresh && (await fs.lstat(target.manifest).catch(() => null))?.isFile()) {
const now = Date.now();
await fs.utimes(target.manifest, new Date(now), new Date(now));
if (touched.size >= 4096) touched.delete(touched.keys().next().value!);
touched.set(target.manifest, now);
for (const wake of observers) wake();
}
return result;
});
}
async function protectedAssets(db: Db): Promise<Set<string> | null> {
const rows = await db.select({ id: heartbeatRuns.id, companyId: heartbeatRuns.companyId, agentId: heartbeatRuns.agentId,
input: sql<unknown>`${heartbeatRuns.runnerProfileJson}->'nativeExecutionInput'` }).from(heartbeatRuns)
.where(and(eq(heartbeatRuns.runtimeMode, "native"), skillRunNeedsFilesSql())).limit(1001);
if (rows.length > 1000) return null;
const pins = new Set<string>();
for (const row of rows) {
try {
const input = parseNativeExecutionInput(row.input);
if (input.binding.runId !== row.id || input.binding.companyId !== row.companyId || input.binding.agentId !== row.agentId) return null;
if (!("runtimeContext" in input)) return null;
for (const asset of [input.runtimeContext.instructions.bundle, ...input.runtimeContext.skills.map(skill => skill.bundle)]) pins.add(asset.digest);
} catch { return null; } // An unfinished/legacy input cannot prove absence.
}
return pins;
}
export async function removeRuntimeAssetTree(directory: string) {
const stat = await fs.lstat(directory).catch((error: NodeJS.ErrnoException) => {
if (error.code === "ENOENT") return null;
throw error;
});
if (!stat) return;
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error("Unsafe runtime asset directory");
await fs.chmod(directory, 0o700);
for (const entry of await fs.readdir(directory, { withFileTypes: true })) {
if (entry.isDirectory()) await removeRuntimeAssetTree(path.join(directory, entry.name));
else if (!entry.isFile()) throw new Error("Unsafe runtime asset entry");
}
await fs.rm(directory, { recursive: true });
}
export function runtimeAssetRetention(db: Db) {
let next: number | null = null;
async function tick(signal: AbortSignal) {
next = null;
const directory = path.join(root(), "manifests");
try { await ensureSkillCacheDirectory(directory); }
catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; }
const candidates: Array<{ digest: string; at: number }> = [];
const seen = new Set<string>();
const manifests = await fs.readdir(directory, { withFileTypes: true }).catch((error: NodeJS.ErrnoException) => {
if (error.code === "ENOENT") return []; throw error;
});
for (const entry of manifests) {
signal.throwIfAborted();
if (!entry.isFile() || !/^[a-f0-9]{64}\.json$/.test(entry.name)) continue;
const stat = await fs.lstat(path.join(directory, entry.name)).catch(() => null);
if (!stat?.isFile()) continue;
seen.add(entry.name.slice(0, -5));
const at = stat.mtimeMs + RUNTIME_ASSET_RETENTION_MS;
if (at <= Date.now()) candidates.push({ digest: entry.name.slice(0, -5), at });
else next = Math.min(next ?? Infinity, at);
}
// A crash between bundle rename and manifest publication can leave an
// unreferenced bundle. The same age, lock and run-pin checks apply.
const bundles = path.join(root(), "bundles");
try {
await ensureSkillCacheDirectory(bundles);
for (const entry of await fs.readdir(bundles, { withFileTypes: true })) {
signal.throwIfAborted();
if (!entry.isDirectory() || !/^[a-f0-9]{64}$/.test(entry.name) || seen.has(entry.name)) continue;
const stat = await fs.lstat(path.join(bundles, entry.name)).catch(() => null);
if (!stat?.isDirectory()) continue;
const at = stat.mtimeMs + RUNTIME_ASSET_RETENTION_MS;
if (at <= Date.now()) candidates.push({ digest: entry.name, at });
else next = Math.min(next ?? Infinity, at);
}
} catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; }
if (!candidates.length) return; // No SQL merely to discover idle history.
const pins = await protectedAssets(db);
if (!pins) return; // Reconsider on the next asset use or startup, without polling.
const eligible = candidates.filter(entry => !pins.has(entry.digest));
for (const { digest } of eligible.slice(0, 32)) {
signal.throwIfAborted();
const target = paths(digest);
await ensureSkillCacheDirectory(path.dirname(target.bundle), true);
await withDirectoryPublicationLock(target.bundle, async () => {
const stat = await fs.lstat(target.manifest).catch(() => null) ?? await fs.lstat(target.bundle).catch(() => null);
if (!stat || (!stat.isFile() && !stat.isDirectory()) || stat.isSymbolicLink()) return;
const at = stat.mtimeMs + RUNTIME_ASSET_RETENTION_MS;
if (at > Date.now()) { next = Math.min(next ?? Infinity, at); return; }
await removeRuntimeAssetTree(target.bundle);
await fs.rm(target.manifest, { force: true });
touched.delete(target.manifest);
});
}
if (eligible.length > 32) next = Math.min(next ?? Infinity, Date.now() + 1_000);
}
function start(coordinator: Pick<ReturnType<typeof createDeliveryWorkCoordinator>, "register">) {
let requested = 0, completed = -1;
const worker = coordinator.register(DELIVERY_QUEUES.runtimeAssetRetention, {
retryMs: 5_000, pausePassiveMaintenance: true,
async run(signal) { const generation = requested; await tick(signal); completed = generation; },
hasPending: async () => false,
nextRunAt: () => requested !== completed ? Date.now() + 5_000 : next,
});
const wake = () => { requested++; worker.wake(); };
observers.add(wake);
return { ...worker, async stop() { observers.delete(wake); await worker.stop(); } };
}
return { tick, start, nextRunAt: () => next };
}
@@ -1,3 +1,4 @@
import fs from "node:fs/promises";
import { mkdtemp, mkdir, readFile, readdir, chmod, lstat, rm, stat, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
@@ -290,7 +291,11 @@ describe("buildNativeRuntimeContext", () => {
};
const context = await buildNativeRuntimeContext(input);
const reads = vi.spyOn(fs, "readFile");
const repeated = await buildNativeRuntimeContext(input);
expect(reads.mock.calls.some(([file]) => String(file).startsWith(selectedRoot)
|| String(file).startsWith(context.skills[0]!.bundle.rootPath))).toBe(false);
reads.mockRestore();
expect(serviceMocks.exportFiles).toHaveBeenCalledWith(input.agent, { rejectSymlinks: true });
expect(serviceMocks.getEffectiveToolAssignmentsForAgent).toHaveBeenCalledWith("company-1", "agent-1");
@@ -322,6 +327,19 @@ describe("buildNativeRuntimeContext", () => {
expect(repeated.aggregateDigest).toBe(context.aggregateDigest);
expect(repeated.instructions.bundle.rootPath).toBe(context.instructions.bundle.rootPath);
expect(repeated.skills[0]!.bundle.rootPath).toBe(context.skills[0]!.bundle.rootPath);
await writeFile(path.join(selectedRoot, "references/checklist.md"), "- Verify edits\n");
const edited = await buildNativeRuntimeContext(input);
expect(edited.skills[0]!.bundle.digest).not.toBe(context.skills[0]!.bundle.digest);
const corrupted = path.join(edited.skills[0]!.bundle.rootPath, "SKILL.md");
await chmod(corrupted, 0o644);
await writeFile(corrupted, "Corrupted saved asset");
const repaired = await buildNativeRuntimeContext(input);
expect(repaired.skills[0]!.bundle.digest).toBe(edited.skills[0]!.bundle.digest);
expect(await readFile(corrupted, "utf8")).toContain("Use the checklist");
await chmod(path.dirname(corrupted), 0o755);
await fs.rm(corrupted);
expect((await buildNativeRuntimeContext(input)).skills[0]!.bundle.digest).toBe(edited.skills[0]!.bundle.digest);
expect(await readFile(corrupted, "utf8")).toContain("Use the checklist");
});
it("does not materialize assigned skills or external MCP in preconfigured mode", async () => {
@@ -1,3 +1,6 @@
import { withRuntimeAssetUse, removeRuntimeAssetTree } from "./runtime-asset-retention.js";
import { cachedSkillAsset, rememberSkillAsset } from "./skill-asset-cache.js";
import { verifyFileManifest, type FileManifestEntry } from "../file-manifest-cache.js";
import { measureRunPhase } from "../execution-preparation-timing.js";
import { spekoToolsForSession } from "../voice/speko-agent-tools.js";
import { githubBotConnectionIdsForRun } from "../chat-github-tools.js";
@@ -83,14 +86,9 @@ async function verifyMaterializedAsset(
rootPath: string,
manifestFiles: Array<{ path: string; sha256: string; mode: number; size: number }>,
): Promise<void> {
const actual = await measureRunPhase("runtime_context.asset_read", () => collectDirectoryFiles(rootPath));
const actualByPath = new Map(actual.map((file) => [file.path, file]));
if (actualByPath.size !== manifestFiles.length) throw new Error("runtime context asset file count mismatch");
for (const expected of manifestFiles) {
const file = actualByPath.get(expected.path);
if (!file || file.content.byteLength !== expected.size || sha256(file.content) !== expected.sha256 || (file.mode & 0o555) !== expected.mode) {
throw new Error(`runtime context asset digest mismatch: ${expected.path}`);
}
const expected = manifestFiles.map(file => ({ ...file, executable: Boolean(file.mode & 0o111) }));
if (!await measureRunPhase("runtime_context.asset_read", () => verifyFileManifest(rootPath, expected))) {
throw new Error("runtime context asset digest mismatch");
}
}
@@ -105,55 +103,62 @@ export async function materializeAsset(files: AssetFile[]): Promise<NativeRuntim
const manifestDigest = sha256(manifestText);
return { sorted, manifestFiles, totalBytes, assetDigest, manifestText, manifestDigest };
});
const assetsRoot = path.join(resolvePaperclipInstanceRoot(), "runtime-context-assets");
const rootPath = path.join(assetsRoot, "bundles", assetDigest);
const manifestPath = path.join(assetsRoot, "manifests", `${assetDigest}.json`);
const existing = await measureRunPhase("runtime_context.asset_cache", () => fs.readFile(manifestPath, "utf8").catch(() => null));
if (existing !== null) {
if (sha256(existing) !== manifestDigest || !(await fs.stat(rootPath).catch(() => null))?.isDirectory()) throw new Error(`runtime context asset verification failed: ${assetDigest}`);
await measureRunPhase("runtime_context.asset_verify", () => verifyMaterializedAsset(rootPath, manifestFiles), { fileCount: manifestFiles.length, bytes: totalBytes });
return { schema: NATIVE_RUNTIME_ASSET_SCHEMA, digest: assetDigest, manifestDigest, rootPath, fileCount: manifestFiles.length, totalBytes };
}
await measureRunPhase("runtime_context.asset_materialize", async () => {
const stagingRoot = path.join(assetsRoot, ".staging", randomUUID());
await fs.mkdir(stagingRoot, { recursive: true, mode: 0o700 });
try {
for (const file of sorted) {
const target = path.join(stagingRoot, file.path);
await fs.mkdir(path.dirname(target), { recursive: true, mode: 0o700 });
await fs.writeFile(target, file.content, { mode: file.mode & 0o555 });
await fs.chmod(target, file.mode & 0o555);
}
await fs.mkdir(path.dirname(rootPath), { recursive: true, mode: 0o700 });
let publishedRoot = false;
await fs.rename(stagingRoot, rootPath).then(() => {
publishedRoot = true;
}).catch(async (error: NodeJS.ErrnoException) => {
// Concurrent publishers can surface EACCES on macOS when the winning
// destination has already been made read-only. Treat that as a race only
// when the content-addressed destination now exists; a genuine permission
// failure with no published destination must still fail loudly.
const publishedByPeer = (await fs.stat(rootPath).catch(() => null))?.isDirectory() === true;
if (error.code !== "EEXIST" && error.code !== "ENOTEMPTY" && !publishedByPeer) throw error;
await fs.rm(stagingRoot, { recursive: true, force: true });
});
// Publish the writable staging directory first. Renaming a read-only
// directory can fail with EACCES on macOS even when both parents are owned
// and writable. Files are already immutable; directories become immutable
// immediately after this process wins publication.
if (publishedRoot) await makeDirectoriesReadOnly(rootPath);
await fs.mkdir(path.dirname(manifestPath), { recursive: true, mode: 0o700 });
await fs.writeFile(manifestPath, manifestText, { flag: "wx", mode: 0o400 }).catch(async (error: NodeJS.ErrnoException) => {
if (error.code !== "EEXIST") throw error;
if (sha256(await measureRunPhase("runtime_context.asset_cache", () => fs.readFile(manifestPath, "utf8"))) !== manifestDigest) throw new Error(`runtime context manifest race mismatch: ${assetDigest}`);
});
await measureRunPhase("runtime_context.asset_verify", () => verifyMaterializedAsset(rootPath, manifestFiles), { fileCount: manifestFiles.length, bytes: totalBytes });
} catch (error) {
await fs.rm(stagingRoot, { recursive: true, force: true }).catch(() => undefined);
throw error;
return withRuntimeAssetUse(assetDigest, async function publishOrReuse(locked): Promise<NativeRuntimeAssetReference> {
const assetsRoot = path.join(resolvePaperclipInstanceRoot(), "runtime-context-assets");
const rootPath = path.join(assetsRoot, "bundles", assetDigest);
const manifestPath = path.join(assetsRoot, "manifests", `${assetDigest}.json`);
const existing = await measureRunPhase("runtime_context.asset_cache", () => fs.readFile(manifestPath, "utf8").catch(() => null));
const rootExists = (await fs.stat(rootPath).catch(() => null))?.isDirectory();
if (existing !== null && rootExists && sha256(existing) === manifestDigest
&& await verifyFileManifest(rootPath, manifestFiles.map(file => ({ ...file, executable: Boolean(file.mode & 0o111) })))) {
return { schema: NATIVE_RUNTIME_ASSET_SCHEMA, digest: assetDigest, manifestDigest, rootPath, fileCount: manifestFiles.length, totalBytes };
}
}, { fileCount: manifestFiles.length, bytes: totalBytes });
return { schema: NATIVE_RUNTIME_ASSET_SCHEMA, digest: assetDigest, manifestDigest, rootPath, fileCount: manifestFiles.length, totalBytes };
if (!locked) return withRuntimeAssetUse(assetDigest, publishOrReuse, true);
// The caller supplied the exact authoritative bytes for this digest. A missing
// or damaged disposable copy can be rebuilt under the publication/GC lock.
if (rootExists) await removeRuntimeAssetTree(rootPath);
await fs.rm(manifestPath, { force: true });
await measureRunPhase("runtime_context.asset_materialize", async () => {
const stagingRoot = path.join(assetsRoot, ".staging", randomUUID());
await fs.mkdir(stagingRoot, { recursive: true, mode: 0o700 });
try {
for (const file of sorted) {
const target = path.join(stagingRoot, file.path);
await fs.mkdir(path.dirname(target), { recursive: true, mode: 0o700 });
await fs.writeFile(target, file.content, { mode: file.mode & 0o555 });
await fs.chmod(target, file.mode & 0o555);
}
await fs.mkdir(path.dirname(rootPath), { recursive: true, mode: 0o700 });
let publishedRoot = false;
await fs.rename(stagingRoot, rootPath).then(() => {
publishedRoot = true;
}).catch(async (error: NodeJS.ErrnoException) => {
// Concurrent publishers can surface EACCES on macOS when the winning
// destination has already been made read-only. Treat that as a race only
// when the content-addressed destination now exists; a genuine permission
// failure with no published destination must still fail loudly.
const publishedByPeer = (await fs.stat(rootPath).catch(() => null))?.isDirectory() === true;
if (error.code !== "EEXIST" && error.code !== "ENOTEMPTY" && !publishedByPeer) throw error;
await fs.rm(stagingRoot, { recursive: true, force: true });
});
// Publish the writable staging directory first. Renaming a read-only
// directory can fail with EACCES on macOS even when both parents are owned
// and writable. Files are already immutable; directories become immutable
// immediately after this process wins publication.
if (publishedRoot) await makeDirectoriesReadOnly(rootPath);
await fs.mkdir(path.dirname(manifestPath), { recursive: true, mode: 0o700 });
await fs.writeFile(manifestPath, manifestText, { flag: "wx", mode: 0o400 }).catch(async (error: NodeJS.ErrnoException) => {
if (error.code !== "EEXIST") throw error;
if (sha256(await measureRunPhase("runtime_context.asset_cache", () => fs.readFile(manifestPath, "utf8"))) !== manifestDigest) throw new Error(`runtime context manifest race mismatch: ${assetDigest}`);
});
await measureRunPhase("runtime_context.asset_verify", () => verifyMaterializedAsset(rootPath, manifestFiles), { fileCount: manifestFiles.length, bytes: totalBytes });
} catch (error) {
await fs.rm(stagingRoot, { recursive: true, force: true }).catch(() => undefined);
throw error;
}
}, { fileCount: manifestFiles.length, bytes: totalBytes });
return { schema: NATIVE_RUNTIME_ASSET_SCHEMA, digest: assetDigest, manifestDigest, rootPath, fileCount: manifestFiles.length, totalBytes };
});
}
async function materializeInstructionBundle(db: Db, agent: RuntimeAgent, agentFiles = false) {
@@ -180,7 +185,7 @@ async function materializeInstructionBundle(db: Db, agent: RuntimeAgent, agentFi
return { entryPath, bundle: await materializeAsset(files) };
}
async function materializeSelectedSkills(runtimeConfig: Record<string, unknown>, entries: PaperclipSkillEntry[], omitLegacy: boolean) {
export async function materializeSelectedSkills(runtimeConfig: Record<string, unknown>, entries: PaperclipSkillEntry[], omitLegacy: boolean) {
const desiredKeys = resolvePaperclipDesiredSkillNames(runtimeConfig, entries).filter(
(key) => !omitLegacy || key !== PAPERCLIP_OPERATIONAL_SKILL_KEY,
);
@@ -189,7 +194,17 @@ async function materializeSelectedSkills(runtimeConfig: Record<string, unknown>,
const entry = byKey.get(key);
if (!entry) throw new Error(`assigned runtime skill is missing from the company library: ${key}`);
if (entry.sourceStatus === "missing") throw new Error(entry.missingDetail ?? `assigned runtime skill source is missing: ${key}`);
return { key: entry.key, runtimeName: safeRelativePath(entry.runtimeName, `runtime name for ${key}`), versionId: entry.versionId ?? entry.currentVersionId ?? null, bundle: await materializeAsset(await measureRunPhase("runtime_context.asset_read", () => collectDirectoryFiles(entry.source))) };
let bundle = await measureRunPhase("runtime_context.asset_verify", () => cachedSkillAsset(entry.source));
if (!bundle) {
const files = await measureRunPhase("runtime_context.asset_read", () => collectDirectoryFiles(entry.source));
bundle = await materializeAsset(files);
const manifest: FileManifestEntry[] = files.map(file => ({ path: file.path, size: file.content.length,
sha256: sha256(file.content), mode: file.mode & 0o555, executable: Boolean(file.mode & 0o111) }));
// Populate the stat/digest cache against these exact source bytes before
// remembering the reference. A concurrent source edit cannot seed it.
if (await verifyFileManifest(entry.source, manifest)) rememberSkillAsset(entry.source, manifest, bundle);
}
return { key: entry.key, runtimeName: safeRelativePath(entry.runtimeName, `runtime name for ${key}`), versionId: entry.versionId ?? entry.currentVersionId ?? null, bundle };
}));
}
@@ -0,0 +1,46 @@
import { withRuntimeAssetUse } from "./runtime-asset-retention.js";
import fs from "node:fs/promises";
import path from "node:path";
import { createHash } from "node:crypto";
import type { NativeRuntimeAssetReference } from "../../vendor/paperclip-runner/index.js";
import { verifyFileManifest, type FileManifestEntry } from "../file-manifest-cache.js";
import { resolvePaperclipInstanceRoot } from "../../home-paths.js";
type Cached = { manifest: FileManifestEntry[]; bundle: NativeRuntimeAssetReference };
const entries = new Map<string, Cached>();
let fileCount = 0;
const MAX_FILES = 32_000;
const MAX_ENTRIES = 128;
const cacheKey = (source: string) => `${resolvePaperclipInstanceRoot()}:${source}`;
/** The caller supplies freshly authorized runtime entries on every turn. This
* cache retains no authority or file bodies, and verifies source and destination
* identities before reusing the content-addressed native asset reference. */
export async function cachedSkillAsset(source: string): Promise<NativeRuntimeAssetReference | null> {
const cached = entries.get(cacheKey(source));
if (!cached || !await verifyFileManifest(source, cached.manifest)) return null;
return withRuntimeAssetUse(cached.bundle.digest, async () => {
const manifestPath = path.join(resolvePaperclipInstanceRoot(), "runtime-context-assets", "manifests", `${cached.bundle.digest}.json`);
const manifest = await fs.readFile(manifestPath).catch(() => null);
if (!manifest || !(await fs.lstat(cached.bundle.rootPath).catch(() => null))?.isDirectory()) return null;
if (createHash("sha256").update(manifest).digest("hex") !== cached.bundle.manifestDigest
|| !await verifyFileManifest(cached.bundle.rootPath, cached.manifest)) {
return null;
}
return cached.bundle;
});
}
export function rememberSkillAsset(source: string, manifest: FileManifestEntry[], bundle: NativeRuntimeAssetReference) {
if (manifest.length > MAX_FILES) return;
const key = cacheKey(source);
const previous = entries.get(key);
if (previous) { fileCount -= previous.manifest.length; entries.delete(key); }
while (entries.size && (fileCount + manifest.length > MAX_FILES || entries.size >= MAX_ENTRIES)) {
const first = entries.keys().next().value!;
fileCount -= entries.get(first)!.manifest.length;
entries.delete(first);
}
entries.set(key, { manifest, bundle });
fileCount += manifest.length;
}