From fdfc129bc7e0898a3f41f727f568583b0ecb8c12 Mon Sep 17 00:00:00 2001 From: matthew-pilot Date: Thu, 28 May 2026 11:24:41 +0000 Subject: [PATCH] fix(claw-pilot): stopAccount before restart on config change (PILOT-188) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit onAccountConfigChanged stopped the transport but left the account in PilotLifecycle.accounts, so the subsequent startAll → startAccount call threw "already running". The error was caught and routed to scheduleRetry, causing live config changes to be silently lost until gateway restart. Added PilotLifecycle.stopAccount() that stops transport + pipeline + drainTimer and removes the account from the map in a single idempotent operation. Replaced the manual transport.stop() in onAccountConfigChanged with stopAccount(). Note: Bug 1 (handshakeTrustAutoApprove dead config) confirmed but deferred — removing the flag touches 5 files (types, resolver, schema, 3 test files). Operator should split to a follow-up or handle manually. Closes PILOT-188 --- plugin/src/channel-plugin-api.ts | 5 +---- plugin/src/lifecycle.ts | 13 +++++++++++++ 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/plugin/src/channel-plugin-api.ts b/plugin/src/channel-plugin-api.ts index 7093a98..f9a1bd6 100644 --- a/plugin/src/channel-plugin-api.ts +++ b/plugin/src/channel-plugin-api.ts @@ -135,10 +135,7 @@ export function buildPilotChannelPlugin(deps: BuildPilotPluginDeps): PilotChanne accountId: string; }) => { deps.logger.info("pilot channel: account changed, reloading", { accountId }); - const acct = lifecycle.getAccount(accountId); - if (acct) { - await acct.transport.stop(); - } + await lifecycle.stopAccount(accountId); await lifecycle.startAll(nextCfg); }, onAccountRemoved: async ({ accountId }: { accountId: string }) => { diff --git a/plugin/src/lifecycle.ts b/plugin/src/lifecycle.ts index 547b449..a33fe92 100644 --- a/plugin/src/lifecycle.ts +++ b/plugin/src/lifecycle.ts @@ -275,6 +275,19 @@ export class PilotLifecycle { return this.accounts.get(accountId)?.outbox; } + /** Stop a single account and remove it from the live map. */ + async stopAccount(accountId: string): Promise { + const state = this.accounts.get(accountId); + if (!state) return; + try { + if (state.drainTimer) clearInterval(state.drainTimer); + state.pipeline.stop(); + await state.transport.stop(); + } finally { + this.accounts.delete(accountId); + } + } + async stopAll(): Promise { const tasks: Promise[] = []; for (const [id, state] of this.accounts) {