From e800924d3459a207ba6d68e5ad78db7ecce909c2 Mon Sep 17 00:00:00 2001 From: Arnaud Moncel Date: Tue, 5 May 2026 15:18:45 +0200 Subject: [PATCH 1/3] fix(agent): avoid concurent restart lead to an empty schema sendig --- packages/agent/src/agent.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/agent/src/agent.ts b/packages/agent/src/agent.ts index edad2de466..54a4a96791 100644 --- a/packages/agent/src/agent.ts +++ b/packages/agent/src/agent.ts @@ -50,6 +50,8 @@ export default class Agent extends FrameworkMounter private mcpEnabled = false; private mcpEnabledTools?: ToolName[]; + private isRestarting = false; + /** * Create a new Agent Builder. * If any options are missing, the default will be applied: @@ -111,11 +113,23 @@ export default class Agent extends FrameworkMounter * Restart the agent at runtime (remount routes). */ async restart(): Promise { + if (this.isRestarting) { + this.options.logger('Debug', 'Agent is already restarting. Do nothing.'); + + return; + } + + this.options.logger('Info', `Agent is restarting...`); + + this.isRestarting = true; + // We force sending schema when restarting const { router, mcpHttpCallback } = await this.buildRouterAndSendSchema(); this.setMcpCallback(mcpHttpCallback ?? null); await this.remount(router); + + this.isRestarting = false; } /** From 2afd9e5cc33e3bc4bf3b7f44ad88e33778f7f558 Mon Sep 17 00:00:00 2001 From: Arnaud Moncel Date: Tue, 5 May 2026 15:24:00 +0200 Subject: [PATCH 2/3] test: add test --- packages/agent/test/agent.test.ts | 63 +++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/packages/agent/test/agent.test.ts b/packages/agent/test/agent.test.ts index 7d41be47cf..0d5c515094 100644 --- a/packages/agent/test/agent.test.ts +++ b/packages/agent/test/agent.test.ts @@ -223,6 +223,69 @@ describe('Agent', () => { expect(DataSourceCustomizer.prototype.use).toHaveBeenCalledTimes(2); }); + + describe('when a restart is already in progress', () => { + test('should ignore concurrent restart calls and not rebuild routes again', async () => { + const mockLogger = jest.fn(); + const agent = new Agent({ ...options, logger: mockLogger }); + + await agent.start(); + jest.clearAllMocks(); + + // Make the next getDataSource call hang so the first restart is still + // in progress when the second one is triggered. + let releaseFirstRestart: () => void = () => {}; + jest + .mocked(DataSourceCustomizer.prototype.getDataSource) + .mockImplementationOnce( + () => + new Promise(resolve => { + releaseFirstRestart = () => resolve(factories.dataSource.build()); + }), + ); + + const firstRestart = agent.restart(); + const secondRestart = agent.restart(); + + releaseFirstRestart(); + await Promise.all([firstRestart, secondRestart]); + + expect(DataSourceCustomizer.prototype.getDataSource).toHaveBeenCalledTimes(1); + expect(mockSetupRoute).toHaveBeenCalledTimes(1); + expect(mockBootstrap).toHaveBeenCalledTimes(1); + expect(mockMakeRoutes).toHaveBeenCalledTimes(1); + expect(mockLogger).toHaveBeenCalledWith( + 'Debug', + 'Agent is already restarting. Do nothing.', + ); + }); + + test('should allow a new restart once the previous one has finished', async () => { + const agent = new Agent(options); + + await agent.start(); + jest.clearAllMocks(); + + await agent.restart(); + await agent.restart(); + + expect(DataSourceCustomizer.prototype.getDataSource).toHaveBeenCalledTimes(2); + expect(mockSetupRoute).toHaveBeenCalledTimes(2); + expect(mockBootstrap).toHaveBeenCalledTimes(2); + }); + + test('should log that the agent is restarting on each accepted restart', async () => { + const mockLogger = jest.fn(); + const agent = new Agent({ ...options, logger: mockLogger }); + + await agent.start(); + mockLogger.mockClear(); + + await agent.restart(); + + expect(mockLogger).toHaveBeenCalledWith('Info', 'Agent is restarting...'); + }); + }); }); describe('Production', () => { From d858470fd4eaa0c86cc32ac70321ceda2ad6c03c Mon Sep 17 00:00:00 2001 From: Arnaud Moncel Date: Tue, 5 May 2026 15:42:06 +0200 Subject: [PATCH 3/3] fix: review --- packages/agent/src/agent.ts | 14 ++++++++------ packages/agent/test/agent.test.ts | 15 +++++++-------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/packages/agent/src/agent.ts b/packages/agent/src/agent.ts index 54a4a96791..3c88834f69 100644 --- a/packages/agent/src/agent.ts +++ b/packages/agent/src/agent.ts @@ -123,13 +123,15 @@ export default class Agent extends FrameworkMounter this.isRestarting = true; - // We force sending schema when restarting - const { router, mcpHttpCallback } = await this.buildRouterAndSendSchema(); - - this.setMcpCallback(mcpHttpCallback ?? null); - await this.remount(router); + try { + // We force sending schema when restarting + const { router, mcpHttpCallback } = await this.buildRouterAndSendSchema(); - this.isRestarting = false; + this.setMcpCallback(mcpHttpCallback ?? null); + await this.remount(router); + } finally { + this.isRestarting = false; + } } /** diff --git a/packages/agent/test/agent.test.ts b/packages/agent/test/agent.test.ts index 0d5c515094..dce11a9326 100644 --- a/packages/agent/test/agent.test.ts +++ b/packages/agent/test/agent.test.ts @@ -235,14 +235,13 @@ describe('Agent', () => { // Make the next getDataSource call hang so the first restart is still // in progress when the second one is triggered. let releaseFirstRestart: () => void = () => {}; - jest - .mocked(DataSourceCustomizer.prototype.getDataSource) - .mockImplementationOnce( - () => - new Promise(resolve => { - releaseFirstRestart = () => resolve(factories.dataSource.build()); - }), - ); + + jest.mocked(DataSourceCustomizer.prototype.getDataSource).mockImplementationOnce( + () => + new Promise(resolve => { + releaseFirstRestart = () => resolve(factories.dataSource.build()); + }), + ); const firstRestart = agent.restart(); const secondRestart = agent.restart();