From c19891926b774b3b3b7b80b7229191bb7b730e3d Mon Sep 17 00:00:00 2001 From: Simon <10131203+gaomeng1900@users.noreply.github.com> Date: Thu, 11 Jun 2026 19:15:48 +0800 Subject: [PATCH] refactor(ext): drive heartbeat and running flag from statuschange Project agent status into chrome.storage via a statuschange listener instead of pairing setup/teardown across lifecycle hooks. A throwing hook can no longer leak the heartbeat or strand isAgentRunning, and rejected concurrent execute() calls never touch the active run's state. --- .../extension/src/agent/MultiPageAgent.ts | 66 ++++++++----------- 1 file changed, 27 insertions(+), 39 deletions(-) diff --git a/packages/extension/src/agent/MultiPageAgent.ts b/packages/extension/src/agent/MultiPageAgent.ts index aa768bc..2418782 100644 --- a/packages/extension/src/agent/MultiPageAgent.ts +++ b/packages/extension/src/agent/MultiPageAgent.ts @@ -39,15 +39,6 @@ export class MultiPageAgent extends PageAgentCore { const includeInitialTab = config.includeInitialTab ?? true const experimentalIncludeAllTabs = config.experimentalIncludeAllTabs ?? false - /** - * When the agent is in side-panel and user closed the side-panel. - * There is no chance for isAgentRunning to be set false. - * (unload event doesn't work well in side panel.) - * (I'm trying not to use long-lived connection because the lifecycle of a sw is hard to predict.) - * This heartbeat mechanism acts as a backup. - */ - let heartBeatInterval: null | number = null - super({ ...config, pageController: pageController as any, @@ -56,27 +47,6 @@ export class MultiPageAgent extends PageAgentCore { onBeforeTask: async (agent) => { await tabsController.init(agent.task, { includeInitialTab, experimentalIncludeAllTabs }) - - heartBeatInterval = window.setInterval(() => { - chrome.storage.local.set({ - agentHeartbeat: Date.now(), - }) - }, 1_000) - - await chrome.storage.local.set({ - isAgentRunning: true, - }) - }, - - onAfterTask: async () => { - if (heartBeatInterval) { - window.clearInterval(heartBeatInterval) - heartBeatInterval = null - } - - await chrome.storage.local.set({ - isAgentRunning: false, - }) }, onBeforeStep: async (agent) => { @@ -86,17 +56,35 @@ export class MultiPageAgent extends PageAgentCore { }, onDispose: () => { - if (heartBeatInterval) { - window.clearInterval(heartBeatInterval) - heartBeatInterval = null - } - - chrome.storage.local.set({ - isAgentRunning: false, - }) - tabsController.dispose() }, }) + + /** + * Project agent status into chrome.storage. The content script polls + * `isAgentRunning` + `agentHeartbeat` (eventually consistent by design). + * + * When the agent is in side-panel and user closed the side-panel. + * There is no chance for isAgentRunning to be set false. + * (unload event doesn't work well in side panel.) + * (I'm trying not to use long-lived connection because the lifecycle of a sw is hard to predict.) + * This heartbeat mechanism acts as a backup. + */ + let heartBeatInterval: number | null = null + + this.addEventListener('statuschange', () => { + const running = this.status === 'running' + + if (running && !heartBeatInterval) { + heartBeatInterval = window.setInterval(() => { + void chrome.storage.local.set({ agentHeartbeat: Date.now() }) + }, 1_000) + } else if (!running && heartBeatInterval) { + clearInterval(heartBeatInterval) + heartBeatInterval = null + } + + chrome.storage.local.set({ isAgentRunning: running }).catch(console.error) + }) } }