fix(skills): pull upstream skill runtime resolution to stop event-loop starvation
Build: Production / build (push) Failing after 12m39s
Build: Production / build (push) Failing after 12m39s
The fork's listRuntimeSkillEntries rematerialized every skill's files from the DB on every heartbeat run dispatch — fs.rm + fs.mkdir + per-file readFile/writeFile, sequentially per skill. With 24 configured skills and 5 concurrent agents, this saturated the Node event loop badly enough that executeRun continuations couldn't reach activeRunExecutions.add() within the orphan-reaper's 5-min threshold, causing reaper to false-positive runs as "process_lost". Upstream's listRuntimeSkillEntries calls resolveRuntimeSkillSource, which checks if the materialized directory already exists on disk and short- circuits when it does. Fixes the symptom at the root. Replaces these files with upstream/master content: - server/src/services/company-skills.ts - server/src/services/heartbeat.ts - server/src/services/workspace-runtime.ts - server/src/services/company-portability.ts - server/src/routes/company-skills.ts - server/src/routes/agents.ts - packages/adapter-utils/src/server-utils.ts Pulls in supporting upstream files: - server/src/services/catalog-provenance.ts - server/src/services/skills-catalog.ts - server/src/services/github-fetch.ts - server/src/services/portable-path.ts - packages/skills-catalog/ (new package) - packages/db document_annotation_* schema + migration 0091 - packages/shared document-annotation types/validators Drops fork features (to be re-evaluated later): - Gitea/Forgejo git skill sources (server/src/services/git-source.ts deleted) - PAT support for private skill repos - Fork-specific secret-export portability extensions Adds agentId: null to acquireRunLease test-probe call in routes/agents.ts to satisfy the fork's environment-runtime agentId requirement (kept). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -133,6 +133,8 @@ export interface PaperclipSkillEntry {
|
||||
key: string;
|
||||
runtimeName: string;
|
||||
source: string;
|
||||
sourceStatus?: "available" | "missing";
|
||||
missingDetail?: string | null;
|
||||
required?: boolean;
|
||||
requiredReason?: string | null;
|
||||
}
|
||||
@@ -161,6 +163,22 @@ interface PersistentSkillSnapshotOptions {
|
||||
warnings?: string[];
|
||||
}
|
||||
|
||||
interface RuntimeMountedSkillSnapshotOptions {
|
||||
adapterType: string;
|
||||
availableEntries: PaperclipSkillEntry[];
|
||||
desiredSkills: string[];
|
||||
configuredDetail: string | ((entry: PaperclipSkillEntry) => string | null);
|
||||
missingDetail?: string;
|
||||
mode?: "ephemeral" | "unsupported";
|
||||
supported?: boolean;
|
||||
unsupportedDetail?: string | ((entry: PaperclipSkillEntry) => string | null);
|
||||
warnings?: string[];
|
||||
externalInstalled?: Map<string, InstalledSkillTarget>;
|
||||
externalLocationLabel?: string | null;
|
||||
externalDetail?: string;
|
||||
skillsHome?: string;
|
||||
}
|
||||
|
||||
function normalizePathSlashes(value: string): string {
|
||||
return value.replaceAll("\\", "/");
|
||||
}
|
||||
@@ -193,6 +211,26 @@ function buildManagedSkillOrigin(entry: { required?: boolean }): Pick<
|
||||
};
|
||||
}
|
||||
|
||||
function isPaperclipSkillSourceMissing(entry: PaperclipSkillEntry) {
|
||||
return entry.sourceStatus === "missing";
|
||||
}
|
||||
|
||||
function resolvePaperclipSkillMissingDetail(
|
||||
entry: PaperclipSkillEntry,
|
||||
fallback: string,
|
||||
) {
|
||||
return entry.missingDetail?.trim() || fallback;
|
||||
}
|
||||
|
||||
function resolveSkillDetail(
|
||||
detail: string | ((entry: PaperclipSkillEntry) => string | null) | null | undefined,
|
||||
entry: PaperclipSkillEntry,
|
||||
): string | null {
|
||||
if (typeof detail === "function") return detail(entry);
|
||||
if (typeof detail === "string") return detail;
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolveInstalledEntryTarget(
|
||||
skillsHome: string,
|
||||
entryName: string,
|
||||
@@ -1381,6 +1419,120 @@ export async function readInstalledSkillTargets(skillsHome: string): Promise<Map
|
||||
return out;
|
||||
}
|
||||
|
||||
export function buildRuntimeMountedSkillSnapshot(
|
||||
options: RuntimeMountedSkillSnapshotOptions,
|
||||
): AdapterSkillSnapshot {
|
||||
const {
|
||||
adapterType,
|
||||
availableEntries,
|
||||
desiredSkills,
|
||||
configuredDetail,
|
||||
missingDetail = "Paperclip cannot find this skill in the local runtime skills directory.",
|
||||
mode = "ephemeral",
|
||||
externalInstalled,
|
||||
externalLocationLabel,
|
||||
externalDetail = "Installed outside Paperclip management.",
|
||||
skillsHome,
|
||||
} = options;
|
||||
const supported = options.supported ?? mode !== "unsupported";
|
||||
const availableByKey = new Map(availableEntries.map((entry) => [entry.key, entry]));
|
||||
const desiredSet = new Set(desiredSkills);
|
||||
const entries: AdapterSkillEntry[] = [];
|
||||
const warnings = [...(options.warnings ?? [])];
|
||||
|
||||
for (const available of availableEntries) {
|
||||
const desired = desiredSet.has(available.key);
|
||||
if (isPaperclipSkillSourceMissing(available)) {
|
||||
entries.push({
|
||||
key: available.key,
|
||||
runtimeName: available.runtimeName,
|
||||
desired,
|
||||
managed: true,
|
||||
state: "missing",
|
||||
sourcePath: null,
|
||||
targetPath: null,
|
||||
detail: resolvePaperclipSkillMissingDetail(available, missingDetail),
|
||||
required: Boolean(available.required),
|
||||
requiredReason: available.requiredReason ?? null,
|
||||
...buildManagedSkillOrigin(available),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const configured = supported && mode === "ephemeral" && desired;
|
||||
entries.push({
|
||||
key: available.key,
|
||||
runtimeName: available.runtimeName,
|
||||
desired,
|
||||
managed: true,
|
||||
state: configured ? "configured" : "available",
|
||||
sourcePath: available.source,
|
||||
targetPath: null,
|
||||
detail: desired
|
||||
? configured
|
||||
? resolveSkillDetail(configuredDetail, available)
|
||||
: resolveSkillDetail(
|
||||
options.unsupportedDetail
|
||||
?? "Desired state is stored in Paperclip only; this adapter cannot apply skills at runtime.",
|
||||
available,
|
||||
)
|
||||
: null,
|
||||
required: Boolean(available.required),
|
||||
requiredReason: available.requiredReason ?? null,
|
||||
...buildManagedSkillOrigin(available),
|
||||
});
|
||||
}
|
||||
|
||||
for (const desiredSkill of desiredSkills) {
|
||||
if (availableByKey.has(desiredSkill)) continue;
|
||||
warnings.push(`Desired skill "${desiredSkill}" is not available from the Paperclip skills directory.`);
|
||||
entries.push({
|
||||
key: desiredSkill,
|
||||
runtimeName: null,
|
||||
desired: true,
|
||||
managed: true,
|
||||
state: "missing",
|
||||
sourcePath: null,
|
||||
targetPath: null,
|
||||
detail: missingDetail,
|
||||
origin: "external_unknown",
|
||||
originLabel: "External or unavailable",
|
||||
readOnly: false,
|
||||
});
|
||||
}
|
||||
|
||||
if (externalInstalled) {
|
||||
for (const [name, installedEntry] of externalInstalled.entries()) {
|
||||
if (availableEntries.some((entry) => entry.runtimeName === name)) continue;
|
||||
entries.push({
|
||||
key: name,
|
||||
runtimeName: name,
|
||||
desired: false,
|
||||
managed: false,
|
||||
state: "external",
|
||||
origin: "user_installed",
|
||||
originLabel: "User-installed",
|
||||
locationLabel: skillLocationLabel(externalLocationLabel),
|
||||
readOnly: true,
|
||||
sourcePath: null,
|
||||
targetPath: installedEntry.targetPath ?? (skillsHome ? path.join(skillsHome, name) : null),
|
||||
detail: externalDetail,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
entries.sort((left, right) => left.key.localeCompare(right.key));
|
||||
|
||||
return {
|
||||
adapterType,
|
||||
supported,
|
||||
mode,
|
||||
desiredSkills,
|
||||
entries,
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildPersistentSkillSnapshot(
|
||||
options: PersistentSkillSnapshotOptions,
|
||||
): AdapterSkillSnapshot {
|
||||
@@ -1404,6 +1556,26 @@ export function buildPersistentSkillSnapshot(
|
||||
for (const available of availableEntries) {
|
||||
const installedEntry = installed.get(available.runtimeName) ?? null;
|
||||
const desired = desiredSet.has(available.key);
|
||||
if (isPaperclipSkillSourceMissing(available)) {
|
||||
entries.push({
|
||||
key: available.key,
|
||||
runtimeName: available.runtimeName,
|
||||
desired,
|
||||
managed: true,
|
||||
state: "missing",
|
||||
sourcePath: null,
|
||||
targetPath: path.join(skillsHome, available.runtimeName),
|
||||
detail: resolvePaperclipSkillMissingDetail(
|
||||
available,
|
||||
missingDetail,
|
||||
),
|
||||
required: Boolean(available.required),
|
||||
requiredReason: available.requiredReason ?? null,
|
||||
...buildManagedSkillOrigin(available),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
let state: AdapterSkillEntry["state"] = "available";
|
||||
let managed = false;
|
||||
let detail: string | null = null;
|
||||
@@ -1496,6 +1668,11 @@ function normalizeConfiguredPaperclipRuntimeSkills(value: unknown): PaperclipSki
|
||||
key,
|
||||
runtimeName,
|
||||
source,
|
||||
sourceStatus: entry.sourceStatus === "missing" ? "missing" : "available",
|
||||
missingDetail:
|
||||
typeof entry.missingDetail === "string" && entry.missingDetail.trim().length > 0
|
||||
? entry.missingDetail.trim()
|
||||
: null,
|
||||
required: asBoolean(entry.required, false),
|
||||
requiredReason:
|
||||
typeof entry.requiredReason === "string" && entry.requiredReason.trim().length > 0
|
||||
|
||||
Reference in New Issue
Block a user