From f9927bdaaa7e536e64d694e4c78fb86295a57897 Mon Sep 17 00:00:00 2001 From: dotta Date: Mon, 23 Mar 2026 16:30:28 -0500 Subject: [PATCH] Disable imported timer heartbeats Prevent company imports from re-enabling scheduler heartbeats on imported agents and cover both new-company and existing-company import flows in portability tests. Co-Authored-By: Paperclip --- .../src/__tests__/company-portability.test.ts | 60 +++++++++++++++++++ server/src/services/company-portability.ts | 10 +++- 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/server/src/__tests__/company-portability.test.ts b/server/src/__tests__/company-portability.test.ts index fb9a4497..fdf0f9b9 100644 --- a/server/src/__tests__/company-portability.test.ts +++ b/server/src/__tests__/company-portability.test.ts @@ -1832,6 +1832,61 @@ describe("company portability", () => { }); }); + it("disables timer heartbeats on imported agents", async () => { + const portability = companyPortabilityService({} as any); + + companySvc.create.mockResolvedValue({ + id: "company-imported", + name: "Imported Paperclip", + }); + agentSvc.create.mockImplementation(async (_companyId: string, input: Record) => ({ + id: `agent-${String(input.name).toLowerCase()}`, + name: input.name, + adapterConfig: input.adapterConfig, + runtimeConfig: input.runtimeConfig, + })); + + const exported = await portability.exportBundle("company-1", { + include: { + company: true, + agents: true, + projects: false, + issues: false, + }, + }); + + agentSvc.list.mockResolvedValue([]); + + await portability.importBundle({ + source: { + type: "inline", + rootPath: exported.rootPath, + files: exported.files, + }, + include: { + company: true, + agents: true, + projects: false, + issues: false, + }, + target: { + mode: "new_company", + newCompanyName: "Imported Paperclip", + }, + agents: "all", + collisionStrategy: "rename", + }, "user-1"); + + const createdClaude = agentSvc.create.mock.calls.find(([, input]) => input.name === "ClaudeCoder"); + expect(createdClaude?.[1]).toMatchObject({ + runtimeConfig: { + heartbeat: { + enabled: false, + }, + }, + }); + }); + it("imports only selected files and leaves unchecked company metadata alone", async () => { const portability = companyPortabilityService({} as any); @@ -1902,6 +1957,11 @@ describe("company portability", () => { expect(agentSvc.create).toHaveBeenCalledTimes(1); expect(agentSvc.create).toHaveBeenCalledWith("company-1", expect.objectContaining({ name: "CMO", + runtimeConfig: { + heartbeat: { + enabled: false, + }, + }, })); expect(result.company.action).toBe("unchanged"); expect(result.agents).toEqual([ diff --git a/server/src/services/company-portability.ts b/server/src/services/company-portability.ts index 21beeb2f..fb8f2b2e 100644 --- a/server/src/services/company-portability.ts +++ b/server/src/services/company-portability.ts @@ -619,6 +619,14 @@ function clonePortableRecord(value: unknown) { return structuredClone(value) as Record; } +function disableImportedTimerHeartbeat(runtimeConfig: unknown) { + const next = clonePortableRecord(runtimeConfig) ?? {}; + const heartbeat = isPlainRecord(next.heartbeat) ? { ...next.heartbeat } : {}; + heartbeat.enabled = false; + next.heartbeat = heartbeat; + return next; +} + function normalizePortableProjectWorkspaceExtension( workspaceKey: string, value: unknown, @@ -3853,7 +3861,7 @@ export function companyPortabilityService(db: Db, storage?: StorageService) { reportsTo: null, adapterType: effectiveAdapterType, adapterConfig: adapterConfigWithSkills, - runtimeConfig: manifestAgent.runtimeConfig, + runtimeConfig: disableImportedTimerHeartbeat(manifestAgent.runtimeConfig), budgetMonthlyCents: manifestAgent.budgetMonthlyCents, permissions: manifestAgent.permissions, metadata: manifestAgent.metadata,