From 1adfd30b3be2dd3d8fa0aa54d7b2ad554d067f27 Mon Sep 17 00:00:00 2001 From: dotta Date: Mon, 23 Mar 2026 07:21:34 -0500 Subject: [PATCH] fix: recover managed agent instructions from disk Co-Authored-By: Paperclip --- .../agent-instructions-service.test.ts | 31 +++++++++++++ server/src/services/agent-instructions.ts | 44 ++++++++++++++++--- 2 files changed, 68 insertions(+), 7 deletions(-) diff --git a/server/src/__tests__/agent-instructions-service.test.ts b/server/src/__tests__/agent-instructions-service.test.ts index 0e0d9d39..a0731ee6 100644 --- a/server/src/__tests__/agent-instructions-service.test.ts +++ b/server/src/__tests__/agent-instructions-service.test.ts @@ -161,4 +161,35 @@ describe("agent instructions service", () => { "docs/TOOLS.md", ]); }); + + it("recovers a managed bundle from disk when bundle config metadata is missing", async () => { + const paperclipHome = await makeTempDir("paperclip-agent-instructions-recover-"); + cleanupDirs.add(paperclipHome); + process.env.PAPERCLIP_HOME = paperclipHome; + process.env.PAPERCLIP_INSTANCE_ID = "test-instance"; + + const managedRoot = path.join( + paperclipHome, + "instances", + "test-instance", + "companies", + "company-1", + "agents", + "agent-1", + "instructions", + ); + await fs.mkdir(managedRoot, { recursive: true }); + await fs.writeFile(path.join(managedRoot, "AGENTS.md"), "# Recovered Agent\n", "utf8"); + + const svc = agentInstructionsService(); + const agent = makeAgent({}); + + const bundle = await svc.getBundle(agent); + const exported = await svc.exportFiles(agent); + + expect(bundle.mode).toBe("managed"); + expect(bundle.rootPath).toBe(managedRoot); + expect(bundle.files.map((file) => file.path)).toEqual(["AGENTS.md"]); + expect(exported.files).toEqual({ "AGENTS.md": "# Recovered Agent\n" }); + }); }); diff --git a/server/src/services/agent-instructions.ts b/server/src/services/agent-instructions.ts index d3fc7008..1e1a5b32 100644 --- a/server/src/services/agent-instructions.ts +++ b/server/src/services/agent-instructions.ts @@ -272,6 +272,24 @@ function deriveBundleState(agent: AgentLike): BundleState { }; } +async function recoverManagedBundleState(agent: AgentLike, state: BundleState): Promise { + if (state.rootPath) return state; + + const managedRootPath = resolveManagedInstructionsRoot(agent); + const stat = await statIfExists(managedRootPath); + if (!stat?.isDirectory()) return state; + + const files = await listFilesRecursive(managedRootPath); + if (files.length === 0) return state; + + return { + ...state, + mode: "managed", + rootPath: managedRootPath, + resolvedEntryPath: path.resolve(managedRootPath, state.entryFile), + }; +} + function toBundle(agent: AgentLike, state: BundleState, files: AgentInstructionsFileSummary[]): AgentInstructionsBundle { const nextFiles = [...files]; if (state.legacyPromptTemplateActive && !nextFiles.some((file) => file.path === LEGACY_PROMPT_TEMPLATE_PATH)) { @@ -366,7 +384,7 @@ export function syncInstructionsBundleConfigFromFilePath( export function agentInstructionsService() { async function getBundle(agent: AgentLike): Promise { - const state = deriveBundleState(agent); + const state = await recoverManagedBundleState(agent, deriveBundleState(agent)); if (!state.rootPath) return toBundle(agent, state, []); const stat = await statIfExists(state.rootPath); if (!stat?.isDirectory()) { @@ -381,7 +399,7 @@ export function agentInstructionsService() { } async function readFile(agent: AgentLike, relativePath: string): Promise { - const state = deriveBundleState(agent); + const state = await recoverManagedBundleState(agent, deriveBundleState(agent)); if (relativePath === LEGACY_PROMPT_TEMPLATE_PATH) { const content = asString(state.config[PROMPT_KEY]); if (content === null) throw notFound("Instructions file not found"); @@ -422,9 +440,21 @@ export function agentInstructionsService() { agent: AgentLike, options?: { clearLegacyPromptTemplate?: boolean }, ): Promise<{ adapterConfig: Record; state: BundleState }> { - const current = deriveBundleState(agent); + const derived = deriveBundleState(agent); + const current = await recoverManagedBundleState(agent, derived); if (current.rootPath && current.mode) { - return { adapterConfig: current.config, state: current }; + const adapterConfig = derived.rootPath + ? current.config + : applyBundleConfig(current.config, { + mode: current.mode, + rootPath: current.rootPath, + entryFile: current.entryFile, + clearLegacyPromptTemplate: options?.clearLegacyPromptTemplate, + }); + return { + adapterConfig, + state: deriveBundleState({ ...agent, adapterConfig }), + }; } const managedRoot = resolveManagedInstructionsRoot(agent); @@ -462,7 +492,7 @@ export function agentInstructionsService() { clearLegacyPromptTemplate?: boolean; }, ): Promise<{ bundle: AgentInstructionsBundle; adapterConfig: Record }> { - const state = deriveBundleState(agent); + const state = await recoverManagedBundleState(agent, deriveBundleState(agent)); const nextMode = input.mode ?? state.mode ?? "managed"; const nextEntryFile = input.entryFile ? normalizeRelativeFilePath(input.entryFile) : state.entryFile; let nextRootPath: string; @@ -544,7 +574,7 @@ export function agentInstructionsService() { bundle: AgentInstructionsBundle; adapterConfig: Record; }> { - const state = deriveBundleState(agent); + const state = await recoverManagedBundleState(agent, deriveBundleState(agent)); if (relativePath === LEGACY_PROMPT_TEMPLATE_PATH) { throw unprocessable("Cannot delete the legacy promptTemplate pseudo-file"); } @@ -564,7 +594,7 @@ export function agentInstructionsService() { entryFile: string; warnings: string[]; }> { - const state = deriveBundleState(agent); + const state = await recoverManagedBundleState(agent, deriveBundleState(agent)); if (state.rootPath) { const stat = await statIfExists(state.rootPath); if (stat?.isDirectory()) {