[codex] Speed up company skill detail loading (#4380)

## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies
> - Company skills are part of the control plane for distributing
reusable capabilities
> - Board flows that inspect company skill detail should stay responsive
because they are operator-facing control-plane reads
> - The existing detail path was doing broader work than needed for the
specific detail screen
> - This pull request narrows that company-skill detail loading path and
adds a regression test around it
> - The benefit is faster company skill detail reads without changing
the external API contract

## What Changed

- tightened the company-skill detail loading path in
`server/src/services/company-skills.ts`
- added `server/src/__tests__/company-skills-detail.test.ts` to verify
the detail route only pulls the required data

## Verification

- `pnpm exec vitest run --project @paperclipai/server
server/src/__tests__/company-skills-detail.test.ts`

## Risks

- Low risk: this only changes the company-skill detail query path, but
any missed assumption in the detail consumer would surface when loading
that screen

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

- OpenAI Codex GPT-5-based coding agent with tool use and code execution
in the Codex CLI environment

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots
- [ ] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
This commit is contained in:
Dotta 2026-04-24 07:37:13 -05:00 committed by GitHub
parent e4995bbb1c
commit 35a9dc37b0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 317 additions and 64 deletions

View file

@ -27,13 +27,11 @@ import type {
CompanySkillUsageAgent,
} from "@paperclipai/shared";
import { normalizeAgentUrlKey } from "@paperclipai/shared";
import { findActiveServerAdapter } from "../adapters/index.js";
import { resolvePaperclipInstanceRoot } from "../home-paths.js";
import { notFound, unprocessable } from "../errors.js";
import { ghFetch, gitHubApiBase, resolveRawGitHubUrl } from "./github-fetch.js";
import { agentService } from "./agents.js";
import { projectService } from "./projects.js";
import { secretService } from "./secrets.js";
type CompanySkillRow = typeof companySkills.$inferSelect;
type CompanySkillListDbRow = Pick<
@ -72,6 +70,12 @@ type CompanySkillListRow = Pick<
| "createdAt"
| "updatedAt"
>;
type CompanySkillReferenceRow = Pick<
CompanySkillRow,
| "id"
| "key"
| "slug"
>;
type SkillReferenceTarget = Pick<CompanySkill, "id" | "key" | "slug">;
type SkillSourceInfoTarget = Pick<
CompanySkill,
@ -147,6 +151,27 @@ type RuntimeSkillEntryOptions = {
const skillInventoryRefreshPromises = new Map<string, Promise<void>>();
function selectCompanySkillColumns() {
return {
id: companySkills.id,
companyId: companySkills.companyId,
key: companySkills.key,
slug: companySkills.slug,
name: companySkills.name,
description: companySkills.description,
markdown: companySkills.markdown,
sourceType: companySkills.sourceType,
sourceLocator: companySkills.sourceLocator,
sourceRef: companySkills.sourceRef,
trustLevel: companySkills.trustLevel,
compatibility: companySkills.compatibility,
fileInventory: companySkills.fileInventory,
metadata: companySkills.metadata,
createdAt: companySkills.createdAt,
updatedAt: companySkills.updatedAt,
};
}
const PROJECT_SCAN_DIRECTORY_ROOTS = [
"skills",
"skills/.curated",
@ -1523,7 +1548,6 @@ function toCompanySkillListItem(skill: CompanySkillListRow, attachedAgentCount:
export function companySkillService(db: Db) {
const agents = agentService(db);
const projects = projectService(db);
const secretsSvc = secretService(db);
async function ensureBundledSkills(companyId: string) {
for (const skillsRoot of resolveBundledSkillsRoot()) {
@ -1553,10 +1577,19 @@ export function companySkillService(db: Db) {
async function pruneMissingLocalPathSkills(companyId: string) {
const rows = await db
.select()
.select({
id: companySkills.id,
key: companySkills.key,
slug: companySkills.slug,
sourceType: companySkills.sourceType,
sourceLocator: companySkills.sourceLocator,
})
.from(companySkills)
.where(eq(companySkills.companyId, companyId));
const skills = rows.map((row) => toCompanySkill(row));
const skills = rows.map((row) => ({
...row,
sourceType: row.sourceType as CompanySkillSourceType,
}));
const missingIds = new Set(await findMissingLocalSkillIds(skills));
if (missingIds.size === 0) return;
@ -1628,25 +1661,37 @@ export function companySkillService(db: Db) {
async function listFull(companyId: string): Promise<CompanySkill[]> {
await ensureSkillInventoryCurrent(companyId);
const rows = await db
.select()
.select(selectCompanySkillColumns())
.from(companySkills)
.where(eq(companySkills.companyId, companyId))
.orderBy(asc(companySkills.name), asc(companySkills.key));
return rows.map((row) => toCompanySkill(row));
}
async function getById(id: string) {
const row = await db
.select()
async function listReferenceTargets(companyId: string): Promise<SkillReferenceTarget[]> {
const rows = await db
.select({
id: companySkills.id,
key: companySkills.key,
slug: companySkills.slug,
})
.from(companySkills)
.where(eq(companySkills.id, id))
.where(eq(companySkills.companyId, companyId));
return rows as CompanySkillReferenceRow[];
}
async function getById(companyId: string, id: string) {
const row = await db
.select(selectCompanySkillColumns())
.from(companySkills)
.where(and(eq(companySkills.companyId, companyId), eq(companySkills.id, id)))
.then((rows) => rows[0] ?? null);
return row ? toCompanySkill(row) : null;
}
async function getByKey(companyId: string, key: string) {
const row = await db
.select()
.select(selectCompanySkillColumns())
.from(companySkills)
.where(and(eq(companySkills.companyId, companyId), eq(companySkills.key, key)))
.then((rows) => rows[0] ?? null);
@ -1654,67 +1699,36 @@ export function companySkillService(db: Db) {
}
async function usage(companyId: string, key: string): Promise<CompanySkillUsageAgent[]> {
const skills = await listFull(companyId);
const skills = await listReferenceTargets(companyId);
const agentRows = await agents.list(companyId);
const desiredAgents = agentRows.filter((agent) => {
const desiredSkills = resolveDesiredSkillKeys(skills, agent.adapterConfig as Record<string, unknown>);
return desiredSkills.includes(key);
});
return Promise.all(
desiredAgents.map(async (agent) => {
const adapter = findActiveServerAdapter(agent.adapterType);
let actualState: string | null = null;
if (!adapter?.listSkills) {
actualState = "unsupported";
} else {
try {
const { config: runtimeConfig } = await secretsSvc.resolveAdapterConfigForRuntime(
agent.companyId,
agent.adapterConfig as Record<string, unknown>,
);
const runtimeSkillEntries = await listRuntimeSkillEntries(agent.companyId);
const snapshot = await adapter.listSkills({
agentId: agent.id,
companyId: agent.companyId,
adapterType: agent.adapterType,
config: {
...runtimeConfig,
paperclipRuntimeSkills: runtimeSkillEntries,
},
});
actualState = snapshot.entries.find((entry) => entry.key === key)?.state
?? (snapshot.supported ? "missing" : "unsupported");
} catch {
actualState = "unknown";
}
}
return {
id: agent.id,
name: agent.name,
urlKey: agent.urlKey,
adapterType: agent.adapterType,
desired: true,
actualState,
};
}),
);
return desiredAgents.map((agent) => ({
id: agent.id,
name: agent.name,
urlKey: agent.urlKey,
adapterType: agent.adapterType,
desired: true,
// Runtime adapter state is intentionally omitted from this bounded metadata read.
actualState: null,
}));
}
async function detail(companyId: string, id: string): Promise<CompanySkillDetail | null> {
await ensureSkillInventoryCurrent(companyId);
const skill = await getById(id);
if (!skill || skill.companyId !== companyId) return null;
const skill = await getById(companyId, id);
if (!skill) return null;
const usedByAgents = await usage(companyId, skill.key);
return enrichSkill(skill, usedByAgents.length, usedByAgents);
}
async function updateStatus(companyId: string, skillId: string): Promise<CompanySkillUpdateStatus | null> {
await ensureSkillInventoryCurrent(companyId);
const skill = await getById(skillId);
if (!skill || skill.companyId !== companyId) return null;
const skill = await getById(companyId, skillId);
if (!skill) return null;
if (skill.sourceType !== "github" && skill.sourceType !== "skills_sh") {
return {
@ -1757,8 +1771,8 @@ export function companySkillService(db: Db) {
async function readFile(companyId: string, skillId: string, relativePath: string): Promise<CompanySkillFileDetail | null> {
await ensureSkillInventoryCurrent(companyId);
const skill = await getById(skillId);
if (!skill || skill.companyId !== companyId) return null;
const skill = await getById(companyId, skillId);
if (!skill) return null;
const normalizedPath = normalizePortablePath(relativePath || "SKILL.md");
const fileEntry = skill.fileInventory.find((entry) => entry.path === normalizedPath);
@ -1855,8 +1869,8 @@ export function companySkillService(db: Db) {
async function updateFile(companyId: string, skillId: string, relativePath: string, content: string): Promise<CompanySkillFileDetail> {
await ensureSkillInventoryCurrent(companyId);
const skill = await getById(skillId);
if (!skill || skill.companyId !== companyId) throw notFound("Skill not found");
const skill = await getById(companyId, skillId);
if (!skill) throw notFound("Skill not found");
const source = deriveSkillSourceInfo(skill);
if (!source.editable || skill.sourceType !== "local_path") {
@ -1895,8 +1909,8 @@ export function companySkillService(db: Db) {
async function installUpdate(companyId: string, skillId: string): Promise<CompanySkill | null> {
await ensureSkillInventoryCurrent(companyId);
const skill = await getById(skillId);
if (!skill || skill.companyId !== companyId) return null;
const skill = await getById(companyId, skillId);
if (!skill) return null;
const status = await updateStatus(companyId, skillId);
if (!status?.supported) {
@ -2136,7 +2150,7 @@ export function companySkillService(db: Db) {
return skillDir;
}
function resolveRuntimeSkillMaterializedPath(companyId: string, skill: CompanySkill) {
function resolveRuntimeSkillMaterializedPath(companyId: string, skill: Pick<CompanySkill, "key" | "slug">) {
const runtimeRoot = path.resolve(resolveManagedSkillsRoot(companyId), "__runtime__");
return path.resolve(runtimeRoot, buildSkillRuntimeName(skill.key, skill.slug));
}