From 20e55849ebb5adf91c913e03c247c4aa7fd53fcd Mon Sep 17 00:00:00 2001 From: hzhang Date: Tue, 26 May 2026 16:48:53 +0100 Subject: [PATCH] fix(channel): add describeAccount so health-monitor sees real configured MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit openclaw's `channelManager.getRuntimeSnapshot()` — called every minute by the channel-health-monitor — runs accounts through `applyDescribedAccountFields(next, plugin.config.describeAccount?.(...))`. When the callback is missing it defaults `configured: true`. Fabric never defined it, so every health-monitor cycle: snapshot = { enabled: true, configured: true, running: false } For fabric's synthetic 'default' account (returned by `listFabricAccountIds` when `channels.fabric.accounts` is empty — the prod shape, where per-agent api-keys live in `~/.openclaw/fabric-identity.json` and the channel framework never runs `startAccount` so `running` stays false): isManagedAccount({enabled:true, configured:true}) === true -> not-running -> 'stopped' -> restart every ~10 min, logging '[fabric:default] health-monitor: restarting (reason: stopped)' The restart is a no-op (fabric's `gateway.startAccount` is absent so `startChannelInternal` returns early), but the log is loud and operators chasing real outages keep wasting time on it. Mirror `isConfigured` from describeAccount so the snapshot truthfully reports configured:false for any account without a fabricApiKey. The fabric plugin still self-manages real agents via `gateway_start` -> `FabricInbound.start()`; the framework just no longer thinks 'default' is something it should restart. Verified in sim (this patch alone, no debug instrumentation): - gateway up 8+ minutes, 0 restart events - pre-patch sim with same config restarted at 5min mark - evaluateChannelHealth snapshot for both 'default' and 'recruiter' accountId reads configured:false (instrumented with temporary console.log in channel-health-policy, since reverted) Co-Authored-By: Claude Opus 4.7 (1M context) --- dist/fabric/src/channel.js | 13 +++++++++++++ src/channel.ts | 13 +++++++++++++ 2 files changed, 26 insertions(+) diff --git a/dist/fabric/src/channel.js b/dist/fabric/src/channel.js index bca9610..a5f841d 100644 --- a/dist/fabric/src/channel.js +++ b/dist/fabric/src/channel.js @@ -117,6 +117,19 @@ export const fabricChannelPlugin = createChatChannelPlugin({ resolveAccount: (cfg, accountId) => resolveFabricAccount(cfg, accountId), defaultAccountId: (cfg) => resolveDefaultFabricAccountId(cfg), isConfigured: (account) => Boolean(account.fabricApiKey), + // openclaw's channelManager.getRuntimeSnapshot() — called every minute + // by the channel-health-monitor — defaults `configured: true` when the + // plugin doesn't expose describeAccount (see applyDescribedAccountFields + // in server-channels). Without this, fabric's synthetic 'default' + // account (returned by listFabricAccountIds when channels.fabric.accounts + // is empty — the prod shape) gets snapshot {enabled:true, configured:true, + // running:false} → isManagedAccount=true → not-running → restart loop + // every ~10 min, logging `[fabric:default] health-monitor: restarting`. + // Mirror isConfigured here so the snapshot truthfully reports false for + // any account without a fabricApiKey. + describeAccount: (account) => ({ + configured: Boolean(account.fabricApiKey), + }), }, // Minimal setup adapter: Fabric is configured directly under // channels.fabric.* (no interactive wizard). applyAccountConfig is the diff --git a/src/channel.ts b/src/channel.ts index 8fb58c0..09ef50c 100644 --- a/src/channel.ts +++ b/src/channel.ts @@ -153,6 +153,19 @@ export const fabricChannelPlugin = createChatChannelPlugin resolveFabricAccount(cfg as never, accountId), defaultAccountId: (cfg) => resolveDefaultFabricAccountId(cfg as never), isConfigured: (account: ResolvedFabricAccount) => Boolean(account.fabricApiKey), + // openclaw's channelManager.getRuntimeSnapshot() — called every minute + // by the channel-health-monitor — defaults `configured: true` when the + // plugin doesn't expose describeAccount (see applyDescribedAccountFields + // in server-channels). Without this, fabric's synthetic 'default' + // account (returned by listFabricAccountIds when channels.fabric.accounts + // is empty — the prod shape) gets snapshot {enabled:true, configured:true, + // running:false} → isManagedAccount=true → not-running → restart loop + // every ~10 min, logging `[fabric:default] health-monitor: restarting`. + // Mirror isConfigured here so the snapshot truthfully reports false for + // any account without a fabricApiKey. + describeAccount: (account: ResolvedFabricAccount) => ({ + configured: Boolean(account.fabricApiKey), + }), }, // Minimal setup adapter: Fabric is configured directly under // channels.fabric.* (no interactive wizard). applyAccountConfig is the